From a7b46a975d2a9bac7fe2ed40f9af18972e6e2784 Mon Sep 17 00:00:00 2001 From: Constance Date: Fri, 5 Feb 2021 12:26:19 -0800 Subject: [PATCH 01/51] Update eslint-plugin-import to latest (#90483) -to grab fixes, case-sensitivity, etc. --- package.json | 2 +- yarn.lock | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fc5cd02a03253d..4afe7a579ad458 100644 --- a/package.json +++ b/package.json @@ -636,7 +636,7 @@ "eslint-plugin-ban": "^1.4.0", "eslint-plugin-cypress": "^2.11.2", "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-import": "^2.19.1", + "eslint-plugin-import": "^2.22.1", "eslint-plugin-jest": "^24.0.2", "eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-mocha": "^6.2.2", diff --git a/yarn.lock b/yarn.lock index 24fe6463fa41c0..e4f5fc2e1a8e16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5968,6 +5968,11 @@ resolved "https://registry.yarnpkg.com/@types/json-stable-stringify/-/json-stable-stringify-1.0.32.tgz#121f6917c4389db3923640b2e68de5fa64dda88e" integrity sha512-q9Q6+eUEGwQkv4Sbst3J4PNgDOvpuVuKj79Hl/qnmBMEIPzB5QoFRUtjcgcg2xNUZyYUGXBk5wYIBKHt0A+Mxw== +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + "@types/json5@^0.0.30": version "0.0.30" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.30.tgz#44cb52f32a809734ca562e685c6473b5754a7818" @@ -13614,6 +13619,14 @@ eslint-import-resolver-node@0.3.2, eslint-import-resolver-node@^0.3.2: debug "^2.6.9" resolve "^1.5.0" +eslint-import-resolver-node@^0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" + integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA== + dependencies: + debug "^2.6.9" + resolve "^1.13.1" + eslint-import-resolver-webpack@0.11.1: version "0.11.1" resolved "https://registry.yarnpkg.com/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.11.1.tgz#fcf1fd57a775f51e18f442915f85dd6ba45d2f26" @@ -13638,6 +13651,14 @@ eslint-module-utils@2.5.0, eslint-module-utils@^2.4.1: debug "^2.6.9" pkg-dir "^2.0.0" +eslint-module-utils@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6" + integrity sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA== + dependencies: + debug "^2.6.9" + pkg-dir "^2.0.0" + eslint-plugin-babel@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/eslint-plugin-babel/-/eslint-plugin-babel-5.3.1.tgz#75a2413ffbf17e7be57458301c60291f2cfbf560" @@ -13693,6 +13714,25 @@ eslint-plugin-import@^2.19.1: read-pkg-up "^2.0.0" resolve "^1.12.0" +eslint-plugin-import@^2.22.1: + version "2.22.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz#0896c7e6a0cf44109a2d97b95903c2bb689d7702" + integrity sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw== + dependencies: + array-includes "^3.1.1" + array.prototype.flat "^1.2.3" + contains-path "^0.1.0" + debug "^2.6.9" + doctrine "1.5.0" + eslint-import-resolver-node "^0.3.4" + eslint-module-utils "^2.6.0" + has "^1.0.3" + minimatch "^3.0.4" + object.values "^1.1.1" + read-pkg-up "^2.0.0" + resolve "^1.17.0" + tsconfig-paths "^3.9.0" + eslint-plugin-jest@^24.0.2: version "24.0.2" resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-24.0.2.tgz#4bf0fcdc86289d702a7dacb430b4363482af773b" @@ -25534,7 +25574,7 @@ resolve@1.8.1: dependencies: path-parse "^1.0.5" -resolve@^1.1.10, resolve@^1.1.4, resolve@^1.1.5, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.7.1, resolve@^1.8.1: +resolve@^1.1.10, resolve@^1.1.4, resolve@^1.1.5, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.7.1, resolve@^1.8.1: version "1.19.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== @@ -28427,6 +28467,16 @@ ts-pnp@^1.1.6: resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== +tsconfig-paths@^3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" + integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.0" + strip-bom "^3.0.0" + tsd@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/tsd/-/tsd-0.13.1.tgz#d2a8baa80b8319dafea37fbeb29fef3cec86e92b" From fc516bacbd367baea0b06447c3710f693ebab06d Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 5 Feb 2021 14:13:51 -0700 Subject: [PATCH 02/51] [index patterns] Add pattern validation method to index patterns fetcher (#90170) --- ...lugins-data-server.indexpatternsfetcher.md | 1 + ...tternsfetcher.validatepatternlistactive.md | 24 ++++ .../fetcher/index_patterns_fetcher.test.ts | 72 ++++++++++++ .../fetcher/index_patterns_fetcher.ts | 40 ++++++- src/plugins/data/server/server.api.md | 1 + .../fields_for_wildcard_route/response.js | 110 ++++++++++-------- 6 files changed, 197 insertions(+), 51 deletions(-) create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.validatepatternlistactive.md create mode 100644 src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.test.ts diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md index 3ba3c862bf16a9..608d738676bcfd 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.md @@ -22,4 +22,5 @@ export declare class IndexPatternsFetcher | --- | --- | --- | | [getFieldsForTimePattern(options)](./kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsfortimepattern.md) | | Get a list of field objects for a time pattern | | [getFieldsForWildcard(options)](./kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md) | | Get a list of field objects for an index pattern that may contain wildcards | +| [validatePatternListActive(patternList)](./kibana-plugin-plugins-data-server.indexpatternsfetcher.validatepatternlistactive.md) | | Returns an index pattern list of only those index pattern strings in the given list that return indices | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.validatepatternlistactive.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.validatepatternlistactive.md new file mode 100644 index 00000000000000..8944c412043237 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.validatepatternlistactive.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsFetcher](./kibana-plugin-plugins-data-server.indexpatternsfetcher.md) > [validatePatternListActive](./kibana-plugin-plugins-data-server.indexpatternsfetcher.validatepatternlistactive.md) + +## IndexPatternsFetcher.validatePatternListActive() method + +Returns an index pattern list of only those index pattern strings in the given list that return indices + +Signature: + +```typescript +validatePatternListActive(patternList: string[]): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| patternList | string[] | | + +Returns: + +`Promise` + diff --git a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.test.ts b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.test.ts new file mode 100644 index 00000000000000..ffdd47e5cdf490 --- /dev/null +++ b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.test.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IndexPatternsFetcher } from '.'; +import { ElasticsearchClient } from 'kibana/server'; +import * as indexNotFoundException from '../../../common/search/test_data/index_not_found_exception.json'; + +describe('Index Pattern Fetcher - server', () => { + let indexPatterns: IndexPatternsFetcher; + let esClient: ElasticsearchClient; + const emptyResponse = { + body: { + count: 0, + }, + }; + const response = { + body: { + count: 1115, + }, + }; + const patternList = ['a', 'b', 'c']; + beforeEach(() => { + esClient = ({ + count: jest.fn().mockResolvedValueOnce(emptyResponse).mockResolvedValue(response), + } as unknown) as ElasticsearchClient; + indexPatterns = new IndexPatternsFetcher(esClient); + }); + + it('Removes pattern without matching indices', async () => { + const result = await indexPatterns.validatePatternListActive(patternList); + expect(result).toEqual(['b', 'c']); + }); + + it('Returns all patterns when all match indices', async () => { + esClient = ({ + count: jest.fn().mockResolvedValue(response), + } as unknown) as ElasticsearchClient; + indexPatterns = new IndexPatternsFetcher(esClient); + const result = await indexPatterns.validatePatternListActive(patternList); + expect(result).toEqual(patternList); + }); + it('Removes pattern when "index_not_found_exception" error is thrown', async () => { + class ServerError extends Error { + public body?: Record; + constructor( + message: string, + public readonly statusCode: number, + errBody?: Record + ) { + super(message); + this.body = errBody; + } + } + + esClient = ({ + count: jest + .fn() + .mockResolvedValueOnce(response) + .mockRejectedValue( + new ServerError('index_not_found_exception', 404, indexNotFoundException) + ), + } as unknown) as ElasticsearchClient; + indexPatterns = new IndexPatternsFetcher(esClient); + const result = await indexPatterns.validatePatternListActive(patternList); + expect(result).toEqual([patternList[0]]); + }); +}); diff --git a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts index cc8bfe28bbc9a7..3acdde33f599e0 100644 --- a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts +++ b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts @@ -58,9 +58,16 @@ export class IndexPatternsFetcher { rollupIndex?: string; }): Promise { const { pattern, metaFields, fieldCapsOptions, type, rollupIndex } = options; + const patternList = Array.isArray(pattern) ? pattern : pattern.split(','); + let patternListActive: string[] = patternList; + // if only one pattern, don't bother with validation. We let getFieldCapabilities fail if the single pattern is bad regardless + if (patternList.length > 1) { + patternListActive = await this.validatePatternListActive(patternList); + } const fieldCapsResponse = await getFieldCapabilities( this.elasticsearchClient, - pattern, + // if none of the patterns are active, pass the original list to get an error + patternListActive.length > 0 ? patternListActive : patternList, metaFields, { allow_no_indices: fieldCapsOptions @@ -68,6 +75,7 @@ export class IndexPatternsFetcher { : this.allowNoIndices, } ); + if (type === 'rollup' && rollupIndex) { const rollupFields: FieldDescriptor[] = []; const rollupIndexCapabilities = getCapabilitiesForRollupIndices( @@ -118,4 +126,34 @@ export class IndexPatternsFetcher { } return await getFieldCapabilities(this.elasticsearchClient, indices, metaFields); } + + /** + * Returns an index pattern list of only those index pattern strings in the given list that return indices + * + * @param patternList string[] + * @return {Promise} + */ + async validatePatternListActive(patternList: string[]) { + const result = await Promise.all( + patternList + .map((pattern) => + this.elasticsearchClient.count({ + index: pattern, + }) + ) + .map((p) => + p.catch((e) => { + if (e.body.error.type === 'index_not_found_exception') { + return { body: { count: 0 } }; + } + throw e; + }) + ) + ); + return result.reduce( + (acc: string[], { body: { count } }, patternListIndex) => + count > 0 ? [...acc, patternList[patternListIndex]] : acc, + [] + ); + } } diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 68582a9d877e92..3b1440f211bfed 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -885,6 +885,7 @@ export class IndexPatternsFetcher { type?: string; rollupIndex?: string; }): Promise; + validatePatternListActive(patternList: string[]): Promise; } // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStart" needs to be exported by the entry point index.d.ts diff --git a/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js b/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js index e84052e58dac48..87c5aa535ccd98 100644 --- a/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js +++ b/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js @@ -17,6 +17,55 @@ export default function ({ getService }) { expect(resp.body.fields).to.eql(sortBy(resp.body.fields, 'name')); }; + const testFields = [ + { + type: 'boolean', + esTypes: ['boolean'], + searchable: true, + aggregatable: true, + name: 'bar', + readFromDocValues: true, + }, + { + type: 'string', + esTypes: ['text'], + searchable: true, + aggregatable: false, + name: 'baz', + readFromDocValues: false, + }, + { + type: 'string', + esTypes: ['keyword'], + searchable: true, + aggregatable: true, + name: 'baz.keyword', + readFromDocValues: true, + subType: { multi: { parent: 'baz' } }, + }, + { + type: 'number', + esTypes: ['long'], + searchable: true, + aggregatable: true, + name: 'foo', + readFromDocValues: true, + }, + { + aggregatable: true, + esTypes: ['keyword'], + name: 'nestedField.child', + readFromDocValues: true, + searchable: true, + subType: { + nested: { + path: 'nestedField', + }, + }, + type: 'string', + }, + ]; + describe('fields_for_wildcard_route response', () => { before(() => esArchiver.load('index_patterns/basic_index')); after(() => esArchiver.unload('index_patterns/basic_index')); @@ -26,54 +75,7 @@ export default function ({ getService }) { .get('/api/index_patterns/_fields_for_wildcard') .query({ pattern: 'basic_index' }) .expect(200, { - fields: [ - { - type: 'boolean', - esTypes: ['boolean'], - searchable: true, - aggregatable: true, - name: 'bar', - readFromDocValues: true, - }, - { - type: 'string', - esTypes: ['text'], - searchable: true, - aggregatable: false, - name: 'baz', - readFromDocValues: false, - }, - { - type: 'string', - esTypes: ['keyword'], - searchable: true, - aggregatable: true, - name: 'baz.keyword', - readFromDocValues: true, - subType: { multi: { parent: 'baz' } }, - }, - { - type: 'number', - esTypes: ['long'], - searchable: true, - aggregatable: true, - name: 'foo', - readFromDocValues: true, - }, - { - aggregatable: true, - esTypes: ['keyword'], - name: 'nestedField.child', - readFromDocValues: true, - searchable: true, - subType: { - nested: { - path: 'nestedField', - }, - }, - type: 'string', - }, - ], + fields: testFields, }) .then(ensureFieldsAreSorted); }); @@ -162,11 +164,19 @@ export default function ({ getService }) { .then(ensureFieldsAreSorted); }); - it('returns 404 when the pattern does not exist', async () => { + it('returns fields when one pattern exists and the other does not', async () => { + await supertest + .get('/api/index_patterns/_fields_for_wildcard') + .query({ pattern: 'bad_index,basic_index' }) + .expect(200, { + fields: testFields, + }); + }); + it('returns 404 when no patterns exist', async () => { await supertest .get('/api/index_patterns/_fields_for_wildcard') .query({ - pattern: '[non-existing-pattern]its-invalid-*', + pattern: 'bad_index', }) .expect(404); }); From feda8a07854ca18ce5d932c4ab990adfa3a752fd Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 5 Feb 2021 21:55:09 +0000 Subject: [PATCH 03/51] chore(NA): build bazel projects all at once in the distributable build process (#90328) * chore(NA): build bazel projects all at once in the distributable build process * chore(NA): make sure bazelisk is installed * chore(NA): install bazelisk using npm * chore(NA): remove extra spac * chore(NA): test yarn path exports * chore(NA): add direct global dir * chore(NA): some more debug steps * chore(NA): remove one statement * chore(NA): comment one more line out for testing purposes * chore(NA): export the correct yarn bin location into the PATH * chore(NA): cleaning implementation * chore(NA): move installation process of bazelisk into npm * chore(NA): add missing type --- packages/kbn-pm/dist/index.js | 60 ++++++++++++------- .../build_bazel_production_projects.ts | 7 ++- .../kbn-pm/src/utils/bazel/install_tools.ts | 32 +++++++--- src/dev/ci_setup/setup.sh | 5 -- src/dev/ci_setup/setup_env.sh | 11 ++++ 5 files changed, 76 insertions(+), 39 deletions(-) diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 4d065411f91b61..abb941d2117131 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -48106,23 +48106,34 @@ async function isBazelBinAvailable() { } } +async function isBazeliskInstalled(bazeliskVersion) { + try { + const { + stdout: bazeliskPkgInstallStdout + } = await Object(_child_process__WEBPACK_IMPORTED_MODULE_2__["spawn"])('npm', ['ls', '--global', '--parseable', '--long', `@bazel/bazelisk@${bazeliskVersion}`], { + stdio: 'pipe' + }); + return bazeliskPkgInstallStdout.includes(`@bazel/bazelisk@${bazeliskVersion}`); + } catch { + return false; + } +} + async function installBazelTools(repoRootPath) { _log__WEBPACK_IMPORTED_MODULE_4__["log"].debug(`[bazel_tools] reading bazel tools versions from version files`); const bazeliskVersion = await readBazelToolsVersionFile(repoRootPath, '.bazeliskversion'); const bazelVersion = await readBazelToolsVersionFile(repoRootPath, '.bazelversion'); // Check what globals are installed - _log__WEBPACK_IMPORTED_MODULE_4__["log"].debug(`[bazel_tools] verify if bazelisk is installed`); - const { - stdout: bazeliskPkgInstallStdout - } = await Object(_child_process__WEBPACK_IMPORTED_MODULE_2__["spawn"])('yarn', ['global', 'list'], { - stdio: 'pipe' - }); + _log__WEBPACK_IMPORTED_MODULE_4__["log"].debug(`[bazel_tools] verify if bazelisk is installed`); // Test if bazelisk is already installed in the correct version + + const isBazeliskPkgInstalled = await isBazeliskInstalled(bazeliskVersion); // Test if bazel bin is available + const isBazelBinAlreadyAvailable = await isBazelBinAvailable(); // Install bazelisk if not installed - if (!bazeliskPkgInstallStdout.includes(`@bazel/bazelisk@${bazeliskVersion}`) || !isBazelBinAlreadyAvailable) { + if (!isBazeliskPkgInstalled || !isBazelBinAlreadyAvailable) { _log__WEBPACK_IMPORTED_MODULE_4__["log"].info(`[bazel_tools] installing Bazel tools`); _log__WEBPACK_IMPORTED_MODULE_4__["log"].debug(`[bazel_tools] bazelisk is not installed. Installing @bazel/bazelisk@${bazeliskVersion} and bazel@${bazelVersion}`); - await Object(_child_process__WEBPACK_IMPORTED_MODULE_2__["spawn"])('yarn', ['global', 'add', `@bazel/bazelisk@${bazeliskVersion}`], { + await Object(_child_process__WEBPACK_IMPORTED_MODULE_2__["spawn"])('npm', ['install', '--global', `@bazel/bazelisk@${bazeliskVersion}`], { env: { USE_BAZEL_VERSION: bazelVersion }, @@ -48132,7 +48143,7 @@ async function installBazelTools(repoRootPath) { if (!isBazelBinAvailableAfterInstall) { throw new Error(dedent__WEBPACK_IMPORTED_MODULE_0___default.a` - [bazel_tools] an error occurred when installing the Bazel tools. Please make sure 'yarn global bin' is on your $PATH, otherwise just add it there + [bazel_tools] an error occurred when installing the Bazel tools. Please make sure you have access to npm globally installed modules on your $PATH `); } } @@ -59771,10 +59782,11 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(745); -/* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(131); -/* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(246); -/* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(251); -/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(248); +/* harmony import */ var _utils_bazel_run__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(374); +/* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(131); +/* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(246); +/* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(251); +/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(248); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License @@ -59790,17 +59802,19 @@ __webpack_require__.r(__webpack_exports__); + async function buildBazelProductionProjects({ kibanaRoot, buildRoot, onlyOSS }) { - const projects = await Object(_utils_projects__WEBPACK_IMPORTED_MODULE_7__["getBazelProjectsOnly"])(await Object(_build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__["getProductionProjects"])(kibanaRoot, onlyOSS)); + const projects = await Object(_utils_projects__WEBPACK_IMPORTED_MODULE_8__["getBazelProjectsOnly"])(await Object(_build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__["getProductionProjects"])(kibanaRoot, onlyOSS)); const projectNames = [...projects.values()].map(project => project.name); - _utils_log__WEBPACK_IMPORTED_MODULE_5__["log"].info(`Preparing Bazel projects production build for [${projectNames.join(', ')}]`); + _utils_log__WEBPACK_IMPORTED_MODULE_6__["log"].info(`Preparing Bazel projects production build for [${projectNames.join(', ')}]`); + await Object(_utils_bazel_run__WEBPACK_IMPORTED_MODULE_4__["runBazel"])(['build', '//packages:build']); + _utils_log__WEBPACK_IMPORTED_MODULE_6__["log"].info(`All Bazel projects production builds for [${projectNames.join(', ')}] are complete}]`); for (const project of projects.values()) { - await Object(_build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__["buildProject"])(project); await copyToBuild(project, kibanaRoot, buildRoot); await applyCorrectPermissions(project, kibanaRoot, buildRoot); } @@ -59835,9 +59849,9 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { // the intermediate build, we fall back to using the project's already defined // `package.json`. - const packageJson = (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_4__["isFile"])(Object(path__WEBPACK_IMPORTED_MODULE_2__["join"])(buildProjectPath, 'package.json'))) ? await Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_6__["readPackageJson"])(buildProjectPath) : project.json; - const preparedPackageJson = Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_6__["createProductionPackageJson"])(packageJson); - await Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_6__["writePackageJson"])(buildProjectPath, preparedPackageJson); + const packageJson = (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["isFile"])(Object(path__WEBPACK_IMPORTED_MODULE_2__["join"])(buildProjectPath, 'package.json'))) ? await Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_7__["readPackageJson"])(buildProjectPath) : project.json; + const preparedPackageJson = Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_7__["createProductionPackageJson"])(packageJson); + await Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_7__["writePackageJson"])(buildProjectPath, preparedPackageJson); } async function applyCorrectPermissions(project, kibanaRoot, buildRoot) { @@ -59852,12 +59866,12 @@ async function applyCorrectPermissions(project, kibanaRoot, buildRoot) { for (const pluginPath of allPluginPaths) { const resolvedPluginPath = Object(path__WEBPACK_IMPORTED_MODULE_2__["resolve"])(buildRoot, pluginPath); - if (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_4__["isFile"])(resolvedPluginPath)) { - await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_4__["chmod"])(resolvedPluginPath, 0o644); + if (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["isFile"])(resolvedPluginPath)) { + await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["chmod"])(resolvedPluginPath, 0o644); } - if (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_4__["isDirectory"])(resolvedPluginPath)) { - await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_4__["chmod"])(resolvedPluginPath, 0o755); + if (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["isDirectory"])(resolvedPluginPath)) { + await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["chmod"])(resolvedPluginPath, 0o755); } } } diff --git a/packages/kbn-pm/src/production/build_bazel_production_projects.ts b/packages/kbn-pm/src/production/build_bazel_production_projects.ts index cd40653a6b54c2..a54d6c753d8d77 100644 --- a/packages/kbn-pm/src/production/build_bazel_production_projects.ts +++ b/packages/kbn-pm/src/production/build_bazel_production_projects.ts @@ -10,7 +10,8 @@ import copy from 'cpy'; import globby from 'globby'; import { basename, join, relative, resolve } from 'path'; -import { buildProject, getProductionProjects } from './build_non_bazel_production_projects'; +import { getProductionProjects } from './build_non_bazel_production_projects'; +import { runBazel } from '../utils/bazel/run'; import { chmod, isFile, isDirectory } from '../utils/fs'; import { log } from '../utils/log'; import { @@ -35,8 +36,10 @@ export async function buildBazelProductionProjects({ const projectNames = [...projects.values()].map((project) => project.name); log.info(`Preparing Bazel projects production build for [${projectNames.join(', ')}]`); + await runBazel(['build', '//packages:build']); + log.info(`All Bazel projects production builds for [${projectNames.join(', ')}] are complete}]`); + for (const project of projects.values()) { - await buildProject(project); await copyToBuild(project, kibanaRoot, buildRoot); await applyCorrectPermissions(project, kibanaRoot, buildRoot); } diff --git a/packages/kbn-pm/src/utils/bazel/install_tools.ts b/packages/kbn-pm/src/utils/bazel/install_tools.ts index dfd20f5974d67d..cee6eff317afa4 100644 --- a/packages/kbn-pm/src/utils/bazel/install_tools.ts +++ b/packages/kbn-pm/src/utils/bazel/install_tools.ts @@ -36,6 +36,22 @@ async function isBazelBinAvailable() { } } +async function isBazeliskInstalled(bazeliskVersion: string) { + try { + const { stdout: bazeliskPkgInstallStdout } = await spawn( + 'npm', + ['ls', '--global', '--parseable', '--long', `@bazel/bazelisk@${bazeliskVersion}`], + { + stdio: 'pipe', + } + ); + + return bazeliskPkgInstallStdout.includes(`@bazel/bazelisk@${bazeliskVersion}`); + } catch { + return false; + } +} + export async function installBazelTools(repoRootPath: string) { log.debug(`[bazel_tools] reading bazel tools versions from version files`); const bazeliskVersion = await readBazelToolsVersionFile(repoRootPath, '.bazeliskversion'); @@ -43,23 +59,21 @@ export async function installBazelTools(repoRootPath: string) { // Check what globals are installed log.debug(`[bazel_tools] verify if bazelisk is installed`); - const { stdout: bazeliskPkgInstallStdout } = await spawn('yarn', ['global', 'list'], { - stdio: 'pipe', - }); + // Test if bazelisk is already installed in the correct version + const isBazeliskPkgInstalled = await isBazeliskInstalled(bazeliskVersion); + + // Test if bazel bin is available const isBazelBinAlreadyAvailable = await isBazelBinAvailable(); // Install bazelisk if not installed - if ( - !bazeliskPkgInstallStdout.includes(`@bazel/bazelisk@${bazeliskVersion}`) || - !isBazelBinAlreadyAvailable - ) { + if (!isBazeliskPkgInstalled || !isBazelBinAlreadyAvailable) { log.info(`[bazel_tools] installing Bazel tools`); log.debug( `[bazel_tools] bazelisk is not installed. Installing @bazel/bazelisk@${bazeliskVersion} and bazel@${bazelVersion}` ); - await spawn('yarn', ['global', 'add', `@bazel/bazelisk@${bazeliskVersion}`], { + await spawn('npm', ['install', '--global', `@bazel/bazelisk@${bazeliskVersion}`], { env: { USE_BAZEL_VERSION: bazelVersion, }, @@ -69,7 +83,7 @@ export async function installBazelTools(repoRootPath: string) { const isBazelBinAvailableAfterInstall = await isBazelBinAvailable(); if (!isBazelBinAvailableAfterInstall) { throw new Error(dedent` - [bazel_tools] an error occurred when installing the Bazel tools. Please make sure 'yarn global bin' is on your $PATH, otherwise just add it there + [bazel_tools] an error occurred when installing the Bazel tools. Please make sure you have access to npm globally installed modules on your $PATH `); } } diff --git a/src/dev/ci_setup/setup.sh b/src/dev/ci_setup/setup.sh index e5e21e312b0ddb..61f578ba339715 100755 --- a/src/dev/ci_setup/setup.sh +++ b/src/dev/ci_setup/setup.sh @@ -65,8 +65,3 @@ if [ "$GIT_CHANGES" ]; then echo -e "$GIT_CHANGES\n" exit 1 fi - -### -### copy .bazelrc-ci into $HOME/.bazelrc -### -cp "src/dev/ci_setup/.bazelrc-ci" "$HOME/.bazelrc"; diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 5dac270239c4af..0b835d4b9fa947 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -175,4 +175,15 @@ if [[ -d "$ES_DIR" && -f "$ES_JAVA_PROP_PATH" ]]; then export JAVA_HOME=$HOME/.java/$ES_BUILD_JAVA fi +### +### copy .bazelrc-ci into $HOME/.bazelrc +### +cp -f "$KIBANA_DIR/src/dev/ci_setup/.bazelrc-ci" "$HOME/.bazelrc"; + +### +### append auth token to buildbuddy into "$HOME/.bazelrc"; +### +echo "# Appended by $KIBANA_DIR/src/dev/ci_setup/setup.sh" >> "$HOME/.bazelrc" +echo "build --remote_header=x-buildbuddy-api-key=$KIBANA_BUILDBUDDY_CI_API_KEY" >> "$HOME/.bazelrc" + export CI_ENV_SETUP=true From befe41067e2cd4d5cc3e11fe2d910b42344bc4eb Mon Sep 17 00:00:00 2001 From: liza-mae Date: Fri, 5 Feb 2021 15:06:15 -0700 Subject: [PATCH 04/51] [Docs] Update reporting troubleshooting for arm rhel/centos (#90385) * Update reporting document * Move to own section * Remove extra line --- docs/user/reporting/reporting-troubleshooting.asciidoc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/user/reporting/reporting-troubleshooting.asciidoc b/docs/user/reporting/reporting-troubleshooting.asciidoc index 1f07b0b57d8c7a..ebe095e0881b38 100644 --- a/docs/user/reporting/reporting-troubleshooting.asciidoc +++ b/docs/user/reporting/reporting-troubleshooting.asciidoc @@ -15,6 +15,7 @@ Having trouble? Here are solutions to common problems you might encounter while * <> * <> * <> +* <> [float] [[reporting-diagnostics]] @@ -156,3 +157,9 @@ requests to render. If the {kib} instance doesn't have enough memory to run the report, the report fails with an error such as `Error: Page crashed!` In this case, try increasing the memory for the {kib} instance to 2GB. + +[float] +[[reporting-troubleshooting-arm-systems]] +=== ARM systems + +Chromium is not compatible with ARM RHEL/CentOS. From f4dc6d0235f3abeaa5196587eaac199b829aad4c Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Fri, 5 Feb 2021 14:14:31 -0800 Subject: [PATCH 05/51] [Fleet] Fix incorrect conversion of string to numeric values in agent YAML (#90371) * Convert user values back to string after yaml template compilation if they were strings originally * Add better test cases and adjust patch * Fix when field is undefined * Handle array of strings too --- .../server/services/epm/agent/agent.test.ts | 16 ++++++++++++++++ .../fleet/server/services/epm/agent/agent.ts | 12 ++++++++++++ 2 files changed, 28 insertions(+) diff --git a/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts b/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts index 3e1d3d57bbf71e..7ab904b2f15e13 100644 --- a/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts @@ -22,10 +22,23 @@ password: {{password}} {{#if password}} hidden_password: {{password}} {{/if}} +{{#if optional_field}} +optional_field: {{optional_field}} +{{/if}} +foo: {{bar}} +some_text_field: {{should_be_text}} +multi_text_field: +{{#each multi_text}} + - {{this}} +{{/each}} `; const vars = { paths: { value: ['/usr/local/var/log/nginx/access.log'] }, password: { type: 'password', value: '' }, + optional_field: { type: 'text', value: undefined }, + bar: { type: 'text', value: 'bar' }, + should_be_text: { type: 'text', value: '1234' }, + multi_text: { type: 'text', value: ['1234', 'foo', 'bar'] }, }; const output = compileTemplate(vars, streamTemplate); @@ -35,6 +48,9 @@ hidden_password: {{password}} exclude_files: ['.gz$'], processors: [{ add_locale: null }], password: '', + foo: 'bar', + some_text_field: '1234', + multi_text_field: ['1234', 'foo', 'bar'], }); }); diff --git a/x-pack/plugins/fleet/server/services/epm/agent/agent.ts b/x-pack/plugins/fleet/server/services/epm/agent/agent.ts index 6b1d84ea28b0a0..4f39da5b0b70d7 100644 --- a/x-pack/plugins/fleet/server/services/epm/agent/agent.ts +++ b/x-pack/plugins/fleet/server/services/epm/agent/agent.ts @@ -58,6 +58,10 @@ function replaceVariablesInYaml(yamlVariables: { [k: string]: any }, yaml: any) return yaml; } +const maybeEscapeNumericString = (value: string) => { + return value.length && !isNaN(+value) ? `"${value}"` : value; +}; + function buildTemplateVariables(variables: PackagePolicyConfigRecord, templateStr: string) { const yamlValues: { [k: string]: any } = {}; const vars = Object.entries(variables).reduce((acc, [key, recordEntry]) => { @@ -84,6 +88,14 @@ function buildTemplateVariables(variables: PackagePolicyConfigRecord, templateSt const yamlKeyPlaceholder = `##${key}##`; varPart[lastKeyPart] = `"${yamlKeyPlaceholder}"`; yamlValues[yamlKeyPlaceholder] = recordEntry.value ? safeLoad(recordEntry.value) : null; + } else if (recordEntry.type && recordEntry.type === 'text' && recordEntry.value?.length) { + if (Array.isArray(recordEntry.value)) { + varPart[lastKeyPart] = recordEntry.value.map((value: string) => + maybeEscapeNumericString(value) + ); + } else { + varPart[lastKeyPart] = maybeEscapeNumericString(recordEntry.value); + } } else { varPart[lastKeyPart] = recordEntry.value; } From efcd2c38ef0181756af49e7228f1c9e37f372034 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Fri, 5 Feb 2021 17:23:26 -0500 Subject: [PATCH 06/51] Skip failing suite (#90526) --- .../apps/ml/data_frame_analytics/feature_importance.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts index 49728603c246c7..b8bdc7de16e1e1 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts @@ -14,7 +14,8 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('total feature importance panel and decision path popover', function () { + // Failing: See https://github.com/elastic/kibana/issues/90526 + describe.skip('total feature importance panel and decision path popover', function () { const testDataList: Array<{ suiteTitle: string; archive: string; From a9fce985a5e22e64c07b5d082848d272997373ad Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 5 Feb 2021 23:45:30 +0000 Subject: [PATCH 07/51] chore(NA): integrate build buddy with our bazel setup and remote cache for ci (#90116) * chore(NA): simple changes on bazelrc * chore(NA): integrate bazel tools with BuildBuddy and remote cache service * chore(NA) fix bazelrc line config * chore(NA): move non auth settings out of bazelrc.auth * chore(NA): output home dir * chore(NA): load .bazelrc-ci.auth from /Users/tiagocosta dir * chore(NA): remove bazelrc auth file and append directly into home bazelrc * chore(NA): comment announce option * chore(NA): integrate build buddy metadata * chore(NA): update src/dev/ci_setup/.bazelrc-ci Co-authored-by: Tyler Smalley * chore(NA): move build metadata integation to common confdig * chore(NA): fix problem on bazel file location * chore(NA): correct sh file permissions * chore(NA): only get host on CI * chore(NA): add cores into host info on CI * chore(NA): sync with last settings to setup bazelisk tools on ci * chore(NA): sync last changes on ci setup env * chore(NA): sync settings on ci setup with the other PR * chore(NA): remove yarn export Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Tyler Smalley --- .bazelrc | 9 +++++ src/dev/bazel_workspace_status.sh | 57 +++++++++++++++++++++++++++++ src/dev/ci_setup/.bazelrc-ci | 12 ++++-- src/dev/ci_setup/.bazelrc-ci.common | 3 -- src/dev/ci_setup/load_env_keys.sh | 3 ++ src/dev/ci_setup/setup.sh | 11 ++++++ 6 files changed, 89 insertions(+), 6 deletions(-) create mode 100755 src/dev/bazel_workspace_status.sh diff --git a/.bazelrc b/.bazelrc index 741067e4ff18e5..158338ec5f093c 100644 --- a/.bazelrc +++ b/.bazelrc @@ -2,8 +2,17 @@ # Import shared settings first so we can override below import %workspace%/.bazelrc.common +## Disabled for now # Remote cache settings for local env # build --remote_cache=https://storage.googleapis.com/kibana-bazel-cache # build --incompatible_remote_results_ignore_disk=true # build --remote_accept_cached=true # build --remote_upload_local_results=false + +# BuildBuddy +## Metadata settings +build --workspace_status_command=$(pwd)/src/dev/bazel_workspace_status.sh +# Enable this in case you want to share your build info +# build --build_metadata=VISIBILITY=PUBLIC +build --build_metadata=TEST_GROUPS=//packages + diff --git a/src/dev/bazel_workspace_status.sh b/src/dev/bazel_workspace_status.sh new file mode 100755 index 00000000000000..efaca4bb98849b --- /dev/null +++ b/src/dev/bazel_workspace_status.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# Inspired on https://github.com/buildbuddy-io/buildbuddy/blob/master/workspace_status.sh +# This script will be run bazel when building process starts to +# generate key-value information that represents the status of the +# workspace. The output should be like +# +# KEY1 VALUE1 +# KEY2 VALUE2 +# +# If the script exits with non-zero code, it's considered as a failure +# and the output will be discarded. + +# Git repo +repo_url=$(git config --get remote.origin.url) +if [[ $? != 0 ]]; +then + exit 1 +fi +echo "REPO_URL ${repo_url}" + +# Commit SHA +commit_sha=$(git rev-parse HEAD) +if [[ $? != 0 ]]; +then + exit 1 +fi +echo "COMMIT_SHA ${commit_sha}" + +# Git branch +repo_url=$(git rev-parse --abbrev-ref HEAD) +if [[ $? != 0 ]]; +then + exit 1 +fi +echo "GIT_BRANCH ${repo_url}" + +# Tree status +git diff-index --quiet HEAD -- +if [[ $? == 0 ]]; +then + tree_status="Clean" +else + tree_status="Modified" +fi +echo "GIT_TREE_STATUS ${tree_status}" + +# Host +if [ "$CI" = "true" ]; then + host=$(hostname | sed 's|\(.*\)-.*|\1|') + cores=$(grep ^cpu\\scores /proc/cpuinfo | uniq | awk '{print $4}' ) + if [[ $? != 0 ]]; + then + exit 1 + fi + echo "HOST ${host}-${cores}" +fi diff --git a/src/dev/ci_setup/.bazelrc-ci b/src/dev/ci_setup/.bazelrc-ci index 5b345d3c9e2074..ef6fab3a30590a 100644 --- a/src/dev/ci_setup/.bazelrc-ci +++ b/src/dev/ci_setup/.bazelrc-ci @@ -5,6 +5,12 @@ # Import and load bazelrc common settings for ci env try-import %workspace%/src/dev/ci_setup/.bazelrc-ci.common -# Remote cache settings for ci env -# build --google_default_credentials -# build --remote_upload_local_results=true +# BuildBuddy settings +## Remote settings including cache +build --bes_results_url=https://app.buildbuddy.io/invocation/ +build --bes_backend=grpcs://cloud.buildbuddy.io +build --remote_cache=grpcs://cloud.buildbuddy.io +build --remote_timeout=3600 + +## Metadata settings +build --build_metadata=ROLE=CI diff --git a/src/dev/ci_setup/.bazelrc-ci.common b/src/dev/ci_setup/.bazelrc-ci.common index 3f58e4e03a1785..9d00ee5639741f 100644 --- a/src/dev/ci_setup/.bazelrc-ci.common +++ b/src/dev/ci_setup/.bazelrc-ci.common @@ -4,8 +4,5 @@ # Don't be spammy in the logs build --noshow_progress -# Print all the options that apply to the build. -build --announce_rc - # More details on failures build --verbose_failures=true diff --git a/src/dev/ci_setup/load_env_keys.sh b/src/dev/ci_setup/load_env_keys.sh index 62d29db232eae9..5f7a6c26bab21b 100644 --- a/src/dev/ci_setup/load_env_keys.sh +++ b/src/dev/ci_setup/load_env_keys.sh @@ -34,6 +34,9 @@ else PERCY_TOKEN=$(retry 5 vault read -field=value secret/kibana-issues/dev/percy) export PERCY_TOKEN + KIBANA_BUILDBUDDY_CI_API_KEY=$(retry 5 vault read -field=value secret/kibana-issues/dev/kibana-buildbuddy-ci-api-key) + export KIBANA_BUILDBUDDY_CI_API_KEY + # remove vault related secrets unset VAULT_ROLE_ID VAULT_SECRET_ID VAULT_TOKEN VAULT_ADDR fi diff --git a/src/dev/ci_setup/setup.sh b/src/dev/ci_setup/setup.sh index 61f578ba339715..0b24f0b22b81a9 100755 --- a/src/dev/ci_setup/setup.sh +++ b/src/dev/ci_setup/setup.sh @@ -10,6 +10,17 @@ echo " -- PARENT_DIR='$PARENT_DIR'" echo " -- KIBANA_PKG_BRANCH='$KIBANA_PKG_BRANCH'" echo " -- TEST_ES_SNAPSHOT_VERSION='$TEST_ES_SNAPSHOT_VERSION'" +### +### copy .bazelrc-ci into $HOME/.bazelrc +### +cp "src/dev/ci_setup/.bazelrc-ci" "$HOME/.bazelrc"; + +### +### append auth token to buildbuddy into "$HOME/.bazelrc"; +### +echo "# Appended by src/dev/ci_setup/setup.sh" >> "$HOME/.bazelrc" +echo "build --remote_header=x-buildbuddy-api-key=$KIBANA_BUILDBUDDY_CI_API_KEY" >> "$HOME/.bazelrc" + ### ### install dependencies ### From be725cabc2f74d2c0a812b44717c1f3add4b130a Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Fri, 5 Feb 2021 17:10:49 -0800 Subject: [PATCH 08/51] [test] Await retry.waitFor (#90456) Signed-off-by: Tyler Smalley --- test/functional/apps/console/_console.ts | 4 +++- x-pack/test/functional/page_objects/upgrade_assistant_page.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test/functional/apps/console/_console.ts b/test/functional/apps/console/_console.ts index 6aeb1e2a624ada..05933ebf1ea2a6 100644 --- a/test/functional/apps/console/_console.ts +++ b/test/functional/apps/console/_console.ts @@ -85,7 +85,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.console.dismissTutorial(); expect(await PageObjects.console.hasAutocompleter()).to.be(false); await PageObjects.console.promptAutocomplete(); - retry.waitFor('autocomplete to be visible', () => PageObjects.console.hasAutocompleter()); + await retry.waitFor('autocomplete to be visible', () => + PageObjects.console.hasAutocompleter() + ); }); }); } diff --git a/x-pack/test/functional/page_objects/upgrade_assistant_page.ts b/x-pack/test/functional/page_objects/upgrade_assistant_page.ts index da1518ed72b489..1c4a85450a8da4 100644 --- a/x-pack/test/functional/page_objects/upgrade_assistant_page.ts +++ b/x-pack/test/functional/page_objects/upgrade_assistant_page.ts @@ -24,7 +24,7 @@ export function UpgradeAssistantPageProvider({ getPageObjects, getService }: Ftr return await retry.try(async () => { await common.navigateToApp('settings'); await testSubjects.click('upgrade_assistant'); - retry.waitFor('url to contain /upgrade_assistant', async () => { + await retry.waitFor('url to contain /upgrade_assistant', async () => { const url = await browser.getCurrentUrl(); return url.includes('/upgrade_assistant'); }); @@ -61,7 +61,7 @@ export function UpgradeAssistantPageProvider({ getPageObjects, getService }: Ftr async waitForTelemetryHidden() { const self = this; - retry.waitFor('Telemetry to disappear.', async () => { + await retry.waitFor('Telemetry to disappear.', async () => { return (await self.isTelemetryExists()) === false; }); } From 6408a668e454f8c696d59d59ca1b9ffbe938326d Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Sat, 6 Feb 2021 03:27:21 +0000 Subject: [PATCH 09/51] chore(NA): add safe guard to remove bazelisk from yarn global at bootstrap (#90538) --- packages/kbn-pm/dist/index.js | 27 ++++++++++++++++++- .../kbn-pm/src/utils/bazel/install_tools.ts | 26 ++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index abb941d2117131..d939e7b3000fa3 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -48119,6 +48119,29 @@ async function isBazeliskInstalled(bazeliskVersion) { } } +async function tryRemoveBazeliskFromYarnGlobal() { + try { + // Check if Bazelisk is installed on the yarn global scope + const { + stdout: bazeliskPkgInstallStdout + } = await Object(_child_process__WEBPACK_IMPORTED_MODULE_2__["spawn"])('yarn', ['global', 'list'], { + stdio: 'pipe' + }); // Bazelisk was found on yarn global scope so lets remove it + + if (bazeliskPkgInstallStdout.includes(`@bazel/bazelisk@`)) { + await Object(_child_process__WEBPACK_IMPORTED_MODULE_2__["spawn"])('yarn', ['global', 'remove', `@bazel/bazelisk`], { + stdio: 'pipe' + }); + _log__WEBPACK_IMPORTED_MODULE_4__["log"].info(`[bazel_tools] bazelisk was installed on Yarn global packages and is now removed`); + return true; + } + + return false; + } catch { + return false; + } +} + async function installBazelTools(repoRootPath) { _log__WEBPACK_IMPORTED_MODULE_4__["log"].debug(`[bazel_tools] reading bazel tools versions from version files`); const bazeliskVersion = await readBazelToolsVersionFile(repoRootPath, '.bazeliskversion'); @@ -48128,7 +48151,9 @@ async function installBazelTools(repoRootPath) { const isBazeliskPkgInstalled = await isBazeliskInstalled(bazeliskVersion); // Test if bazel bin is available - const isBazelBinAlreadyAvailable = await isBazelBinAvailable(); // Install bazelisk if not installed + const isBazelBinAlreadyAvailable = await isBazelBinAvailable(); // Check if we need to remove bazelisk from yarn + + await tryRemoveBazeliskFromYarnGlobal(); // Install bazelisk if not installed if (!isBazeliskPkgInstalled || !isBazelBinAlreadyAvailable) { _log__WEBPACK_IMPORTED_MODULE_4__["log"].info(`[bazel_tools] installing Bazel tools`); diff --git a/packages/kbn-pm/src/utils/bazel/install_tools.ts b/packages/kbn-pm/src/utils/bazel/install_tools.ts index cee6eff317afa4..b547c2bc141bda 100644 --- a/packages/kbn-pm/src/utils/bazel/install_tools.ts +++ b/packages/kbn-pm/src/utils/bazel/install_tools.ts @@ -52,6 +52,29 @@ async function isBazeliskInstalled(bazeliskVersion: string) { } } +async function tryRemoveBazeliskFromYarnGlobal() { + try { + // Check if Bazelisk is installed on the yarn global scope + const { stdout: bazeliskPkgInstallStdout } = await spawn('yarn', ['global', 'list'], { + stdio: 'pipe', + }); + + // Bazelisk was found on yarn global scope so lets remove it + if (bazeliskPkgInstallStdout.includes(`@bazel/bazelisk@`)) { + await spawn('yarn', ['global', 'remove', `@bazel/bazelisk`], { + stdio: 'pipe', + }); + + log.info(`[bazel_tools] bazelisk was installed on Yarn global packages and is now removed`); + return true; + } + + return false; + } catch { + return false; + } +} + export async function installBazelTools(repoRootPath: string) { log.debug(`[bazel_tools] reading bazel tools versions from version files`); const bazeliskVersion = await readBazelToolsVersionFile(repoRootPath, '.bazeliskversion'); @@ -66,6 +89,9 @@ export async function installBazelTools(repoRootPath: string) { // Test if bazel bin is available const isBazelBinAlreadyAvailable = await isBazelBinAvailable(); + // Check if we need to remove bazelisk from yarn + await tryRemoveBazeliskFromYarnGlobal(); + // Install bazelisk if not installed if (!isBazeliskPkgInstalled || !isBazelBinAlreadyAvailable) { log.info(`[bazel_tools] installing Bazel tools`); From 826a1ecbdbc7de14aebce15330db6a8316ada404 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Sat, 6 Feb 2021 11:52:04 +0200 Subject: [PATCH 10/51] [Search Sessions] Use sync config (#90138) * Search Sessions: Unskip Flaky Functional Test * Save all search sessions and then manage them based on their persisted state * Get default search session expiration from config * randomize sleep time * fix test * fix test * Make sure we poll, and dont persist, searches not in the context of a session * Added keepalive unit tests * fix ts * code review @lukasolson * ts * More tests, rename onScreenTimeout to completedTimeout * lint * lint * Delete async seaches * Support saved object pagination Fix get search status tests * better PersistedSearchSessionSavedObjectAttributes ts * test titles * Remove runAt from monitoring task Increase testing trackingInterval (caused bug) * support workload histograms that take into account overdue tasks * Update touched when changing session status to complete \ error * removed test * Updated management test data * Rename configs * delete tap first add comments * Use sync config in data-enhanced plugin * fix merge * fix merge * ts * code review Co-authored-by: Timothy Sullivan Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Anton Dosov Co-authored-by: Gidi Meir Morris --- x-pack/plugins/data_enhanced/server/plugin.ts | 12 +++----- .../server/search/es_search_strategy.test.ts | 30 +++++++++---------- .../server/search/es_search_strategy.ts | 3 +- .../server/search/session/monitoring_task.ts | 7 ++--- .../search/session/session_service.test.ts | 7 ++--- .../server/search/session/session_service.ts | 29 +++++++++--------- 6 files changed, 39 insertions(+), 49 deletions(-) diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts index 76235c917b1398..3aaf50fbeb3e69 100644 --- a/x-pack/plugins/data_enhanced/server/plugin.ts +++ b/x-pack/plugins/data_enhanced/server/plugin.ts @@ -6,7 +6,6 @@ */ import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; -import { Observable } from 'rxjs'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { PluginSetup as DataPluginSetup, @@ -40,11 +39,11 @@ export class EnhancedDataServerPlugin implements Plugin { private readonly logger: Logger; private sessionService!: SearchSessionService; - private config$: Observable; + private config: ConfigSchema; constructor(private initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get('data_enhanced'); - this.config$ = this.initializerContext.config.create(); + this.config = this.initializerContext.config.get(); } public setup(core: CoreSetup, deps: SetupDependencies) { @@ -56,7 +55,7 @@ export class EnhancedDataServerPlugin deps.data.search.registerSearchStrategy( ENHANCED_ES_SEARCH_STRATEGY, enhancedEsSearchStrategyProvider( - this.config$, + this.config, this.initializerContext.config.legacy.globalConfig$, this.logger, usage @@ -68,10 +67,7 @@ export class EnhancedDataServerPlugin eqlSearchStrategyProvider(this.logger) ); - this.sessionService = new SearchSessionService( - this.logger, - this.initializerContext.config.create() - ); + this.sessionService = new SearchSessionService(this.logger, this.config); deps.data.__enhance({ search: { diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts index 019b94f638ca4a..d529e981aaea10 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts @@ -72,13 +72,13 @@ describe('ES search strategy', () => { }, }); - const mockConfig$ = new BehaviorSubject({ + const mockConfig: any = { search: { sessions: { defaultExpiration: moment.duration('1', 'm'), }, }, - }); + }; beforeEach(() => { mockApiCaller.mockClear(); @@ -89,7 +89,7 @@ describe('ES search strategy', () => { it('returns a strategy with `search and `cancel`', async () => { const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -104,7 +104,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -123,7 +123,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -142,7 +142,7 @@ describe('ES search strategy', () => { const params = { index: 'foo-*', body: {} }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -160,7 +160,7 @@ describe('ES search strategy', () => { const params = { index: 'foo-程', body: {} }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -189,7 +189,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -209,7 +209,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -237,7 +237,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -262,7 +262,7 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -287,7 +287,7 @@ describe('ES search strategy', () => { const id = 'some_id'; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -311,7 +311,7 @@ describe('ES search strategy', () => { const id = 'some_id'; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -338,7 +338,7 @@ describe('ES search strategy', () => { const id = 'some_other_id'; const keepAlive = '1d'; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); @@ -357,7 +357,7 @@ describe('ES search strategy', () => { const id = 'some_other_id'; const keepAlive = '1d'; const esSearch = await enhancedEsSearchStrategyProvider( - mockConfig$, + mockConfig, mockLegacyConfig$, mockLogger ); diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 402058a776605f..fc1cc63146358e 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -39,7 +39,7 @@ import { ConfigSchema } from '../../config'; import { getKbnServerError, KbnServerError } from '../../../../../src/plugins/kibana_utils/server'; export const enhancedEsSearchStrategyProvider = ( - config$: Observable, + config: ConfigSchema, legacyConfig$: Observable, logger: Logger, usage?: SearchUsage @@ -60,7 +60,6 @@ export const enhancedEsSearchStrategyProvider = ( const client = esClient.asCurrentUser.asyncSearch; const search = async () => { - const config = await config$.pipe(first()).toPromise(); const params = id ? getDefaultAsyncGetParams(options) : { diff --git a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts index 75b6089cddf9b0..8aa35def387b73 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { Duration } from 'moment'; import { TaskManagerSetupContract, @@ -24,14 +22,13 @@ export const SEARCH_SESSIONS_TASK_ID = `data_enhanced_${SEARCH_SESSIONS_TASK_TYP interface SearchSessionTaskDeps { taskManager: TaskManagerSetupContract; logger: Logger; - config$: Observable; + config: ConfigSchema; } -function searchSessionRunner(core: CoreSetup, { logger, config$ }: SearchSessionTaskDeps) { +function searchSessionRunner(core: CoreSetup, { logger, config }: SearchSessionTaskDeps) { return ({ taskInstance }: RunContext) => { return { async run() { - const config = await config$.pipe(first()).toPromise(); const sessionConfig = config.search.sessions; const [coreStart] = await core.getStartServices(); const internalRepo = coreStart.savedObjects.createInternalRepository([SEARCH_SESSION_TYPE]); diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts index 19679f02df0ad8..24d13cf24ccfb5 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { BehaviorSubject } from 'rxjs'; import { SavedObject, SavedObjectsClientContract, @@ -46,7 +45,7 @@ describe('SearchSessionService', () => { beforeEach(async () => { savedObjectsClient = savedObjectsClientMock.create(); - const config$ = new BehaviorSubject({ + const config: ConfigSchema = { search: { sessions: { enabled: true, @@ -59,13 +58,13 @@ describe('SearchSessionService', () => { management: {} as any, }, }, - }); + }; const mockLogger: any = { debug: jest.fn(), warn: jest.fn(), error: jest.fn(), }; - service = new SearchSessionService(mockLogger, config$); + service = new SearchSessionService(mockLogger, config); const coreStart = coreMock.createStart(); const mockTaskManager = taskManagerMock.createStart(); await flushPromises(); diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index 059edd5edf1deb..2d0e7e519e3bd4 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { CoreSetup, CoreStart, @@ -50,32 +48,33 @@ function sleep(ms: number) { } export class SearchSessionService implements ISearchSessionService { - private config!: SearchSessionsConfig; + private sessionConfig: SearchSessionsConfig; - constructor( - private readonly logger: Logger, - private readonly config$: Observable - ) {} + constructor(private readonly logger: Logger, private readonly config: ConfigSchema) { + this.sessionConfig = this.config.search.sessions; + } public setup(core: CoreSetup, deps: SetupDependencies) { registerSearchSessionsTask(core, { - config$: this.config$, + config: this.config, taskManager: deps.taskManager, logger: this.logger, }); } public async start(core: CoreStart, deps: StartDependencies) { - const configPromise = await this.config$.pipe(first()).toPromise(); - this.config = (await configPromise).search.sessions; return this.setupMonitoring(core, deps); } public stop() {} private setupMonitoring = async (core: CoreStart, deps: StartDependencies) => { - if (this.config.enabled) { - scheduleSearchSessionsTasks(deps.taskManager, this.logger, this.config.trackingInterval); + if (this.sessionConfig.enabled) { + scheduleSearchSessionsTasks( + deps.taskManager, + this.logger, + this.sessionConfig.trackingInterval + ); } }; @@ -107,7 +106,7 @@ export class SearchSessionService } catch (createError) { if ( SavedObjectsErrorHelpers.isConflictError(createError) && - retry < this.config.maxUpdateRetries + retry < this.sessionConfig.maxUpdateRetries ) { return await retryOnConflict(createError); } else { @@ -116,7 +115,7 @@ export class SearchSessionService } } else if ( SavedObjectsErrorHelpers.isConflictError(e) && - retry < this.config.maxUpdateRetries + retry < this.sessionConfig.maxUpdateRetries ) { return await retryOnConflict(e); } else { @@ -164,7 +163,7 @@ export class SearchSessionService sessionId, status: SearchSessionStatus.IN_PROGRESS, expires: new Date( - Date.now() + this.config.defaultExpiration.asMilliseconds() + Date.now() + this.sessionConfig.defaultExpiration.asMilliseconds() ).toISOString(), created: new Date().toISOString(), touched: new Date().toISOString(), From fd1d96503933a4ca8559d7b9fe3951a8e7baafc0 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Sat, 6 Feb 2021 18:45:20 +0100 Subject: [PATCH 11/51] Unrevert "Migrations v2: don't auto-create indices + FTR/esArchiver support (#85778)" (#89992) * Revert "Revert "Migrations v2: don't auto-create indices + FTR/esArchiver support (#85778)"" This reverts commit f97958043f3e037e5e94daa432a7e71caa4b1ba7. * Fix flaky saved objects management test #89953 * If a clone target exists, wait for yellow, not green, index status * Fix test after master merge * Fix types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...orhelpers.createindexaliasnotfounderror.md | 22 ++ ...helpers.decorateindexaliasnotfounderror.md | 23 ++ ...savedobjectserrorhelpers.isgeneralerror.md | 22 ++ ...in-core-server.savedobjectserrorhelpers.md | 3 + .../src/actions/empty_kibana_index.ts | 3 +- packages/kbn-es-archiver/src/es_archiver.ts | 2 +- .../src/lib/indices/kibana_index.ts | 8 +- .../migrationsv2/actions/index.ts | 19 +- .../integration_tests/actions.test.ts | 112 +++++----- .../saved_objects/migrationsv2/model.test.ts | 202 +++++++++++++++--- .../saved_objects/migrationsv2/model.ts | 20 +- .../saved_objects/routes/bulk_create.ts | 3 +- .../server/saved_objects/routes/bulk_get.ts | 3 +- .../saved_objects/routes/bulk_update.ts | 3 +- .../server/saved_objects/routes/create.ts | 3 +- .../server/saved_objects/routes/delete.ts | 3 +- .../server/saved_objects/routes/export.ts | 4 +- src/core/server/saved_objects/routes/find.ts | 3 +- src/core/server/saved_objects/routes/get.ts | 3 +- .../server/saved_objects/routes/import.ts | 4 +- .../server/saved_objects/routes/migrate.ts | 3 +- .../routes/resolve_import_errors.ts | 5 +- .../server/saved_objects/routes/update.ts | 3 +- .../server/saved_objects/routes/utils.test.ts | 75 +++++++ src/core/server/saved_objects/routes/utils.ts | 34 ++- .../service/lib/decorate_es_error.test.ts | 21 ++ .../service/lib/decorate_es_error.ts | 6 + .../saved_objects/service/lib/errors.ts | 17 ++ .../service/lib/repository.test.js | 9 +- .../saved_objects/service/lib/repository.ts | 33 ++- src/core/server/server.api.md | 6 + .../integration_tests/doc_exists.ts | 6 +- .../integration_tests/doc_missing.ts | 6 +- .../doc_missing_and_index_read_only.ts | 12 +- .../integration_tests/index.test.ts | 13 +- .../integration_tests/lib/servers.ts | 3 - src/core/test_helpers/kbn_server.ts | 2 +- test/accessibility/apps/kibana_overview.ts | 3 +- test/api_integration/apis/home/sample_data.ts | 4 + .../apis/saved_objects/bulk_create.ts | 44 ++-- .../apis/saved_objects/bulk_get.ts | 2 +- .../apis/saved_objects/bulk_update.ts | 16 +- .../apis/saved_objects/create.ts | 48 +---- .../apis/saved_objects/delete.ts | 2 +- .../apis/saved_objects/export.ts | 2 +- .../apis/saved_objects/find.ts | 14 +- .../api_integration/apis/saved_objects/get.ts | 2 +- .../saved_objects/resolve_import_errors.ts | 51 ++++- .../apis/saved_objects/update.ts | 13 +- .../apis/saved_objects_management/find.ts | 4 +- .../apis/saved_objects_management/get.ts | 2 +- test/api_integration/apis/search/search.ts | 1 + test/api_integration/apis/telemetry/opt_in.ts | 3 + .../apis/telemetry/telemetry_local.ts | 1 + .../apis/ui_counters/ui_counters.ts | 4 + .../apis/ui_metric/ui_metric.ts | 5 + test/common/config.js | 2 - .../kibana_server/extend_es_archiver.js | 4 +- .../apps/management/_import_objects.ts | 5 +- .../apps/management/_index_pattern_filter.js | 3 +- .../apps/management/_index_patterns_empty.ts | 3 +- .../management/_mgmt_import_saved_objects.js | 3 +- .../apps/management/_test_huge_fields.js | 1 + test/functional/apps/management/index.ts | 2 - .../input_control_vis/input_control_range.ts | 2 - .../test_suites/core_plugins/applications.ts | 2 + .../test_suites/data_plugin/index_patterns.ts | 4 + .../import_warnings.ts | 7 +- .../insecure_cluster_warning.ts | 1 + .../apps/dashboard/_async_dashboard.ts | 2 + .../saved_objects_management_security.ts | 10 - .../feature_controls/security/data.json | 17 -- .../es_archives/visualize/default/data.json | 24 +-- .../reporting_without_security.config.ts | 1 - 74 files changed, 709 insertions(+), 324 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md new file mode 100644 index 00000000000000..2b897db7bba4c3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [createIndexAliasNotFoundError](./kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md) + +## SavedObjectsErrorHelpers.createIndexAliasNotFoundError() method + +Signature: + +```typescript +static createIndexAliasNotFoundError(alias: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| alias | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md new file mode 100644 index 00000000000000..c7e10fc42ead19 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [decorateIndexAliasNotFoundError](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md) + +## SavedObjectsErrorHelpers.decorateIndexAliasNotFoundError() method + +Signature: + +```typescript +static decorateIndexAliasNotFoundError(error: Error, alias: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | | +| alias | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md new file mode 100644 index 00000000000000..4b4ede2f77a7e2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [isGeneralError](./kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md) + +## SavedObjectsErrorHelpers.isGeneralError() method + +Signature: + +```typescript +static isGeneralError(error: Error | DecoratedError): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | DecoratedError | | + +Returns: + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md index 9b69012ed5f123..2dc78f2df3a833 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md @@ -18,6 +18,7 @@ export declare class SavedObjectsErrorHelpers | [createBadRequestError(reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.createbadrequesterror.md) | static | | | [createConflictError(type, id, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md) | static | | | [createGenericNotFoundError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfounderror.md) | static | | +| [createIndexAliasNotFoundError(alias)](./kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md) | static | | | [createInvalidVersionError(versionInput)](./kibana-plugin-core-server.savedobjectserrorhelpers.createinvalidversionerror.md) | static | | | [createTooManyRequestsError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.createtoomanyrequestserror.md) | static | | | [createUnsupportedTypeError(type)](./kibana-plugin-core-server.savedobjectserrorhelpers.createunsupportedtypeerror.md) | static | | @@ -27,6 +28,7 @@ export declare class SavedObjectsErrorHelpers | [decorateEsUnavailableError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateesunavailableerror.md) | static | | | [decorateForbiddenError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateforbiddenerror.md) | static | | | [decorateGeneralError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorategeneralerror.md) | static | | +| [decorateIndexAliasNotFoundError(error, alias)](./kibana-plugin-core-server.savedobjectserrorhelpers.decorateindexaliasnotfounderror.md) | static | | | [decorateNotAuthorizedError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoratenotauthorizederror.md) | static | | | [decorateRequestEntityTooLargeError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoraterequestentitytoolargeerror.md) | static | | | [decorateTooManyRequestsError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoratetoomanyrequestserror.md) | static | | @@ -35,6 +37,7 @@ export declare class SavedObjectsErrorHelpers | [isEsCannotExecuteScriptError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md) | static | | | [isEsUnavailableError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isesunavailableerror.md) | static | | | [isForbiddenError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isforbiddenerror.md) | static | | +| [isGeneralError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isgeneralerror.md) | static | | | [isInvalidVersionError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isinvalidversionerror.md) | static | | | [isNotAuthorizedError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isnotauthorizederror.md) | static | | | [isNotFoundError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isnotfounderror.md) | static | | diff --git a/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts b/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts index d3494512d055a4..f86865ffa66704 100644 --- a/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts +++ b/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts @@ -25,5 +25,6 @@ export async function emptyKibanaIndexAction({ await cleanKibanaIndices({ client, stats, log, kibanaPluginIds }); await migrateKibanaIndex({ client, kbnClient }); - return stats; + stats.createdIndex('.kibana'); + return stats.toJSON(); } diff --git a/packages/kbn-es-archiver/src/es_archiver.ts b/packages/kbn-es-archiver/src/es_archiver.ts index 70dc5370c5a269..b00b9fb8b3f25f 100644 --- a/packages/kbn-es-archiver/src/es_archiver.ts +++ b/packages/kbn-es-archiver/src/es_archiver.ts @@ -155,7 +155,7 @@ export class EsArchiver { * @return Promise */ async emptyKibanaIndex() { - await emptyKibanaIndexAction({ + return await emptyKibanaIndexAction({ client: this.client, log: this.log, kbnClient: this.kbnClient, diff --git a/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts index 6d48c0b2bbaead..64e5626c94c8bb 100644 --- a/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts +++ b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts @@ -82,7 +82,9 @@ export async function migrateKibanaIndex({ */ async function fetchKibanaIndices(client: Client) { const resp = await client.cat.indices({ index: '.kibana*', format: 'json' }); - const isKibanaIndex = (index: string) => /^\.kibana(:?_\d*)?$/.test(index); + const isKibanaIndex = (index: string) => + /^\.kibana(:?_\d*)?$/.test(index) || + /^\.kibana(_task_manager)?_(pre)?\d+\.\d+\.\d+/.test(index); if (!Array.isArray(resp.body)) { throw new Error(`expected response to be an array ${inspect(resp.body)}`); @@ -115,7 +117,7 @@ export async function cleanKibanaIndices({ while (true) { const resp = await client.deleteByQuery( { - index: `.kibana`, + index: `.kibana,.kibana_task_manager`, body: { query: { bool: { @@ -129,7 +131,7 @@ export async function cleanKibanaIndices({ }, }, { - ignore: [409], + ignore: [404, 409], } ); diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts index fe2ce76446cb99..b22c326061f662 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts @@ -150,12 +150,23 @@ export const removeWriteBlock = ( .catch(catchRetryableEsClientErrors); }; -const waitForIndexStatusGreen = ( +/** + * A yellow index status means the index's primary shard is allocated and the + * index is ready for searching/indexing documents, but ES wasn't able to + * allocate the replicas. When migrations proceed with a yellow index it means + * we don't have as much data-redundancy as we could have, but waiting for + * replicas would mean that v2 migrations fail where v1 migrations would have + * succeeded. It doesn't feel like it's Kibana's job to force users to keep + * their clusters green and even if it's green when we migrate it can turn + * yellow at any point in the future. So ultimately data-redundancy is up to + * users to maintain. + */ +const waitForIndexStatusYellow = ( client: ElasticsearchClient, index: string ): TaskEither.TaskEither => () => { return client.cluster - .health({ index, wait_for_status: 'green', timeout: '30s' }) + .health({ index, wait_for_status: 'yellow', timeout: '30s' }) .then(() => { return Either.right({}); }) @@ -259,7 +270,7 @@ export const cloneIndex = ( } else { // Otherwise, wait until the target index has a 'green' status. return pipe( - waitForIndexStatusGreen(client, target), + waitForIndexStatusYellow(client, target), TaskEither.map((value) => { /** When the index status is 'green' we know that all shards were started */ return { acknowledged: true, shardsAcknowledged: true }; @@ -687,7 +698,7 @@ export const createIndex = ( } else { // Otherwise, wait until the target index has a 'green' status. return pipe( - waitForIndexStatusGreen(client, indexName), + waitForIndexStatusYellow(client, indexName), TaskEither.map(() => { /** When the index status is 'green' we know that all shards were started */ return 'create_index_succeeded'; diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts index 1bb4e57b0ac299..46cfd935f429b3 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts @@ -213,12 +213,8 @@ describe('migration actions', () => { } }); it('resolves right if cloning into a new target index', async () => { + const task = cloneIndex(client, 'existing_index_with_write_block', 'clone_target_1'); expect.assertions(1); - const task = cloneIndex( - client, - 'existing_index_with_write_block', - 'clone_yellow_then_green_index_1' - ); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -229,42 +225,48 @@ describe('migration actions', () => { } `); }); - it('resolves right after waiting for index status to be green if clone target already existed', async () => { + it('resolves right after waiting for index status to be yellow if clone target already existed', async () => { expect.assertions(2); + // Create a yellow index - await client.indices.create({ - index: 'clone_yellow_then_green_index_2', - body: { - mappings: { properties: {} }, - settings: { - // Allocate 1 replica so that this index stays yellow - number_of_replicas: '1', + await client.indices + .create({ + index: 'clone_red_then_yellow_index', + timeout: '5s', + body: { + mappings: { properties: {} }, + settings: { + // Allocate 1 replica so that this index stays yellow + number_of_replicas: '1', + // Disable all shard allocation so that the index status is red + 'index.routing.allocation.enable': 'none', + }, }, - }, - }); + }) + .catch((e) => {}); // Call clone even though the index already exists const cloneIndexPromise = cloneIndex( client, 'existing_index_with_write_block', - 'clone_yellow_then_green_index_2' + 'clone_red_then_yellow_index' )(); - let indexGreen = false; + let indexYellow = false; setTimeout(() => { client.indices.putSettings({ + index: 'clone_red_then_yellow_index', body: { - index: { - number_of_replicas: 0, - }, + // Enable all shard allocation so that the index status goes yellow + 'index.routing.allocation.enable': 'all', }, }); - indexGreen = true; + indexYellow = true; }, 10); await cloneIndexPromise.then((res) => { // Assert that the promise didn't resolve before the index became green - expect(indexGreen).toBe(true); + expect(indexYellow).toBe(true); expect(res).toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -278,7 +280,7 @@ describe('migration actions', () => { }); it('resolves left index_not_found_exception if the source index does not exist', async () => { expect.assertions(1); - const task = cloneIndex(client, 'no_such_index', 'clone_yellow_then_green_index_3'); + const task = cloneIndex(client, 'no_such_index', 'clone_target_3'); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", @@ -674,7 +676,6 @@ describe('migration actions', () => { describe('waitForPickupUpdatedMappingsTask', () => { it('rejects if there are failures', async () => { - expect.assertions(1); const res = (await pickupUpdatedMappings( client, 'existing_index_with_write_block' @@ -689,7 +690,6 @@ describe('migration actions', () => { }); }); it('rejects if there is an error', async () => { - expect.assertions(1); const res = (await pickupUpdatedMappings( client, 'no_such_index' @@ -703,7 +703,6 @@ describe('migration actions', () => { `); }); it('resolves right when successful', async () => { - expect.assertions(1); const res = (await pickupUpdatedMappings( client, 'existing_index_with_docs' @@ -722,7 +721,6 @@ describe('migration actions', () => { describe('updateAndPickupMappings', () => { it('resolves right when mappings were updated and picked up', async () => { - expect.assertions(3); // Create an index without any mappings and insert documents into it await createIndex(client, 'existing_index_without_mappings', { dynamic: false as any, @@ -771,7 +769,6 @@ describe('migration actions', () => { describe('updateAliases', () => { describe('remove', () => { it('resolves left index_not_found_exception when the index does not exist', async () => { - expect.assertions(1); const task = updateAliases(client, [ { remove: { @@ -793,7 +790,6 @@ describe('migration actions', () => { }); describe('with must_exist=false', () => { it('resolves left alias_not_found_exception when alias does not exist', async () => { - expect.assertions(1); const task = updateAliases(client, [ { remove: { @@ -815,7 +811,6 @@ describe('migration actions', () => { }); describe('with must_exist=true', () => { it('resolves left alias_not_found_exception when alias does not exist on specified index', async () => { - expect.assertions(1); const task = updateAliases(client, [ { remove: { @@ -835,7 +830,6 @@ describe('migration actions', () => { `); }); it('resolves left alias_not_found_exception when alias does not exist', async () => { - expect.assertions(1); const task = updateAliases(client, [ { remove: { @@ -858,7 +852,6 @@ describe('migration actions', () => { }); describe('remove_index', () => { it('left index_not_found_exception if index does not exist', async () => { - expect.assertions(1); const task = updateAliases(client, [ { remove_index: { @@ -877,7 +870,6 @@ describe('migration actions', () => { `); }); it('left remove_index_not_a_concrete_index when remove_index targets an alias', async () => { - expect.assertions(1); const task = updateAliases(client, [ { remove_index: { @@ -899,44 +891,50 @@ describe('migration actions', () => { describe('createIndex', () => { afterAll(async () => { - await client.indices.delete({ index: 'yellow_then_green_index' }); + await client.indices.delete({ index: 'red_then_yellow_index' }); }); - it('resolves right after waiting for an index status to be green if the index already existed', async () => { + it('resolves right after waiting for an index status to be yellow if the index already existed', async () => { expect.assertions(2); - // Create a yellow index - await client.indices.create( - { - index: 'yellow_then_green_index', - body: { - mappings: { properties: {} }, - settings: { - // Allocate 1 replica so that this index stays yellow - number_of_replicas: '1', + // Create a red index + await client.indices + .create( + { + index: 'red_then_yellow_index', + timeout: '5s', + body: { + mappings: { properties: {} }, + settings: { + // Allocate 1 replica so that this index stays yellow + number_of_replicas: '1', + // Disable all shard allocation so that the index status is red + 'index.routing.allocation.enable': 'none', + }, }, }, - }, - { maxRetries: 0 /** handle retry ourselves for now */ } - ); + { maxRetries: 0 /** handle retry ourselves for now */ } + ) + .catch((e) => { + /** ignore */ + }); // Call createIndex even though the index already exists - const createIndexPromise = createIndex(client, 'yellow_then_green_index', undefined as any)(); - let indexGreen = false; + const createIndexPromise = createIndex(client, 'red_then_yellow_index', undefined as any)(); + let indexYellow = false; setTimeout(() => { client.indices.putSettings({ - index: 'yellow_then_green_index', + index: 'red_then_yellow_index', body: { - index: { - number_of_replicas: 0, - }, + // Disable all shard allocation so that the index status is red + 'index.routing.allocation.enable': 'all', }, }); - indexGreen = true; + indexYellow = true; }, 10); await createIndexPromise.then((res) => { // Assert that the promise didn't resolve before the index became green - expect(indexGreen).toBe(true); + expect(indexYellow).toBe(true); expect(res).toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -946,7 +944,6 @@ describe('migration actions', () => { }); }); it('rejects when there is an unexpected error creating the index', async () => { - expect.assertions(1); // Creating an index with the same name as an existing alias to induce // failure await expect( @@ -957,7 +954,6 @@ describe('migration actions', () => { describe('bulkOverwriteTransformedDocuments', () => { it('resolves right when documents do not yet exist in the index', async () => { - expect.assertions(1); const newDocs = ([ { _source: { title: 'doc 5' } }, { _source: { title: 'doc 6' } }, @@ -972,7 +968,6 @@ describe('migration actions', () => { `); }); it('resolves right even if there were some version_conflict_engine_exception', async () => { - expect.assertions(1); const existingDocs = ((await searchForOutdatedDocuments( client, 'existing_index_with_docs', @@ -991,7 +986,6 @@ describe('migration actions', () => { `); }); it('rejects if there are errors', async () => { - expect.assertions(1); const newDocs = ([ { _source: { title: 'doc 5' } }, { _source: { title: 'doc 6' } }, diff --git a/src/core/server/saved_objects/migrationsv2/model.test.ts b/src/core/server/saved_objects/migrationsv2/model.test.ts index 895db80983fc17..5531f847f8bb41 100644 --- a/src/core/server/saved_objects/migrationsv2/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model.test.ts @@ -182,6 +182,21 @@ describe('migrations v2 model', () => { versionAlias: '.kibana_7.11.0', versionIndex: '.kibana_7.11.0_001', }; + const mappingsWithUnknownType = { + properties: { + disabled_saved_object_type: { + properties: { + value: { type: 'keyword' }, + }, + }, + }, + _meta: { + migrationMappingPropertyHashes: { + disabled_saved_object_type: '7997cf5a56cc02bdc9c93361bde732b0', + }, + }, + }; + test('INIT -> OUTDATED_DOCUMENTS_SEARCH if .kibana is already pointing to the target index', () => { const res: ResponseType<'INIT'> = Either.right({ '.kibana_7.11.0_001': { @@ -189,38 +204,27 @@ describe('migrations v2 model', () => { '.kibana': {}, '.kibana_7.11.0': {}, }, - mappings: { - properties: { - disabled_saved_object_type: { - properties: { - value: { type: 'keyword' }, - }, - }, - }, - _meta: { - migrationMappingPropertyHashes: { - disabled_saved_object_type: '7997cf5a56cc02bdc9c93361bde732b0', - }, - }, - }, + mappings: mappingsWithUnknownType, settings: {}, }, }); const newState = model(initState, res); expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_SEARCH'); + // This snapshot asserts that we merge the + // migrationMappingPropertyHashes of the existing index, but we leave + // the mappings for the disabled_saved_object_type untouched. There + // might be another Kibana instance that knows about this type and + // needs these mappings in place. expect(newState.targetIndexMappings).toMatchInlineSnapshot(` Object { "_meta": Object { "migrationMappingPropertyHashes": Object { + "disabled_saved_object_type": "7997cf5a56cc02bdc9c93361bde732b0", "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", }, }, "properties": Object { - "disabled_saved_object_type": Object { - "dynamic": false, - "properties": Object {}, - }, "new_saved_object_type": Object { "properties": Object { "value": Object { @@ -271,7 +275,7 @@ describe('migrations v2 model', () => { '.kibana': {}, '.kibana_7.12.0': {}, }, - mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } }, + mappings: mappingsWithUnknownType, settings: {}, }, '.kibana_7.11.0_001': { @@ -288,12 +292,37 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('.kibana_7.invalid.0_001'), targetIndex: '.kibana_7.11.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); }); test('INIT -> SET_SOURCE_WRITE_BLOCK when migrating from a v2 migrations index (>= 7.11.0)', () => { const res: ResponseType<'INIT'> = Either.right({ '.kibana_7.11.0_001': { aliases: { '.kibana': {}, '.kibana_7.11.0': {} }, - mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } }, + mappings: mappingsWithUnknownType, settings: {}, }, '.kibana_3': { @@ -319,6 +348,31 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('.kibana_7.11.0_001'), targetIndex: '.kibana_7.12.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); @@ -328,7 +382,7 @@ describe('migrations v2 model', () => { aliases: { '.kibana': {}, }, - mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } }, + mappings: mappingsWithUnknownType, settings: {}, }, }); @@ -339,6 +393,31 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('.kibana_3'), targetIndex: '.kibana_7.11.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); @@ -346,7 +425,7 @@ describe('migrations v2 model', () => { const res: ResponseType<'INIT'> = Either.right({ '.kibana': { aliases: {}, - mappings: { properties: {}, _meta: {} }, + mappings: mappingsWithUnknownType, settings: {}, }, }); @@ -357,6 +436,31 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('.kibana_pre6.5.0_001'), targetIndex: '.kibana_7.11.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); @@ -366,7 +470,7 @@ describe('migrations v2 model', () => { aliases: { 'my-saved-objects': {}, }, - mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } }, + mappings: mappingsWithUnknownType, settings: {}, }, }); @@ -386,6 +490,31 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('my-saved-objects_3'), targetIndex: 'my-saved-objects_7.11.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); @@ -395,7 +524,7 @@ describe('migrations v2 model', () => { aliases: { 'my-saved-objects': {}, }, - mappings: { properties: {}, _meta: { migrationMappingPropertyHashes: {} } }, + mappings: mappingsWithUnknownType, settings: {}, }, }); @@ -416,6 +545,31 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('my-saved-objects_7.11.0'), targetIndex: 'my-saved-objects_7.12.0_001', }); + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); diff --git a/src/core/server/saved_objects/migrationsv2/model.ts b/src/core/server/saved_objects/migrationsv2/model.ts index c9a3aa25db4c15..6f915df9dd9588 100644 --- a/src/core/server/saved_objects/migrationsv2/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model.ts @@ -60,13 +60,13 @@ function throwBadResponse(state: State, res: any): never { * Merge the _meta.migrationMappingPropertyHashes mappings of an index with * the given target mappings. * - * @remarks Mapping updates are commutative (deeply merged) by Elasticsearch, - * except for the _meta key. The source index we're migrating from might - * contain documents created by a plugin that is disabled in the Kibana - * instance performing this migration. We merge the - * _meta.migrationMappingPropertyHashes mappings from the source index into - * the targetMappings to ensure that any `migrationPropertyHashes` for - * disabled plugins aren't lost. + * @remarks When another instance already completed a migration, the existing + * target index might contain documents and mappings created by a plugin that + * is disabled in the current Kibana instance performing this migration. + * Mapping updates are commutative (deeply merged) by Elasticsearch, except + * for the `_meta` key. By merging the `_meta.migrationMappingPropertyHashes` + * mappings from the existing target index index into the targetMappings we + * ensure that any `migrationPropertyHashes` for disabled plugins aren't lost. * * Right now we don't use these `migrationPropertyHashes` but it could be used * in the future to detect if mappings were changed. If mappings weren't @@ -209,7 +209,7 @@ export const model = (currentState: State, resW: ResponseType): // index sourceIndex: Option.none, targetIndex: `${stateP.indexPrefix}_${stateP.kibanaVersion}_001`, - targetIndexMappings: disableUnknownTypeMappingFields( + targetIndexMappings: mergeMigrationMappingPropertyHashes( stateP.targetIndexMappings, indices[aliases[stateP.currentAlias]].mappings ), @@ -242,7 +242,7 @@ export const model = (currentState: State, resW: ResponseType): controlState: 'SET_SOURCE_WRITE_BLOCK', sourceIndex: Option.some(source) as Option.Some, targetIndex: target, - targetIndexMappings: mergeMigrationMappingPropertyHashes( + targetIndexMappings: disableUnknownTypeMappingFields( stateP.targetIndexMappings, indices[source].mappings ), @@ -279,7 +279,7 @@ export const model = (currentState: State, resW: ResponseType): controlState: 'LEGACY_SET_WRITE_BLOCK', sourceIndex: Option.some(legacyReindexTarget) as Option.Some, targetIndex: target, - targetIndexMappings: mergeMigrationMappingPropertyHashes( + targetIndexMappings: disableUnknownTypeMappingFields( stateP.targetIndexMappings, indices[stateP.legacyIndex].mappings ), diff --git a/src/core/server/saved_objects/routes/bulk_create.ts b/src/core/server/saved_objects/routes/bulk_create.ts index 7574f26979ab1e..344a0d151cfb9e 100644 --- a/src/core/server/saved_objects/routes/bulk_create.ts +++ b/src/core/server/saved_objects/routes/bulk_create.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -44,7 +45,7 @@ export const registerBulkCreateRoute = (router: IRouter, { coreUsageData }: Rout ), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { overwrite } = req.query; const usageStatsClient = coreUsageData.getClient(); diff --git a/src/core/server/saved_objects/routes/bulk_get.ts b/src/core/server/saved_objects/routes/bulk_get.ts index 2484daf2ea8754..3838e4d3b3c8e7 100644 --- a/src/core/server/saved_objects/routes/bulk_get.ts +++ b/src/core/server/saved_objects/routes/bulk_get.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -28,7 +29,7 @@ export const registerBulkGetRoute = (router: IRouter, { coreUsageData }: RouteDe ), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const usageStatsClient = coreUsageData.getClient(); usageStatsClient.incrementSavedObjectsBulkGet({ request: req }).catch(() => {}); diff --git a/src/core/server/saved_objects/routes/bulk_update.ts b/src/core/server/saved_objects/routes/bulk_update.ts index 1a717f330d4c25..de47ab9c596114 100644 --- a/src/core/server/saved_objects/routes/bulk_update.ts +++ b/src/core/server/saved_objects/routes/bulk_update.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -39,7 +40,7 @@ export const registerBulkUpdateRoute = (router: IRouter, { coreUsageData }: Rout ), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const usageStatsClient = coreUsageData.getClient(); usageStatsClient.incrementSavedObjectsBulkUpdate({ request: req }).catch(() => {}); diff --git a/src/core/server/saved_objects/routes/create.ts b/src/core/server/saved_objects/routes/create.ts index db68b2f87d5772..2fa7acfb6cab62 100644 --- a/src/core/server/saved_objects/routes/create.ts +++ b/src/core/server/saved_objects/routes/create.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -43,7 +44,7 @@ export const registerCreateRoute = (router: IRouter, { coreUsageData }: RouteDep }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { type, id } = req.params; const { overwrite } = req.query; const { diff --git a/src/core/server/saved_objects/routes/delete.ts b/src/core/server/saved_objects/routes/delete.ts index dbbb0faf35c31f..609ce2692c7770 100644 --- a/src/core/server/saved_objects/routes/delete.ts +++ b/src/core/server/saved_objects/routes/delete.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -28,7 +29,7 @@ export const registerDeleteRoute = (router: IRouter, { coreUsageData }: RouteDep }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { type, id } = req.params; const { force } = req.query; diff --git a/src/core/server/saved_objects/routes/export.ts b/src/core/server/saved_objects/routes/export.ts index 76e422d24732e4..fa5517303f18f2 100644 --- a/src/core/server/saved_objects/routes/export.ts +++ b/src/core/server/saved_objects/routes/export.ts @@ -18,7 +18,7 @@ import { SavedObjectsExportByObjectOptions, SavedObjectsExportError, } from '../export'; -import { validateTypes, validateObjects } from './utils'; +import { validateTypes, validateObjects, catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { config: SavedObjectConfig; @@ -163,7 +163,7 @@ export const registerExportRoute = ( }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const cleaned = cleanOptions(req.body); const supportedTypes = context.core.savedObjects.typeRegistry .getImportableAndExportableTypes() diff --git a/src/core/server/saved_objects/routes/find.ts b/src/core/server/saved_objects/routes/find.ts index b9ad6ce15df2be..6ba23747cf3745 100644 --- a/src/core/server/saved_objects/routes/find.ts +++ b/src/core/server/saved_objects/routes/find.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -49,7 +50,7 @@ export const registerFindRoute = (router: IRouter, { coreUsageData }: RouteDepen }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const query = req.query; const namespaces = diff --git a/src/core/server/saved_objects/routes/get.ts b/src/core/server/saved_objects/routes/get.ts index 121cb82155b6ec..f28822d95d8142 100644 --- a/src/core/server/saved_objects/routes/get.ts +++ b/src/core/server/saved_objects/routes/get.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -25,7 +26,7 @@ export const registerGetRoute = (router: IRouter, { coreUsageData }: RouteDepend }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { type, id } = req.params; const usageStatsClient = coreUsageData.getClient(); diff --git a/src/core/server/saved_objects/routes/import.ts b/src/core/server/saved_objects/routes/import.ts index 81220f897f36ba..e84c638d3ec999 100644 --- a/src/core/server/saved_objects/routes/import.ts +++ b/src/core/server/saved_objects/routes/import.ts @@ -13,7 +13,7 @@ import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; import { SavedObjectConfig } from '../saved_objects_config'; import { SavedObjectsImportError } from '../import'; -import { createSavedObjectsStreamFromNdJson } from './utils'; +import { catchAndReturnBoomErrors, createSavedObjectsStreamFromNdJson } from './utils'; interface RouteDependencies { config: SavedObjectConfig; @@ -61,7 +61,7 @@ export const registerImportRoute = ( }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { overwrite, createNewCopies } = req.query; const usageStatsClient = coreUsageData.getClient(); diff --git a/src/core/server/saved_objects/routes/migrate.ts b/src/core/server/saved_objects/routes/migrate.ts index 19c6e3d99d6c21..404074124c92b7 100644 --- a/src/core/server/saved_objects/routes/migrate.ts +++ b/src/core/server/saved_objects/routes/migrate.ts @@ -8,6 +8,7 @@ import { IRouter } from '../../http'; import { IKibanaMigrator } from '../migrations'; +import { catchAndReturnBoomErrors } from './utils'; export const registerMigrateRoute = ( router: IRouter, @@ -21,7 +22,7 @@ export const registerMigrateRoute = ( tags: ['access:migrateSavedObjects'], }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const migrator = await migratorPromise; await migrator.runMigrations({ rerun: true }); return res.ok({ diff --git a/src/core/server/saved_objects/routes/resolve_import_errors.ts b/src/core/server/saved_objects/routes/resolve_import_errors.ts index 682b583f6a791f..2a664328d4df29 100644 --- a/src/core/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts @@ -13,8 +13,7 @@ import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; import { SavedObjectConfig } from '../saved_objects_config'; import { SavedObjectsImportError } from '../import'; -import { createSavedObjectsStreamFromNdJson } from './utils'; - +import { catchAndReturnBoomErrors, createSavedObjectsStreamFromNdJson } from './utils'; interface RouteDependencies { config: SavedObjectConfig; coreUsageData: CoreUsageDataSetup; @@ -69,7 +68,7 @@ export const registerResolveImportErrorsRoute = ( }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { createNewCopies } = req.query; const usageStatsClient = coreUsageData.getClient(); diff --git a/src/core/server/saved_objects/routes/update.ts b/src/core/server/saved_objects/routes/update.ts index 857973c5ae0068..cb605dac567772 100644 --- a/src/core/server/saved_objects/routes/update.ts +++ b/src/core/server/saved_objects/routes/update.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { CoreUsageDataSetup } from '../../core_usage_data'; +import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { coreUsageData: CoreUsageDataSetup; @@ -38,7 +39,7 @@ export const registerUpdateRoute = (router: IRouter, { coreUsageData }: RouteDep }), }, }, - router.handleLegacyErrors(async (context, req, res) => { + catchAndReturnBoomErrors(async (context, req, res) => { const { type, id } = req.params; const { attributes, version, references } = req.body; const options = { version, references }; diff --git a/src/core/server/saved_objects/routes/utils.test.ts b/src/core/server/saved_objects/routes/utils.test.ts index a24a4a1b51f6a5..623d2dcc71faca 100644 --- a/src/core/server/saved_objects/routes/utils.test.ts +++ b/src/core/server/saved_objects/routes/utils.test.ts @@ -9,6 +9,15 @@ import { createSavedObjectsStreamFromNdJson, validateTypes, validateObjects } from './utils'; import { Readable } from 'stream'; import { createPromiseFromStreams, createConcatStream } from '@kbn/utils'; +import { catchAndReturnBoomErrors } from './utils'; +import Boom from '@hapi/boom'; +import { + KibanaRequest, + RequestHandler, + RequestHandlerContext, + KibanaResponseFactory, + kibanaResponseFactory, +} from '../../'; async function readStreamToCompletion(stream: Readable) { return createPromiseFromStreams([stream, createConcatStream([])]); @@ -143,3 +152,69 @@ describe('validateObjects', () => { ).toBeUndefined(); }); }); + +describe('catchAndReturnBoomErrors', () => { + let context: RequestHandlerContext; + let request: KibanaRequest; + let response: KibanaResponseFactory; + + const createHandler = (handler: () => any): RequestHandler => () => { + return handler(); + }; + + beforeEach(() => { + context = {} as any; + request = {} as any; + response = kibanaResponseFactory; + }); + + it('should pass-though call parameters to the handler', async () => { + const handler = jest.fn(); + const wrapped = catchAndReturnBoomErrors(handler); + await wrapped(context, request, response); + expect(handler).toHaveBeenCalledWith(context, request, response); + }); + + it('should pass-though result from the handler', async () => { + const handler = createHandler(() => { + return 'handler-response'; + }); + const wrapped = catchAndReturnBoomErrors(handler); + const result = await wrapped(context, request, response); + expect(result).toBe('handler-response'); + }); + + it('should intercept and convert thrown Boom errors', async () => { + const handler = createHandler(() => { + throw Boom.notFound('not there'); + }); + const wrapped = catchAndReturnBoomErrors(handler); + const result = await wrapped(context, request, response); + expect(result.status).toBe(404); + expect(result.payload).toEqual({ + error: 'Not Found', + message: 'not there', + statusCode: 404, + }); + }); + + it('should re-throw non-Boom errors', async () => { + const handler = createHandler(() => { + throw new Error('something went bad'); + }); + const wrapped = catchAndReturnBoomErrors(handler); + await expect(wrapped(context, request, response)).rejects.toMatchInlineSnapshot( + `[Error: something went bad]` + ); + }); + + it('should re-throw Boom internal/500 errors', async () => { + const handler = createHandler(() => { + throw Boom.internal(); + }); + const wrapped = catchAndReturnBoomErrors(handler); + await expect(wrapped(context, request, response)).rejects.toMatchInlineSnapshot( + `[Error: Internal Server Error]` + ); + }); +}); diff --git a/src/core/server/saved_objects/routes/utils.ts b/src/core/server/saved_objects/routes/utils.ts index fc784ac80ed8d2..e933badfe80feb 100644 --- a/src/core/server/saved_objects/routes/utils.ts +++ b/src/core/server/saved_objects/routes/utils.ts @@ -7,7 +7,11 @@ */ import { Readable } from 'stream'; -import { SavedObject, SavedObjectsExportResultDetails } from 'src/core/server'; +import { + RequestHandlerWrapper, + SavedObject, + SavedObjectsExportResultDetails, +} from 'src/core/server'; import { createSplitStream, createMapStream, @@ -16,6 +20,7 @@ import { createListStream, createConcatStream, } from '@kbn/utils'; +import Boom from '@hapi/boom'; export async function createSavedObjectsStreamFromNdJson(ndJsonStream: Readable) { const savedObjects = await createPromiseFromStreams([ @@ -52,3 +57,30 @@ export function validateObjects( .join(', ')}`; } } + +/** + * Catches errors thrown by saved object route handlers and returns an error + * with the payload and statusCode of the boom error. + * + * This is very close to the core `router.handleLegacyErrors` except that it + * throws internal errors (statusCode: 500) so that the internal error's + * message get logged by Core. + * + * TODO: Remove once https://github.com/elastic/kibana/issues/65291 is fixed. + */ +export const catchAndReturnBoomErrors: RequestHandlerWrapper = (handler) => { + return async (context, request, response) => { + try { + return await handler(context, request, response); + } catch (e) { + if (Boom.isBoom(e) && e.output.statusCode !== 500) { + return response.customError({ + body: e.output.payload, + statusCode: e.output.statusCode, + headers: e.output.headers as { [key: string]: string }, + }); + } + throw e; + } + }; +}; diff --git a/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts b/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts index 717fd5fc5ab929..32f12193306e72 100644 --- a/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts +++ b/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts @@ -109,6 +109,27 @@ describe('savedObjectsClient/decorateEsError', () => { expect(SavedObjectsErrorHelpers.isNotFoundError(genericError)).toBe(true); }); + it('if saved objects index does not exist makes NotFound a SavedObjectsClient/generalError', () => { + const error = new esErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ + statusCode: 404, + body: { + error: { + reason: + 'no such index [.kibana_8.0.0] and [require_alias] request flag is [true] and [.kibana_8.0.0] is not an alias', + }, + }, + }) + ); + expect(SavedObjectsErrorHelpers.isGeneralError(error)).toBe(false); + const genericError = decorateEsError(error); + expect(genericError.message).toEqual( + `Saved object index alias [.kibana_8.0.0] not found: Response Error` + ); + expect(genericError.output.statusCode).toBe(500); + expect(SavedObjectsErrorHelpers.isGeneralError(error)).toBe(true); + }); + it('makes BadRequest a SavedObjectsClient/BadRequest error', () => { const error = new esErrors.ResponseError( elasticsearchClientMock.createApiResponse({ statusCode: 400 }) diff --git a/src/core/server/saved_objects/service/lib/decorate_es_error.ts b/src/core/server/saved_objects/service/lib/decorate_es_error.ts index 59a9210ff51306..e1aa1ab2f956da 100644 --- a/src/core/server/saved_objects/service/lib/decorate_es_error.ts +++ b/src/core/server/saved_objects/service/lib/decorate_es_error.ts @@ -63,6 +63,12 @@ export function decorateEsError(error: EsErrors) { } if (responseErrors.isNotFound(error.statusCode)) { + const match = error?.meta?.body?.error?.reason?.match( + /no such index \[(.+)\] and \[require_alias\] request flag is \[true\] and \[.+\] is not an alias/ + ); + if (match?.length > 0) { + return SavedObjectsErrorHelpers.decorateIndexAliasNotFoundError(error, match[1]); + } return SavedObjectsErrorHelpers.createGenericNotFoundError(); } diff --git a/src/core/server/saved_objects/service/lib/errors.ts b/src/core/server/saved_objects/service/lib/errors.ts index 2495679a2f8c2d..581145c7c09d1b 100644 --- a/src/core/server/saved_objects/service/lib/errors.ts +++ b/src/core/server/saved_objects/service/lib/errors.ts @@ -135,6 +135,19 @@ export class SavedObjectsErrorHelpers { return decorate(Boom.notFound(), CODE_NOT_FOUND, 404); } + public static createIndexAliasNotFoundError(alias: string) { + return SavedObjectsErrorHelpers.decorateIndexAliasNotFoundError(Boom.internal(), alias); + } + + public static decorateIndexAliasNotFoundError(error: Error, alias: string) { + return decorate( + error, + CODE_GENERAL_ERROR, + 500, + `Saved object index alias [${alias}] not found` + ); + } + public static isNotFoundError(error: Error | DecoratedError) { return isSavedObjectsClientError(error) && error[code] === CODE_NOT_FOUND; } @@ -185,4 +198,8 @@ export class SavedObjectsErrorHelpers { public static decorateGeneralError(error: Error, reason?: string) { return decorate(error, CODE_GENERAL_ERROR, 500, reason); } + + public static isGeneralError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_GENERAL_ERROR; + } } diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 0a1c18c01ad82d..aac508fb5b909c 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -18,6 +18,7 @@ import { DocumentMigrator } from '../../migrations/core/document_migrator'; import { mockKibanaMigrator } from '../../migrations/kibana/kibana_migrator.mock'; import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; import { esKuery } from '../../es_query'; +import { errors as EsErrors } from '@elastic/elasticsearch'; const { nodeTypes } = esKuery; jest.mock('./search_dsl/search_dsl', () => ({ getSearchDsl: jest.fn() })); @@ -4341,8 +4342,14 @@ describe('SavedObjectsRepository', () => { }); it(`throws when ES is unable to find the document during update`, async () => { + const notFoundError = new EsErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ + statusCode: 404, + body: { error: { type: 'es_type', reason: 'es_reason' } }, + }) + ); client.update.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + elasticsearchClientMock.createErrorTransportRequestPromise(notFoundError) ); await expectNotFoundError(type, id); expect(client.update).toHaveBeenCalledTimes(1); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index a662a374b063ef..fcd72aa4326a20 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -299,6 +299,7 @@ export class SavedObjectsRepository { refresh, body: raw._source, ...(overwrite && version ? decodeRequestVersion(version) : {}), + require_alias: true, }; const { body } = @@ -469,6 +470,7 @@ export class SavedObjectsRepository { const bulkResponse = bulkCreateParams.length ? await this.client.bulk({ refresh, + require_alias: true, body: bulkCreateParams, }) : undefined; @@ -1117,8 +1119,8 @@ export class SavedObjectsRepository { ...(Array.isArray(references) && { references }), }; - const { body, statusCode } = await this.client.update( - { + const { body } = await this.client + .update({ id: this._serializer.generateRawId(namespace, type, id), index: this.getIndexForType(type), ...getExpectedVersionProperties(version, preflightResult), @@ -1128,14 +1130,15 @@ export class SavedObjectsRepository { doc, }, _source_includes: ['namespace', 'namespaces', 'originId'], - }, - { ignore: [404] } - ); - - if (statusCode === 404) { - // see "404s from missing index" above - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } + require_alias: true, + }) + .catch((err) => { + if (SavedObjectsErrorHelpers.isNotFoundError(err)) { + // see "404s from missing index" above + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + throw err; + }); const { originId } = body.get._source; let namespaces = []; @@ -1496,6 +1499,7 @@ export class SavedObjectsRepository { refresh, body: bulkUpdateParams, _source_includes: ['originId'], + require_alias: true, }) : undefined; @@ -1712,6 +1716,7 @@ export class SavedObjectsRepository { id: raw._id, index: this.getIndexForType(type), refresh, + require_alias: true, _source: 'true', body: { script: { @@ -1933,12 +1938,18 @@ export class SavedObjectsRepository { } } -function getBulkOperationError(error: { type: string; reason?: string }, type: string, id: string) { +function getBulkOperationError( + error: { type: string; reason?: string; index?: string }, + type: string, + id: string +) { switch (error.type) { case 'version_conflict_engine_exception': return errorContent(SavedObjectsErrorHelpers.createConflictError(type, id)); case 'document_missing_exception': return errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)); + case 'index_not_found_exception': + return errorContent(SavedObjectsErrorHelpers.createIndexAliasNotFoundError(error.index!)); default: return { message: error.reason || JSON.stringify(error), diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 40a12290be31b8..f3191c5625f8d9 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2335,6 +2335,8 @@ export class SavedObjectsErrorHelpers { // (undocumented) static createGenericNotFoundError(type?: string | null, id?: string | null): DecoratedError; // (undocumented) + static createIndexAliasNotFoundError(alias: string): DecoratedError; + // (undocumented) static createInvalidVersionError(versionInput?: string): DecoratedError; // (undocumented) static createTooManyRequestsError(type: string, id: string): DecoratedError; @@ -2353,6 +2355,8 @@ export class SavedObjectsErrorHelpers { // (undocumented) static decorateGeneralError(error: Error, reason?: string): DecoratedError; // (undocumented) + static decorateIndexAliasNotFoundError(error: Error, alias: string): DecoratedError; + // (undocumented) static decorateNotAuthorizedError(error: Error, reason?: string): DecoratedError; // (undocumented) static decorateRequestEntityTooLargeError(error: Error, reason?: string): DecoratedError; @@ -2369,6 +2373,8 @@ export class SavedObjectsErrorHelpers { // (undocumented) static isForbiddenError(error: Error | DecoratedError): boolean; // (undocumented) + static isGeneralError(error: Error | DecoratedError): boolean; + // (undocumented) static isInvalidVersionError(error: Error | DecoratedError): boolean; // (undocumented) static isNotAuthorizedError(error: Error | DecoratedError): boolean; diff --git a/src/core/server/ui_settings/integration_tests/doc_exists.ts b/src/core/server/ui_settings/integration_tests/doc_exists.ts index b02f2ec9c76105..86a9a24fab6de6 100644 --- a/src/core/server/ui_settings/integration_tests/doc_exists.ts +++ b/src/core/server/ui_settings/integration_tests/doc_exists.ts @@ -8,7 +8,7 @@ import { getServices, chance } from './lib'; -export function docExistsSuite() { +export const docExistsSuite = (savedObjectsIndex: string) => () => { async function setup(options: any = {}) { const { initialSettings } = options; @@ -16,7 +16,7 @@ export function docExistsSuite() { // delete the kibana index to ensure we start fresh await callCluster('deleteByQuery', { - index: kbnServer.config.get('kibana.index'), + index: savedObjectsIndex, body: { conflicts: 'proceed', query: { match_all: {} }, @@ -212,4 +212,4 @@ export function docExistsSuite() { }); }); }); -} +}; diff --git a/src/core/server/ui_settings/integration_tests/doc_missing.ts b/src/core/server/ui_settings/integration_tests/doc_missing.ts index ef3b3928e0d9cd..9fa3e4c1cfe78a 100644 --- a/src/core/server/ui_settings/integration_tests/doc_missing.ts +++ b/src/core/server/ui_settings/integration_tests/doc_missing.ts @@ -8,7 +8,7 @@ import { getServices, chance } from './lib'; -export function docMissingSuite() { +export const docMissingSuite = (savedObjectsIndex: string) => () => { // ensure the kibana index has no documents beforeEach(async () => { const { kbnServer, callCluster } = getServices(); @@ -22,7 +22,7 @@ export function docMissingSuite() { // delete all docs from kibana index to ensure savedConfig is not found await callCluster('deleteByQuery', { - index: kbnServer.config.get('kibana.index'), + index: savedObjectsIndex, body: { query: { match_all: {} }, }, @@ -136,4 +136,4 @@ export function docMissingSuite() { }); }); }); -} +}; diff --git a/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts b/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts index f3a02cfe176e92..78fdab7eb8c5d3 100644 --- a/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts +++ b/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts @@ -8,7 +8,7 @@ import { getServices, chance } from './lib'; -export function docMissingAndIndexReadOnlySuite() { +export const docMissingAndIndexReadOnlySuite = (savedObjectsIndex: string) => () => { // ensure the kibana index has no documents beforeEach(async () => { const { kbnServer, callCluster } = getServices(); @@ -22,7 +22,7 @@ export function docMissingAndIndexReadOnlySuite() { // delete all docs from kibana index to ensure savedConfig is not found await callCluster('deleteByQuery', { - index: kbnServer.config.get('kibana.index'), + index: savedObjectsIndex, body: { query: { match_all: {} }, }, @@ -30,7 +30,7 @@ export function docMissingAndIndexReadOnlySuite() { // set the index to read only await callCluster('indices.putSettings', { - index: kbnServer.config.get('kibana.index'), + index: savedObjectsIndex, body: { index: { blocks: { @@ -42,11 +42,11 @@ export function docMissingAndIndexReadOnlySuite() { }); afterEach(async () => { - const { kbnServer, callCluster } = getServices(); + const { callCluster } = getServices(); // disable the read only block await callCluster('indices.putSettings', { - index: kbnServer.config.get('kibana.index'), + index: savedObjectsIndex, body: { index: { blocks: { @@ -142,4 +142,4 @@ export function docMissingAndIndexReadOnlySuite() { }); }); }); -} +}; diff --git a/src/core/server/ui_settings/integration_tests/index.test.ts b/src/core/server/ui_settings/integration_tests/index.test.ts index 184c75d88f3b8c..6e6c357e6cccc6 100644 --- a/src/core/server/ui_settings/integration_tests/index.test.ts +++ b/src/core/server/ui_settings/integration_tests/index.test.ts @@ -6,20 +6,25 @@ * Side Public License, v 1. */ +import { Env } from '@kbn/config'; +import { REPO_ROOT } from '@kbn/dev-utils'; +import { getEnvOptions } from '@kbn/config/target/mocks'; import { startServers, stopServers } from './lib'; - import { docExistsSuite } from './doc_exists'; import { docMissingSuite } from './doc_missing'; import { docMissingAndIndexReadOnlySuite } from './doc_missing_and_index_read_only'; +const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; +const savedObjectIndex = `.kibana_${kibanaVersion}_001`; + describe('uiSettings/routes', function () { jest.setTimeout(10000); beforeAll(startServers); /* eslint-disable jest/valid-describe */ - describe('doc missing', docMissingSuite); - describe('doc missing and index readonly', docMissingAndIndexReadOnlySuite); - describe('doc exists', docExistsSuite); + describe('doc missing', docMissingSuite(savedObjectIndex)); + describe('doc missing and index readonly', docMissingAndIndexReadOnlySuite(savedObjectIndex)); + describe('doc exists', docExistsSuite(savedObjectIndex)); /* eslint-enable jest/valid-describe */ afterAll(stopServers); }); diff --git a/src/core/server/ui_settings/integration_tests/lib/servers.ts b/src/core/server/ui_settings/integration_tests/lib/servers.ts index 1bea45da51af90..87176bed5de114 100644 --- a/src/core/server/ui_settings/integration_tests/lib/servers.ts +++ b/src/core/server/ui_settings/integration_tests/lib/servers.ts @@ -37,9 +37,6 @@ export async function startServers() { adjustTimeout: (t) => jest.setTimeout(t), settings: { kbn: { - migrations: { - enableV2: false, - }, uiSettings: { overrides: { foo: 'bar', diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts index cf5589fecdf438..011ba67a055121 100644 --- a/src/core/test_helpers/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -40,7 +40,7 @@ const DEFAULTS_SETTINGS = { }, logging: { silent: true }, plugins: {}, - migrations: { skip: true }, + migrations: { skip: false }, }; const DEFAULT_SETTINGS_WITH_CORE_PLUGINS = { diff --git a/test/accessibility/apps/kibana_overview.ts b/test/accessibility/apps/kibana_overview.ts index a6ecd491f169f4..8481e2bf334aa6 100644 --- a/test/accessibility/apps/kibana_overview.ts +++ b/test/accessibility/apps/kibana_overview.ts @@ -16,7 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); before(async () => { - await esArchiver.load('empty_kibana'); + await esArchiver.emptyKibanaIndex(); await PageObjects.common.navigateToApp('kibanaOverview'); }); @@ -25,7 +25,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { useActualUrl: true, }); await PageObjects.home.removeSampleDataSet('flights'); - await esArchiver.unload('empty_kibana'); }); it('Getting started view', async () => { diff --git a/test/api_integration/apis/home/sample_data.ts b/test/api_integration/apis/home/sample_data.ts index 64ef11167b3338..b889b59fdaf329 100644 --- a/test/api_integration/apis/home/sample_data.ts +++ b/test/api_integration/apis/home/sample_data.ts @@ -11,11 +11,15 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); const es = getService('es'); const MILLISECOND_IN_WEEK = 1000 * 60 * 60 * 24 * 7; describe('sample data apis', () => { + before(async () => { + await esArchiver.emptyKibanaIndex(); + }); describe('list', () => { it('should return list of sample data sets with installed status', async () => { const resp = await supertest.get(`/api/sample_data`).set('kbn-xsrf', 'kibana').expect(200); diff --git a/test/api_integration/apis/saved_objects/bulk_create.ts b/test/api_integration/apis/saved_objects/bulk_create.ts index 6239b930434aff..57b7ff0935f587 100644 --- a/test/api_integration/apis/saved_objects/bulk_create.ts +++ b/test/api_integration/apis/saved_objects/bulk_create.ts @@ -97,10 +97,11 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); - it('should return 200 with individual responses', async () => + it('should return 200 with errors', async () => { + await new Promise((resolve) => setTimeout(resolve, 2000)); await supertest .post('/api/saved_objects/_bulk_create') .send(BULK_REQUESTS) @@ -109,38 +110,27 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.body).to.eql({ saved_objects: [ { - type: 'visualization', - id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - updated_at: resp.body.saved_objects[0].updated_at, - version: resp.body.saved_objects[0].version, - attributes: { - title: 'An existing visualization', - }, - references: [], - namespaces: ['default'], - migrationVersion: { - visualization: resp.body.saved_objects[0].migrationVersion.visualization, + id: BULK_REQUESTS[0].id, + type: BULK_REQUESTS[0].type, + error: { + error: 'Internal Server Error', + message: 'An internal server error occurred', + statusCode: 500, }, - coreMigrationVersion: KIBANA_VERSION, // updated from 1.2.3 to the latest kibana version }, { - type: 'dashboard', - id: 'a01b2f57-fcfd-4864-b735-09e28f0d815e', - updated_at: resp.body.saved_objects[1].updated_at, - version: resp.body.saved_objects[1].version, - attributes: { - title: 'A great new dashboard', - }, - references: [], - namespaces: ['default'], - migrationVersion: { - dashboard: resp.body.saved_objects[1].migrationVersion.dashboard, + id: BULK_REQUESTS[1].id, + type: BULK_REQUESTS[1].type, + error: { + error: 'Internal Server Error', + message: 'An internal server error occurred', + statusCode: 500, }, - coreMigrationVersion: KIBANA_VERSION, }, ], }); - })); + }); + }); }); }); } diff --git a/test/api_integration/apis/saved_objects/bulk_get.ts b/test/api_integration/apis/saved_objects/bulk_get.ts index e9514d7d55457a..77f84dee25ded1 100644 --- a/test/api_integration/apis/saved_objects/bulk_get.ts +++ b/test/api_integration/apis/saved_objects/bulk_get.ts @@ -108,7 +108,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); it('should return 200 with individual responses', async () => diff --git a/test/api_integration/apis/saved_objects/bulk_update.ts b/test/api_integration/apis/saved_objects/bulk_update.ts index d9e3c278695910..a5f5262196346e 100644 --- a/test/api_integration/apis/saved_objects/bulk_update.ts +++ b/test/api_integration/apis/saved_objects/bulk_update.ts @@ -235,10 +235,10 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); - it('should return generic 404', async () => { + it('should return 200 with errors', async () => { const response = await supertest .put(`/api/saved_objects/_bulk_update`) .send([ @@ -267,9 +267,9 @@ export default function ({ getService }: FtrProviderContext) { id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', type: 'visualization', error: { - statusCode: 404, - error: 'Not Found', - message: 'Saved object [visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab] not found', + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', }, }); @@ -277,9 +277,9 @@ export default function ({ getService }: FtrProviderContext) { id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', type: 'dashboard', error: { - statusCode: 404, - error: 'Not Found', - message: 'Saved object [dashboard/be3733a0-9efe-11e7-acb3-3dab96693fab] not found', + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', }, }); }); diff --git a/test/api_integration/apis/saved_objects/create.ts b/test/api_integration/apis/saved_objects/create.ts index 355e5df1f18958..de31b621a64803 100644 --- a/test/api_integration/apis/saved_objects/create.ts +++ b/test/api_integration/apis/saved_objects/create.ts @@ -83,10 +83,10 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); - it('should return 200 and create kibana index', async () => { + it('should return 500 and not auto-create saved objects index', async () => { await supertest .post(`/api/saved_objects/visualization`) .send({ @@ -94,50 +94,16 @@ export default function ({ getService }: FtrProviderContext) { title: 'My favorite vis', }, }) - .expect(200) + .expect(500) .then((resp) => { - // loose uuid validation - expect(resp.body) - .to.have.property('id') - .match(/^[0-9a-f-]{36}$/); - - // loose ISO8601 UTC time with milliseconds validation - expect(resp.body) - .to.have.property('updated_at') - .match(/^[\d-]{10}T[\d:\.]{12}Z$/); - expect(resp.body).to.eql({ - id: resp.body.id, - type: 'visualization', - migrationVersion: resp.body.migrationVersion, - coreMigrationVersion: KIBANA_VERSION, - updated_at: resp.body.updated_at, - version: resp.body.version, - attributes: { - title: 'My favorite vis', - }, - references: [], - namespaces: ['default'], + error: 'Internal Server Error', + message: 'An internal server error occurred.', + statusCode: 500, }); - expect(resp.body.migrationVersion).to.be.ok(); }); - expect((await es.indices.exists({ index: '.kibana' })).body).to.be(true); - }); - - it('result should have the latest coreMigrationVersion', async () => { - await supertest - .post(`/api/saved_objects/visualization`) - .send({ - attributes: { - title: 'My favorite vis', - }, - coreMigrationVersion: '1.2.3', - }) - .expect(200) - .then((resp) => { - expect(resp.body.coreMigrationVersion).to.eql(KIBANA_VERSION); - }); + expect((await es.indices.exists({ index: '.kibana' })).body).to.be(false); }); }); }); diff --git a/test/api_integration/apis/saved_objects/delete.ts b/test/api_integration/apis/saved_objects/delete.ts index 5247bc74131d43..0dfece825d3a10 100644 --- a/test/api_integration/apis/saved_objects/delete.ts +++ b/test/api_integration/apis/saved_objects/delete.ts @@ -44,7 +44,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); it('returns generic 404 when kibana index is missing', async () => diff --git a/test/api_integration/apis/saved_objects/export.ts b/test/api_integration/apis/saved_objects/export.ts index 32a72f374cbe17..5206d51054745c 100644 --- a/test/api_integration/apis/saved_objects/export.ts +++ b/test/api_integration/apis/saved_objects/export.ts @@ -534,7 +534,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); it('should return empty response', async () => { diff --git a/test/api_integration/apis/saved_objects/find.ts b/test/api_integration/apis/saved_objects/find.ts index be31e0faf1e467..66c2a083c79e51 100644 --- a/test/api_integration/apis/saved_objects/find.ts +++ b/test/api_integration/apis/saved_objects/find.ts @@ -40,7 +40,7 @@ export default function ({ getService }: FtrProviderContext) { { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - version: 'WzIsMV0=', + version: 'WzE4LDJd', attributes: { title: 'Count of requests', }, @@ -137,7 +137,7 @@ export default function ({ getService }: FtrProviderContext) { { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - version: 'WzIsMV0=', + version: 'WzE4LDJd', attributes: { title: 'Count of requests', }, @@ -174,7 +174,7 @@ export default function ({ getService }: FtrProviderContext) { { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - version: 'WzIsMV0=', + version: 'WzE4LDJd', attributes: { title: 'Count of requests', }, @@ -209,7 +209,7 @@ export default function ({ getService }: FtrProviderContext) { score: 0, type: 'visualization', updated_at: '2017-09-21T18:51:23.794Z', - version: 'WzYsMV0=', + version: 'WzIyLDJd', }, ], }); @@ -256,7 +256,7 @@ export default function ({ getService }: FtrProviderContext) { migrationVersion: resp.body.saved_objects[0].migrationVersion, coreMigrationVersion: KIBANA_VERSION, updated_at: '2017-09-21T18:51:23.794Z', - version: 'WzIsMV0=', + version: 'WzE4LDJd', }, ], }); @@ -426,11 +426,11 @@ export default function ({ getService }: FtrProviderContext) { })); }); - describe.skip('without kibana index', () => { + describe('without kibana index', () => { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); it('should return 200 with empty response', async () => diff --git a/test/api_integration/apis/saved_objects/get.ts b/test/api_integration/apis/saved_objects/get.ts index f912a2efcf0d9c..84ab6e36956d5d 100644 --- a/test/api_integration/apis/saved_objects/get.ts +++ b/test/api_integration/apis/saved_objects/get.ts @@ -78,7 +78,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); it('should return basic 404 without mentioning index', async () => diff --git a/test/api_integration/apis/saved_objects/resolve_import_errors.ts b/test/api_integration/apis/saved_objects/resolve_import_errors.ts index 4fcce29905beb1..b203a2c7b7071b 100644 --- a/test/api_integration/apis/saved_objects/resolve_import_errors.ts +++ b/test/api_integration/apis/saved_objects/resolve_import_errors.ts @@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const esDeleteAllIndices = getService('esDeleteAllIndices'); describe('resolve_import_errors', () => { // mock success results including metadata @@ -34,7 +35,11 @@ export default function ({ getService }: FtrProviderContext) { describe('without kibana index', () => { // Cleanup data that got created in import - after(() => esArchiver.unload('saved_objects/basic')); + before( + async () => + // just in case the kibana server has recreated it + await esDeleteAllIndices('.kibana*') + ); it('should return 200 and import nothing when empty parameters are passed in', async () => { await supertest @@ -51,7 +56,7 @@ export default function ({ getService }: FtrProviderContext) { }); }); - it('should return 200 and import everything when overwrite parameters contains all objects', async () => { + it('should return 200 with internal server errors', async () => { await supertest .post('/api/saved_objects/_resolve_import_errors') .field( @@ -78,12 +83,42 @@ export default function ({ getService }: FtrProviderContext) { .expect(200) .then((resp) => { expect(resp.body).to.eql({ - success: true, - successCount: 3, - successResults: [ - { ...indexPattern, overwrite: true }, - { ...visualization, overwrite: true }, - { ...dashboard, overwrite: true }, + successCount: 0, + success: false, + errors: [ + { + ...indexPattern, + ...{ title: indexPattern.meta.title }, + overwrite: true, + error: { + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', + type: 'unknown', + }, + }, + { + ...visualization, + ...{ title: visualization.meta.title }, + overwrite: true, + error: { + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', + type: 'unknown', + }, + }, + { + ...dashboard, + ...{ title: dashboard.meta.title }, + overwrite: true, + error: { + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', + type: 'unknown', + }, + }, ], warnings: [], }); diff --git a/test/api_integration/apis/saved_objects/update.ts b/test/api_integration/apis/saved_objects/update.ts index ce14e9cea7b13f..631046a0564a36 100644 --- a/test/api_integration/apis/saved_objects/update.ts +++ b/test/api_integration/apis/saved_objects/update.ts @@ -121,10 +121,10 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); - it('should return generic 404', async () => + it('should return 500', async () => await supertest .put(`/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`) .send({ @@ -132,13 +132,12 @@ export default function ({ getService }: FtrProviderContext) { title: 'My second favorite vis', }, }) - .expect(404) + .expect(500) .then((resp) => { expect(resp.body).eql({ - statusCode: 404, - error: 'Not Found', - message: - 'Saved object [visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab] not found', + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred.', }); })); }); diff --git a/test/api_integration/apis/saved_objects_management/find.ts b/test/api_integration/apis/saved_objects_management/find.ts index 87de59a94fd249..6ab2352ebb05f6 100644 --- a/test/api_integration/apis/saved_objects_management/find.ts +++ b/test/api_integration/apis/saved_objects_management/find.ts @@ -42,7 +42,7 @@ export default function ({ getService }: FtrProviderContext) { { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - version: 'WzIsMV0=', + version: 'WzE4LDJd', attributes: { title: 'Count of requests', }, @@ -184,7 +184,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); it('should return 200 with empty response', async () => diff --git a/test/api_integration/apis/saved_objects_management/get.ts b/test/api_integration/apis/saved_objects_management/get.ts index 69c85428d06247..4dfd06a61eecf5 100644 --- a/test/api_integration/apis/saved_objects_management/get.ts +++ b/test/api_integration/apis/saved_objects_management/get.ts @@ -45,7 +45,7 @@ export default function ({ getService }: FtrProviderContext) { before( async () => // just in case the kibana server has recreated it - await esDeleteAllIndices('.kibana') + await esDeleteAllIndices('.kibana*') ); it('should return 404 for object that no longer exists', async () => diff --git a/test/api_integration/apis/search/search.ts b/test/api_integration/apis/search/search.ts index 2b61ed7586384d..bc092dd3889bb8 100644 --- a/test/api_integration/apis/search/search.ts +++ b/test/api_integration/apis/search/search.ts @@ -17,6 +17,7 @@ export default function ({ getService }: FtrProviderContext) { describe('search', () => { before(async () => { + await esArchiver.emptyKibanaIndex(); await esArchiver.loadIfNeeded('../../../functional/fixtures/es_archiver/logstash_functional'); }); diff --git a/test/api_integration/apis/telemetry/opt_in.ts b/test/api_integration/apis/telemetry/opt_in.ts index 2e42fbfc6ac60a..7e0564ac44a43b 100644 --- a/test/api_integration/apis/telemetry/opt_in.ts +++ b/test/api_integration/apis/telemetry/opt_in.ts @@ -14,10 +14,13 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function optInTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + describe('/api/telemetry/v2/optIn API', () => { let defaultAttributes: TelemetrySavedObjectAttributes; let kibanaVersion: any; before(async () => { + await esArchiver.emptyKibanaIndex(); const kibanaVersionAccessor = kibanaServer.version; kibanaVersion = await kibanaVersionAccessor.get(); defaultAttributes = diff --git a/test/api_integration/apis/telemetry/telemetry_local.ts b/test/api_integration/apis/telemetry/telemetry_local.ts index 23a0d3fb2cd3cc..b424cab9ff45b2 100644 --- a/test/api_integration/apis/telemetry/telemetry_local.ts +++ b/test/api_integration/apis/telemetry/telemetry_local.ts @@ -177,6 +177,7 @@ export default function ({ getService }: FtrProviderContext) { describe('basic behaviour', () => { let savedObjectIds: string[] = []; before('create application usage entries', async () => { + await esArchiver.emptyKibanaIndex(); savedObjectIds = await Promise.all([ createSavedObject(), createSavedObject('appView1'), diff --git a/test/api_integration/apis/ui_counters/ui_counters.ts b/test/api_integration/apis/ui_counters/ui_counters.ts index c2286f8ea3dce6..c287e73e3ace9c 100644 --- a/test/api_integration/apis/ui_counters/ui_counters.ts +++ b/test/api_integration/apis/ui_counters/ui_counters.ts @@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); const es = getService('es'); const createUiCounterEvent = (eventName: string, type: UiCounterMetricType, count = 1) => ({ @@ -24,6 +25,9 @@ export default function ({ getService }: FtrProviderContext) { // FLAKY: https://github.com/elastic/kibana/issues/85086 describe.skip('UI Counters API', () => { + before(async () => { + await esArchiver.emptyKibanaIndex(); + }); const dayDate = moment().format('DDMMYYYY'); it('stores ui counter events in savedObjects', async () => { diff --git a/test/api_integration/apis/ui_metric/ui_metric.ts b/test/api_integration/apis/ui_metric/ui_metric.ts index 1e80487da551a3..99007376e1ea49 100644 --- a/test/api_integration/apis/ui_metric/ui_metric.ts +++ b/test/api_integration/apis/ui_metric/ui_metric.ts @@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); const es = getService('es'); const createStatsMetric = ( @@ -34,6 +35,10 @@ export default function ({ getService }: FtrProviderContext) { }); describe('ui_metric savedObject data', () => { + before(async () => { + await esArchiver.emptyKibanaIndex(); + }); + it('increments the count field in the document defined by the {app}/{action_type} path', async () => { const reportManager = new ReportManager(); const uiStatsMetric = createStatsMetric('myEvent'); diff --git a/test/common/config.js b/test/common/config.js index 451324d46f62de..9d108f05fd1fca 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -61,8 +61,6 @@ export default function () { ...(!!process.env.CODE_COVERAGE ? [`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'coverage')}`] : []), - // Disable v2 migrations in tests for now - '--migrations.enableV2=false', ], }, services, diff --git a/test/common/services/kibana_server/extend_es_archiver.js b/test/common/services/kibana_server/extend_es_archiver.js index c2d01eef267bc8..9a06dd7b749699 100644 --- a/test/common/services/kibana_server/extend_es_archiver.js +++ b/test/common/services/kibana_server/extend_es_archiver.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -const ES_ARCHIVER_LOAD_METHODS = ['load', 'loadIfNeeded', 'unload']; +const ES_ARCHIVER_LOAD_METHODS = ['load', 'loadIfNeeded', 'unload', 'emptyKibanaIndex']; const KIBANA_INDEX = '.kibana'; export function extendEsArchiver({ esArchiver, kibanaServer, retry, defaults }) { @@ -25,7 +25,7 @@ export function extendEsArchiver({ esArchiver, kibanaServer, retry, defaults }) const statsKeys = Object.keys(stats); const kibanaKeys = statsKeys.filter( // this also matches stats keys like '.kibana_1' and '.kibana_2,.kibana_1' - (key) => key.includes(KIBANA_INDEX) && (stats[key].created || stats[key].deleted) + (key) => key.includes(KIBANA_INDEX) && stats[key].created ); // if the kibana index was created by the esArchiver then update the uiSettings diff --git a/test/functional/apps/management/_import_objects.ts b/test/functional/apps/management/_import_objects.ts index d13ba0114a598a..e2a056359b48e2 100644 --- a/test/functional/apps/management/_import_objects.ts +++ b/test/functional/apps/management/_import_objects.ts @@ -27,9 +27,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe.skip('import objects', function describeIndexTests() { describe('.ndjson file', () => { beforeEach(async function () { + await esArchiver.load('management'); await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); - await esArchiver.load('management'); await PageObjects.settings.clickKibanaSavedObjects(); }); @@ -213,10 +213,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('.json file', () => { beforeEach(async function () { - // delete .kibana index and then wait for Kibana to re-create it + await esArchiver.load('saved_objects_imports'); await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); - await esArchiver.load('saved_objects_imports'); await PageObjects.settings.clickKibanaSavedObjects(); }); diff --git a/test/functional/apps/management/_index_pattern_filter.js b/test/functional/apps/management/_index_pattern_filter.js index b2d27002f690c5..eeb0b224d5f0ca 100644 --- a/test/functional/apps/management/_index_pattern_filter.js +++ b/test/functional/apps/management/_index_pattern_filter.js @@ -12,10 +12,11 @@ export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const retry = getService('retry'); const PageObjects = getPageObjects(['settings']); + const esArchiver = getService('esArchiver'); describe('index pattern filter', function describeIndexTests() { before(async function () { - // delete .kibana index and then wait for Kibana to re-create it + await esArchiver.emptyKibanaIndex(); await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); diff --git a/test/functional/apps/management/_index_patterns_empty.ts b/test/functional/apps/management/_index_patterns_empty.ts index a58c1298104709..90dd8cdc35c300 100644 --- a/test/functional/apps/management/_index_patterns_empty.ts +++ b/test/functional/apps/management/_index_patterns_empty.ts @@ -19,7 +19,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('index pattern empty view', () => { before(async () => { - await esArchiver.load('empty_kibana'); + await esArchiver.emptyKibanaIndex(); await esArchiver.unload('logstash_functional'); await esArchiver.unload('makelogs'); await kibanaServer.uiSettings.replace({}); @@ -27,7 +27,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { - await esArchiver.unload('empty_kibana'); await esArchiver.loadIfNeeded('makelogs'); // @ts-expect-error await es.transport.request({ diff --git a/test/functional/apps/management/_mgmt_import_saved_objects.js b/test/functional/apps/management/_mgmt_import_saved_objects.js index 27745654f495f9..8697dc49de46a9 100644 --- a/test/functional/apps/management/_mgmt_import_saved_objects.js +++ b/test/functional/apps/management/_mgmt_import_saved_objects.js @@ -18,14 +18,13 @@ export default function ({ getService, getPageObjects }) { describe('mgmt saved objects', function describeIndexTests() { beforeEach(async function () { - await esArchiver.load('empty_kibana'); + await esArchiver.emptyKibanaIndex(); await esArchiver.load('discover'); await PageObjects.settings.navigateTo(); }); afterEach(async function () { await esArchiver.unload('discover'); - await esArchiver.load('empty_kibana'); }); it('should import saved objects mgmt', async function () { diff --git a/test/functional/apps/management/_test_huge_fields.js b/test/functional/apps/management/_test_huge_fields.js index 7ccce2c10c7b1e..3102becbe181f6 100644 --- a/test/functional/apps/management/_test_huge_fields.js +++ b/test/functional/apps/management/_test_huge_fields.js @@ -20,6 +20,7 @@ export default function ({ getService, getPageObjects }) { const EXPECTED_FIELD_COUNT = '10006'; before(async function () { await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader']); + await esArchiver.emptyKibanaIndex(); await esArchiver.loadIfNeeded('large_fields'); await PageObjects.settings.createIndexPattern('testhuge', 'date'); }); diff --git a/test/functional/apps/management/index.ts b/test/functional/apps/management/index.ts index 15828295b190f0..d31245b5492d1c 100644 --- a/test/functional/apps/management/index.ts +++ b/test/functional/apps/management/index.ts @@ -14,13 +14,11 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { describe('management', function () { before(async () => { await esArchiver.unload('logstash_functional'); - await esArchiver.load('empty_kibana'); await esArchiver.loadIfNeeded('makelogs'); }); after(async () => { await esArchiver.unload('makelogs'); - await esArchiver.unload('empty_kibana'); }); describe('', function () { diff --git a/test/functional/apps/visualize/input_control_vis/input_control_range.ts b/test/functional/apps/visualize/input_control_vis/input_control_range.ts index 4f4e6d6655be51..caa008080b2a30 100644 --- a/test/functional/apps/visualize/input_control_vis/input_control_range.ts +++ b/test/functional/apps/visualize/input_control_vis/input_control_range.ts @@ -12,7 +12,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); const find = getService('find'); const security = getService('security'); const { visualize, visEditor } = getPageObjects(['visualize', 'visEditor']); @@ -53,7 +52,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.loadIfNeeded('long_window_logstash'); await esArchiver.load('visualize'); - await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); await security.testUser.restoreDefaults(); }); }); diff --git a/test/plugin_functional/test_suites/core_plugins/applications.ts b/test/plugin_functional/test_suites/core_plugins/applications.ts index 6c600aa996a33a..0e52b536410e4e 100644 --- a/test/plugin_functional/test_suites/core_plugins/applications.ts +++ b/test/plugin_functional/test_suites/core_plugins/applications.ts @@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const find = getService('find'); const retry = getService('retry'); const deployment = getService('deployment'); + const esArchiver = getService('esArchiver'); const loadingScreenNotShown = async () => expect(await testSubjects.exists('kbnLoadingMessage')).to.be(false); @@ -50,6 +51,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide describe('ui applications', function describeIndexTests() { before(async () => { + await esArchiver.emptyKibanaIndex(); await PageObjects.common.navigateToApp('foo'); }); diff --git a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts index 918e9f16c5dae7..7947616ac65684 100644 --- a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts +++ b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts @@ -12,8 +12,12 @@ import '../../plugins/core_provider_plugin/types'; export default function ({ getService }: PluginFunctionalProviderContext) { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); describe('index patterns', function () { + before(async () => { + await esArchiver.emptyKibanaIndex(); + }); let indexPatternId = ''; it('can create an index pattern', async () => { diff --git a/test/plugin_functional/test_suites/saved_objects_management/import_warnings.ts b/test/plugin_functional/test_suites/saved_objects_management/import_warnings.ts index 10a088426c8fdd..da4c785342733b 100644 --- a/test/plugin_functional/test_suites/saved_objects_management/import_warnings.ts +++ b/test/plugin_functional/test_suites/saved_objects_management/import_warnings.ts @@ -10,10 +10,15 @@ import path from 'path'; import expect from '@kbn/expect'; import { PluginFunctionalProviderContext } from '../../services'; -export default function ({ getPageObjects }: PluginFunctionalProviderContext) { +export default function ({ getPageObjects, getService }: PluginFunctionalProviderContext) { const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects']); + const esArchiver = getService('esArchiver'); describe('import warnings', () => { + before(async () => { + await esArchiver.emptyKibanaIndex(); + }); + beforeEach(async () => { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaSavedObjects(); diff --git a/test/security_functional/insecure_cluster_warning.ts b/test/security_functional/insecure_cluster_warning.ts index 229dac20390a29..44a0e2eb0e1212 100644 --- a/test/security_functional/insecure_cluster_warning.ts +++ b/test/security_functional/insecure_cluster_warning.ts @@ -31,6 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async () => { await browser.setLocalStorageItem('insecureClusterWarningVisibility', ''); await esArchiver.unload('hamlet'); + await esArchiver.emptyKibanaIndex(); }); it('should not warn when the cluster contains no user data', async () => { diff --git a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts b/x-pack/test/functional/apps/dashboard/_async_dashboard.ts index 55474bd5a76884..5b2632ef710e4d 100644 --- a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts +++ b/x-pack/test/functional/apps/dashboard/_async_dashboard.ts @@ -13,6 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const browser = getService('browser'); const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); const log = getService('log'); const pieChart = getService('pieChart'); const find = getService('find'); @@ -30,6 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('sample data dashboard', function describeIndexTests() { before(async () => { + await esArchiver.emptyKibanaIndex(); await PageObjects.common.sleep(5000); await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { useActualUrl: true, diff --git a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts index bb60de86aef82b..95ebc7b2ff5d57 100644 --- a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts +++ b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts @@ -70,7 +70,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows all saved objects', async () => { const objects = await PageObjects.savedObjects.getRowTitles(); expect(objects).to.eql([ - 'Advanced Settings [6.0.0]', `Advanced Settings [${version}]`, 'A Dashboard', 'logstash-*', @@ -81,10 +80,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('can view all saved objects in applications', async () => { const bools = await PageObjects.savedObjects.getTableSummary(); expect(bools).to.eql([ - { - title: 'Advanced Settings [6.0.0]', - canViewInApp: false, - }, { title: `Advanced Settings [${version}]`, canViewInApp: false, @@ -189,7 +184,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows all saved objects', async () => { const objects = await PageObjects.savedObjects.getRowTitles(); expect(objects).to.eql([ - 'Advanced Settings [6.0.0]', `Advanced Settings [${version}]`, 'A Dashboard', 'logstash-*', @@ -200,10 +194,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('cannot view any saved objects in applications', async () => { const bools = await PageObjects.savedObjects.getTableSummary(); expect(bools).to.eql([ - { - title: 'Advanced Settings [6.0.0]', - canViewInApp: false, - }, { title: `Advanced Settings [${version}]`, canViewInApp: false, diff --git a/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/data.json b/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/data.json index f085bad4c507ec..b63ae2295f70b5 100644 --- a/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/data.json +++ b/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/data.json @@ -66,20 +66,3 @@ } } } - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "config:6.0.0", - "source": { - "config": { - "buildNum": 9007199254740991, - "defaultIndex": "logstash-*" - }, - "type": "config", - "updated_at": "2019-01-22T19:32:02.235Z" - } - } -} diff --git a/x-pack/test/functional/es_archives/visualize/default/data.json b/x-pack/test/functional/es_archives/visualize/default/data.json index fe29bad0fa3814..26b033e28b4da7 100644 --- a/x-pack/test/functional/es_archives/visualize/default/data.json +++ b/x-pack/test/functional/es_archives/visualize/default/data.json @@ -125,26 +125,8 @@ { "type": "doc", "value": { - "id": "custom-space:index-pattern:metricbeat-*", - "index": ".kibana_1", - "source": { - "index-pattern": { - "fieldFormatMap": "{\"aerospike.namespace.device.available.pct\":{\"id\":\"percent\",\"params\":{}},\"aerospike.namespace.device.free.pct\":{\"id\":\"percent\",\"params\":{}},\"aerospike.namespace.device.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aerospike.namespace.device.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aerospike.namespace.memory.free.pct\":{\"id\":\"percent\",\"params\":{}},\"aerospike.namespace.memory.used.data.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aerospike.namespace.memory.used.index.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aerospike.namespace.memory.used.sindex.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aerospike.namespace.memory.used.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.ec2.diskio.read.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.ec2.diskio.write.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.ec2.network.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.ec2.network.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.cpu.total.pct\":{\"id\":\"percent\",\"params\":{}},\"aws.rds.disk_usage.bin_log.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.free_local_storage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.free_storage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.freeable_memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.latency.commit\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.ddl\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.dml\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.insert\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.read\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.select\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.update\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.latency.write\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.replica_lag.sec\":{\"id\":\"duration\",\"params\":{}},\"aws.rds.swap_usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.rds.volume_used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.s3_daily_storage.bucket.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.s3_request.downloaded.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.s3_request.latency.first_byte.ms\":{\"id\":\"duration\",\"params\":{}},\"aws.s3_request.latency.total_request.ms\":{\"id\":\"duration\",\"params\":{}},\"aws.s3_request.requests.select_returned.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.s3_request.requests.select_scanned.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.s3_request.uploaded.bytes\":{\"id\":\"bytes\",\"params\":{}},\"aws.sqs.oldest_message_age.sec\":{\"id\":\"duration\",\"params\":{}},\"aws.sqs.sent_message_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_disk.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_disk.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_disk.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.degraded.ratio\":{\"id\":\"percent\",\"params\":{}},\"ceph.cluster_status.misplace.ratio\":{\"id\":\"percent\",\"params\":{}},\"ceph.cluster_status.pg.avail_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.pg.data_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.pg.total_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.pg.used_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.traffic.read_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.cluster_status.traffic.write_bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.monitor_health.store_stats.log.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.monitor_health.store_stats.misc.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.monitor_health.store_stats.sst.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.monitor_health.store_stats.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.osd_df.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.osd_df.total.byte\":{\"id\":\"bytes\",\"params\":{}},\"ceph.osd_df.used.byte\":{\"id\":\"bytes\",\"params\":{}},\"ceph.osd_df.used.pct\":{\"id\":\"percent\",\"params\":{}},\"ceph.pool_disk.stats.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"ceph.pool_disk.stats.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"client.bytes\":{\"id\":\"bytes\",\"params\":{}},\"client.nat.port\":{\"id\":\"string\",\"params\":{}},\"client.port\":{\"id\":\"string\",\"params\":{}},\"coredns.stats.dns.request.duration.ns.sum\":{\"id\":\"duration\",\"params\":{}},\"couchbase.bucket.data.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.bucket.disk.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.bucket.memory.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.bucket.quota.ram.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.bucket.quota.use.pct\":{\"id\":\"percent\",\"params\":{}},\"couchbase.cluster.hdd.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.hdd.quota.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.hdd.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.hdd.used.by_data.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.hdd.used.value.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.quota.total.per_node.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.quota.total.value.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.quota.used.per_node.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.quota.used.value.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.used.by_data.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.cluster.ram.used.value.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.node.couch.docs.data_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.node.couch.docs.disk_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"couchbase.node.mcd_memory.allocated.bytes\":{\"id\":\"bytes\",\"params\":{}},\"destination.bytes\":{\"id\":\"bytes\",\"params\":{}},\"destination.nat.port\":{\"id\":\"string\",\"params\":{}},\"destination.port\":{\"id\":\"string\",\"params\":{}},\"docker.cpu.core.*.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.cpu.kernel.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.cpu.system.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.cpu.total.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.cpu.user.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.diskio.read.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.diskio.summary.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.diskio.write.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.commit.peak\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.commit.total\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.limit\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.private_working_set.total\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.rss.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.memory.rss.total\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.usage.max\":{\"id\":\"bytes\",\"params\":{}},\"docker.memory.usage.pct\":{\"id\":\"percent\",\"params\":{}},\"docker.memory.usage.total\":{\"id\":\"bytes\",\"params\":{}},\"docker.network.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.network.inbound.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.network.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"docker.network.outbound.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.summary.primaries.segments.memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.summary.primaries.store.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.summary.total.segments.memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.summary.total.store.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.total.segments.memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.index.total.store.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.jvm.memory.heap.init.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.jvm.memory.heap.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.jvm.memory.nonheap.init.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.jvm.memory.nonheap.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.fs.summary.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.fs.summary.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.fs.summary.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.indices.segments.memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.old.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.old.peak.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.old.peak_max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.old.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.survivor.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.survivor.peak.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.survivor.peak_max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.survivor.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.young.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.young.peak.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.young.peak_max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"elasticsearch.node.stats.jvm.mem.pools.young.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"etcd.disk.mvcc_db_total_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"etcd.memory.go_memstats_alloc.bytes\":{\"id\":\"bytes\",\"params\":{}},\"etcd.network.client_grpc_received.bytes\":{\"id\":\"bytes\",\"params\":{}},\"etcd.network.client_grpc_sent.bytes\":{\"id\":\"bytes\",\"params\":{}},\"event.duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"nanoseconds\",\"outputFormat\":\"asMilliseconds\",\"outputPrecision\":1}},\"event.sequence\":{\"id\":\"string\",\"params\":{}},\"event.severity\":{\"id\":\"string\",\"params\":{}},\"golang.heap.allocations.active\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.allocations.allocated\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.allocations.idle\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.allocations.total\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.gc.next_gc_limit\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.system.obtained\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.system.released\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.system.stack\":{\"id\":\"bytes\",\"params\":{}},\"golang.heap.system.total\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.info.idle.pct\":{\"id\":\"percent\",\"params\":{}},\"haproxy.info.memory.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.info.ssl.frontend.session_reuse.pct\":{\"id\":\"percent\",\"params\":{}},\"haproxy.stat.compressor.bypassed.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.compressor.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.compressor.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.compressor.response.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"haproxy.stat.throttle.pct\":{\"id\":\"percent\",\"params\":{}},\"http.request.body.bytes\":{\"id\":\"bytes\",\"params\":{}},\"http.request.bytes\":{\"id\":\"bytes\",\"params\":{}},\"http.response.body.bytes\":{\"id\":\"bytes\",\"params\":{}},\"http.response.bytes\":{\"id\":\"bytes\",\"params\":{}},\"http.response.status_code\":{\"id\":\"string\",\"params\":{}},\"kibana.stats.process.memory.heap.size_limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kibana.stats.process.memory.heap.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kibana.stats.process.memory.heap.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.apiserver.http.request.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.apiserver.http.response.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.apiserver.process.memory.resident.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.apiserver.process.memory.virtual.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.cpu.usage.limit.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.container.cpu.usage.node.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.container.logs.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.logs.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.logs.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.request.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.memory.usage.limit.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.container.memory.usage.node.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.container.memory.workingset.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.rootfs.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.rootfs.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.container.rootfs.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.controllermanager.http.request.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.controllermanager.http.response.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.controllermanager.process.memory.resident.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.controllermanager.process.memory.virtual.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.fs.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.fs.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.fs.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.allocatable.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.memory.workingset.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.network.rx.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.network.tx.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.runtime.imagefs.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.runtime.imagefs.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.node.runtime.imagefs.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.cpu.usage.limit.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.pod.cpu.usage.node.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.pod.memory.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.memory.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.memory.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.memory.usage.limit.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.pod.memory.usage.node.pct\":{\"id\":\"percent\",\"params\":{}},\"kubernetes.pod.memory.working_set.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.network.rx.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.pod.network.tx.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.proxy.http.request.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.proxy.http.response.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.proxy.process.memory.resident.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.proxy.process.memory.virtual.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.scheduler.http.request.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.scheduler.http.response.size.bytes.sum\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.scheduler.process.memory.resident.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.scheduler.process.memory.virtual.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.system.memory.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.system.memory.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.system.memory.workingset.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.volume.fs.available.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.volume.fs.capacity.bytes\":{\"id\":\"bytes\",\"params\":{}},\"kubernetes.volume.fs.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.avg_obj_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.data_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.extent_free_list.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.file_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.index_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.dbstats.storage_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.replstatus.headroom.max\":{\"id\":\"duration\",\"params\":{}},\"mongodb.replstatus.headroom.min\":{\"id\":\"duration\",\"params\":{}},\"mongodb.replstatus.lag.max\":{\"id\":\"duration\",\"params\":{}},\"mongodb.replstatus.lag.min\":{\"id\":\"duration\",\"params\":{}},\"mongodb.replstatus.oplog.size.allocated\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.replstatus.oplog.size.used\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.extra_info.heap_usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.network.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.network.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.cache.dirty.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.cache.maximum.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.cache.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.log.max_file_size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.log.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mongodb.status.wired_tiger.log.write.bytes\":{\"id\":\"bytes\",\"params\":{}},\"mysql.status.bytes.received\":{\"id\":\"bytes\",\"params\":{}},\"mysql.status.bytes.sent\":{\"id\":\"bytes\",\"params\":{}},\"nats.stats.cpu\":{\"id\":\"percent\",\"params\":{}},\"nats.stats.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"nats.stats.mem.bytes\":{\"id\":\"bytes\",\"params\":{}},\"nats.stats.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"nats.stats.uptime\":{\"id\":\"duration\",\"params\":{}},\"nats.subscriptions.cache.hit_rate\":{\"id\":\"percent\",\"params\":{}},\"network.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.data_file.size.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.data_file.size.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.data_file.size.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.space.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.space.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"oracle.tablespace.space.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"process.pgid\":{\"id\":\"string\",\"params\":{}},\"process.pid\":{\"id\":\"string\",\"params\":{}},\"process.ppid\":{\"id\":\"string\",\"params\":{}},\"process.thread.id\":{\"id\":\"string\",\"params\":{}},\"rabbitmq.connection.frame_max\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.disk.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.disk.free.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.gc.reclaimed.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.io.read.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.io.write.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.node.mem.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"rabbitmq.queue.consumers.utilisation.pct\":{\"id\":\"percent\",\"params\":{}},\"rabbitmq.queue.memory.bytes\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.allocator_stats.active\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.allocator_stats.allocated\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.allocator_stats.fragmentation.bytes\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.allocator_stats.resident\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.allocator_stats.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.fragmentation.bytes\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.max.value\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.used.dataset\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.used.lua\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.used.peak\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.used.rss\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.memory.used.value\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.aof.buffer.size\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.aof.copy_on_write.last_size\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.aof.rewrite.buffer.size\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.aof.rewrite.current_time.sec\":{\"id\":\"duration\",\"params\":{}},\"redis.info.persistence.aof.rewrite.last_time.sec\":{\"id\":\"duration\",\"params\":{}},\"redis.info.persistence.aof.size.base\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.aof.size.current\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.persistence.rdb.bgsave.current_time.sec\":{\"id\":\"duration\",\"params\":{}},\"redis.info.persistence.rdb.bgsave.last_time.sec\":{\"id\":\"duration\",\"params\":{}},\"redis.info.persistence.rdb.copy_on_write.last_size\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.replication.backlog.size\":{\"id\":\"bytes\",\"params\":{}},\"redis.info.replication.master.last_io_seconds_ago\":{\"id\":\"duration\",\"params\":{}},\"redis.info.replication.master.sync.last_io_seconds_ago\":{\"id\":\"duration\",\"params\":{}},\"redis.info.replication.master.sync.left_bytes\":{\"id\":\"bytes\",\"params\":{}},\"server.bytes\":{\"id\":\"bytes\",\"params\":{}},\"server.nat.port\":{\"id\":\"string\",\"params\":{}},\"server.port\":{\"id\":\"string\",\"params\":{}},\"source.bytes\":{\"id\":\"bytes\",\"params\":{}},\"source.nat.port\":{\"id\":\"string\",\"params\":{}},\"source.port\":{\"id\":\"string\",\"params\":{}},\"system.core.idle.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.iowait.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.irq.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.nice.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.softirq.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.steal.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.system.pct\":{\"id\":\"percent\",\"params\":{}},\"system.core.user.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.idle.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.idle.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.iowait.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.iowait.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.irq.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.irq.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.nice.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.nice.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.softirq.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.softirq.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.steal.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.steal.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.system.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.system.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.total.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.total.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.user.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.cpu.user.pct\":{\"id\":\"percent\",\"params\":{}},\"system.diskio.iostat.read.per_sec.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.diskio.iostat.write.per_sec.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.diskio.read.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.diskio.write.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.entropy.pct\":{\"id\":\"percent\",\"params\":{}},\"system.filesystem.available\":{\"id\":\"bytes\",\"params\":{}},\"system.filesystem.free\":{\"id\":\"bytes\",\"params\":{}},\"system.filesystem.total\":{\"id\":\"bytes\",\"params\":{}},\"system.filesystem.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.filesystem.used.pct\":{\"id\":\"percent\",\"params\":{}},\"system.fsstat.total_size.free\":{\"id\":\"bytes\",\"params\":{}},\"system.fsstat.total_size.total\":{\"id\":\"bytes\",\"params\":{}},\"system.fsstat.total_size.used\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.actual.free\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.actual.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.actual.used.pct\":{\"id\":\"percent\",\"params\":{}},\"system.memory.free\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.hugepages.default_size\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.hugepages.free\":{\"id\":\"number\",\"params\":{}},\"system.memory.hugepages.reserved\":{\"id\":\"number\",\"params\":{}},\"system.memory.hugepages.surplus\":{\"id\":\"number\",\"params\":{}},\"system.memory.hugepages.total\":{\"id\":\"number\",\"params\":{}},\"system.memory.hugepages.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.hugepages.used.pct\":{\"id\":\"percent\",\"params\":{}},\"system.memory.swap.free\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.swap.total\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.swap.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.swap.used.pct\":{\"id\":\"percent\",\"params\":{}},\"system.memory.total\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.memory.used.pct\":{\"id\":\"percent\",\"params\":{}},\"system.network.in.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.network.out.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.blkio.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem.usage.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem_tcp.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem_tcp.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.kmem_tcp.usage.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.mem.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.mem.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.mem.usage.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.memsw.limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.memsw.usage.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.memsw.usage.max.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.active_anon.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.active_file.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.cache.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.hierarchical_memory_limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.hierarchical_memsw_limit.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.inactive_anon.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.inactive_file.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.mapped_file.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.rss_huge.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.swap.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cgroup.memory.stats.unevictable.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.cpu.total.norm.pct\":{\"id\":\"percent\",\"params\":{}},\"system.process.cpu.total.pct\":{\"id\":\"percent\",\"params\":{}},\"system.process.memory.rss.bytes\":{\"id\":\"bytes\",\"params\":{}},\"system.process.memory.rss.pct\":{\"id\":\"percent\",\"params\":{}},\"system.process.memory.share\":{\"id\":\"bytes\",\"params\":{}},\"system.process.memory.size\":{\"id\":\"bytes\",\"params\":{}},\"system.socket.summary.tcp.memory\":{\"id\":\"bytes\",\"params\":{}},\"system.socket.summary.udp.memory\":{\"id\":\"bytes\",\"params\":{}},\"system.uptime.duration.ms\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"milliseconds\"}},\"url.port\":{\"id\":\"string\",\"params\":{}},\"vsphere.datastore.capacity.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.datastore.capacity.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.datastore.capacity.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.datastore.capacity.used.pct\":{\"id\":\"percent\",\"params\":{}},\"vsphere.host.memory.free.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.host.memory.total.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.host.memory.used.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.virtualmachine.memory.free.guest.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.virtualmachine.memory.total.guest.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.virtualmachine.memory.used.guest.bytes\":{\"id\":\"bytes\",\"params\":{}},\"vsphere.virtualmachine.memory.used.host.bytes\":{\"id\":\"bytes\",\"params\":{}},\"windows.service.uptime.ms\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"milliseconds\"}}}", - "timeFieldName": "@timestamp", - "title": "metricbeat-*" - }, - "migrationVersion": { - "index-pattern": "7.6.0" - }, - "type": "index-pattern", - "updated_at": "2020-01-22T15:34:59.061Z" - } - } -} - -{ - "type": "doc", - "value": { + "index": ".kibana", + "type": "doc", "id": "index-pattern:logstash-*", "index": ".kibana_1", "source": { @@ -297,4 +279,4 @@ "updated_at": "2019-07-17T17:54:26.378Z" } } -} \ No newline at end of file +} diff --git a/x-pack/test/reporting_api_integration/reporting_without_security.config.ts b/x-pack/test/reporting_api_integration/reporting_without_security.config.ts index 5946a502a4ce37..59d6074d9d8caf 100644 --- a/x-pack/test/reporting_api_integration/reporting_without_security.config.ts +++ b/x-pack/test/reporting_api_integration/reporting_without_security.config.ts @@ -33,7 +33,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { kbnTestServer: { ...apiConfig.get('kbnTestServer'), serverArgs: [ - `--migrations.enableV2=false`, `--elasticsearch.hosts=${formatUrl(esTestConfig.getUrlParts())}`, `--logging.json=false`, `--server.maxPayloadBytes=1679958`, From 8769c8d3ba0b9daf244b67c742b545eab73146d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Sun, 7 Feb 2021 12:04:33 +0100 Subject: [PATCH 12/51] Bump immer dependencies (#90267) * Bump immer dependencies * Update processed_search_response.ts * Update processed_search_response.ts Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 2 +- .../fixtures/processed_search_response.ts | 20 +++++++++++-------- yarn.lock | 7 ++++++- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 4afe7a579ad458..a5c6fa6f7b3c2e 100644 --- a/package.json +++ b/package.json @@ -220,7 +220,7 @@ "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^5.0.0", "idx": "^2.5.6", - "immer": "^1.5.0", + "immer": "^8.0.1", "inline-style": "^2.0.0", "intl": "^1.2.5", "intl-format-cache": "^2.1.0", diff --git a/x-pack/plugins/searchprofiler/public/application/components/profile_tree/__jest__/fixtures/processed_search_response.ts b/x-pack/plugins/searchprofiler/public/application/components/profile_tree/__jest__/fixtures/processed_search_response.ts index 2d8e812408efe2..c2a9bfa6506e59 100644 --- a/x-pack/plugins/searchprofiler/public/application/components/profile_tree/__jest__/fixtures/processed_search_response.ts +++ b/x-pack/plugins/searchprofiler/public/application/components/profile_tree/__jest__/fixtures/processed_search_response.ts @@ -6,6 +6,7 @@ */ import { produce } from 'immer'; +import { Index } from '../../../../types'; const shard1 = { id: ['L22w_FX2SbqlQYOP5QrYDg', '.kibana_1', '0'], @@ -336,11 +337,14 @@ const search1Child = { (searchRoot.treeRoot as any) = search1; (shard1.searches[0] as any) = searchRoot; -export const processedResponseWithFirstShard = produce(null, () => [ - { - shards: [shard1], - time: 0.058419, - name: '.kibana_1', - visible: false, - }, -]); +export const processedResponseWithFirstShard = produce( + [ + { + shards: [shard1], + time: 0.058419, + name: '.kibana_1', + visible: false, + }, + ], + () => undefined +); diff --git a/yarn.lock b/yarn.lock index e4f5fc2e1a8e16..fa7ebacb1cd708 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17000,11 +17000,16 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= -immer@1.10.0, immer@^1.5.0: +immer@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg== +immer@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" + integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA== + import-cwd@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" From 44eff7184eb06dcbf990962ac7690bf438a9986a Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Sun, 7 Feb 2021 16:04:53 +0000 Subject: [PATCH 13/51] skip flaky suite (#90552) --- .../api_integration/tagging_api/apis/delete.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/delete.ts b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/delete.ts index 415cdf4814176c..ed4bc8f4f8c7b7 100644 --- a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/delete.ts +++ b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/delete.ts @@ -13,7 +13,8 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); - describe('DELETE /api/saved_objects_tagging/tags/{id}', () => { + // FLAKY: https://github.com/elastic/kibana/issues/90552 + describe.skip('DELETE /api/saved_objects_tagging/tags/{id}', () => { beforeEach(async () => { await esArchiver.load('delete_with_references'); }); From 91ffe7373a66d5ce512a8b2497bcd02db21cf540 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Sun, 7 Feb 2021 18:57:50 -0500 Subject: [PATCH 14/51] [Fleet] Support Fleet server system indices (#89372) --- .../fleet/common/types/models/agent.ts | 164 +++++++++++++++++- .../server/routes/agent/acks_handlers.ts | 2 +- .../server/routes/agent/actions_handlers.ts | 2 +- .../fleet/server/routes/agent/handlers.ts | 39 +++-- .../server/routes/agent/upgrade_handler.ts | 20 +-- .../fleet/server/services/agent_policy.ts | 16 +- .../fleet/server/services/agents/acks.test.ts | 40 ++--- .../fleet/server/services/agents/acks.ts | 37 ++-- .../server/services/agents/actions.test.ts | 6 +- .../fleet/server/services/agents/actions.ts | 70 +++++++- .../services/agents/authenticate.test.ts | 14 +- .../server/services/agents/authenticate.ts | 5 +- .../server/services/agents/checkin/index.ts | 14 +- .../agents/checkin/state_connected_agents.ts | 18 +- .../agents/checkin/state_new_actions.test.ts | 19 +- .../agents/checkin/state_new_actions.ts | 44 +++-- .../fleet/server/services/agents/crud.ts | 72 ++++---- .../services/agents/crud_fleet_server.ts | 134 ++++++++++---- .../fleet/server/services/agents/crud_so.ts | 36 +++- .../fleet/server/services/agents/enroll.ts | 35 +++- .../fleet/server/services/agents/helpers.ts | 23 ++- .../server/services/agents/reassign.test.ts | 3 + .../fleet/server/services/agents/reassign.ts | 33 ++-- .../fleet/server/services/agents/status.ts | 8 +- .../server/services/agents/unenroll.test.ts | 4 + .../fleet/server/services/agents/unenroll.ts | 46 +++-- .../fleet/server/services/agents/upgrade.ts | 33 ++-- .../services/api_keys/enrollment_api_key.ts | 12 ++ .../enrollment_api_key_fleet_server.ts | 56 +++--- .../api_keys/enrollment_api_key_so.ts | 21 ++- .../fleet/server/services/api_keys/index.ts | 39 +---- .../server/services/fleet_server_migration.ts | 147 ++++++++++++++-- x-pack/plugins/fleet/server/services/index.ts | 1 + x-pack/plugins/fleet/server/services/setup.ts | 10 ++ x-pack/plugins/fleet/server/types/index.tsx | 3 + .../artifacts/download_exception_list.test.ts | 15 ++ .../artifacts/download_exception_list.ts | 6 +- 37 files changed, 882 insertions(+), 365 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts index 57c42d887bc83c..2e18d427272ce5 100644 --- a/x-pack/plugins/fleet/common/types/models/agent.ts +++ b/x-pack/plugins/fleet/common/types/models/agent.ts @@ -130,8 +130,8 @@ interface AgentBase { enrolled_at: string; unenrolled_at?: string; unenrollment_started_at?: string; - upgraded_at?: string; - upgrade_started_at?: string; + upgraded_at?: string | null; + upgrade_started_at?: string | null; access_api_key_id?: string; default_api_key?: string; default_api_key_id?: string; @@ -155,3 +155,163 @@ export interface AgentSOAttributes extends AgentBase { current_error_events?: string; packages?: string[]; } + +// Generated from FleetServer schema.json + +/** + * An Elastic Agent that has enrolled into Fleet + */ +export interface FleetServerAgent { + /** + * The version of the document in the index + */ + _version?: number; + /** + * Shared ID + */ + shared_id?: string; + /** + * Type + */ + type: AgentType; + /** + * Active flag + */ + active: boolean; + /** + * Date/time the Elastic Agent enrolled + */ + enrolled_at: string; + /** + * Date/time the Elastic Agent unenrolled + */ + unenrolled_at?: string; + /** + * Date/time the Elastic Agent unenrolled started + */ + unenrollment_started_at?: string; + /** + * Date/time the Elastic Agent was last upgraded + */ + upgraded_at?: string | null; + /** + * Date/time the Elastic Agent started the current upgrade + */ + upgrade_started_at?: string | null; + /** + * ID of the API key the Elastic Agent must used to contact Fleet Server + */ + access_api_key_id?: string; + agent?: FleetServerAgentMetadata; + /** + * User provided metadata information for the Elastic Agent + */ + user_provided_metadata: AgentMetadata; + /** + * Local metadata information for the Elastic Agent + */ + local_metadata: AgentMetadata; + /** + * The policy ID for the Elastic Agent + */ + policy_id?: string; + /** + * The current policy revision_idx for the Elastic Agent + */ + policy_revision_idx?: number | null; + /** + * The current policy coordinator for the Elastic Agent + */ + policy_coordinator_idx?: number; + /** + * Date/time the Elastic Agent was last updated + */ + last_updated?: string; + /** + * Date/time the Elastic Agent checked in last time + */ + last_checkin?: string; + /** + * Lst checkin status + */ + last_checkin_status?: 'error' | 'online' | 'degraded' | 'updating'; + /** + * ID of the API key the Elastic Agent uses to authenticate with elasticsearch + */ + default_api_key_id?: string; + /** + * API key the Elastic Agent uses to authenticate with elasticsearch + */ + default_api_key?: string; + /** + * Date/time the Elastic Agent was last updated + */ + updated_at?: string; + /** + * Packages array + */ + packages?: string[]; + /** + * The last acknowledged action sequence number for the Elastic Agent + */ + action_seq_no?: number; +} +/** + * An Elastic Agent metadata + */ +export interface FleetServerAgentMetadata { + /** + * The unique identifier for the Elastic Agent + */ + id: string; + /** + * The version of the Elastic Agent + */ + version: string; + [k: string]: any; +} + +/** + * An Elastic Agent action + */ +export interface FleetServerAgentAction { + /** + * The unique identifier for action document + */ + _id?: string; + /** + * The action sequence number + */ + _seq_no?: number; + /** + * The unique identifier for the Elastic Agent action. There could be multiple documents with the same action_id if the action is split into two separate documents. + */ + action_id?: string; + /** + * Date/time the action was created + */ + '@timestamp'?: string; + /** + * The action expiration date/time + */ + expiration?: string; + /** + * The action type. APP_ACTION is the value for the actions that suppose to be routed to the endpoints/beats. + */ + type?: string; + /** + * The input identifier the actions should be routed to. + */ + input_id?: string; + /** + * The Agent IDs the action is intended for. No support for json.RawMessage with the current generator. Could be useful to lazy parse the agent ids + */ + agents?: string[]; + /** + * The opaque payload. + */ + data?: { + [k: string]: unknown; + }; + [k: string]: unknown; +} diff --git a/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts b/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts index 2d7c884edad83c..22b5035378a20d 100644 --- a/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts @@ -20,7 +20,7 @@ export const postAgentAcksHandlerBuilder = function ( try { const soClient = ackService.getSavedObjectsClientContract(request); const esClient = ackService.getElasticsearchClientContract(); - const agent = await ackService.authenticateAgentWithAccessToken(soClient, request); + const agent = await ackService.authenticateAgentWithAccessToken(soClient, esClient, request); const agentEvents = request.body.events as AgentEvent[]; // validate that all events are for the authorized agent obtained from the api key diff --git a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts index bf0cfd2d476dd8..d032945245faf0 100644 --- a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts @@ -30,7 +30,7 @@ export const postNewAgentActionHandlerBuilder = function ( const newAgentAction = request.body.action; - const savedAgentAction = await actionsService.createAgentAction(soClient, { + const savedAgentAction = await actionsService.createAgentAction(soClient, esClient, { created_at: new Date().toISOString(), ...newAgentAction, agent_id: agent.id, diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index 411da6da0223c6..cd91e8c325c066 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -132,8 +132,8 @@ export const updateAgentHandler: RequestHandler< const esClient = context.core.elasticsearch.client.asCurrentUser; try { - await AgentService.updateAgent(soClient, request.params.agentId, { - userProvidedMetatada: request.body.user_provided_metadata, + await AgentService.updateAgent(soClient, esClient, request.params.agentId, { + user_provided_metadata: request.body.user_provided_metadata, }); const agent = await AgentService.getAgent(soClient, esClient, request.params.agentId); @@ -164,12 +164,13 @@ export const postAgentCheckinHandler: RequestHandler< try { const soClient = appContextService.getInternalUserSOClient(request); const esClient = appContextService.getInternalUserESClient(); - const agent = await AgentService.authenticateAgentWithAccessToken(soClient, request); + const agent = await AgentService.authenticateAgentWithAccessToken(soClient, esClient, request); const abortController = new AbortController(); request.events.aborted$.subscribe(() => { abortController.abort(); }); const signal = abortController.signal; + const { actions } = await AgentService.agentCheckin( soClient, esClient, @@ -205,8 +206,13 @@ export const postAgentEnrollHandler: RequestHandler< > = async (context, request, response) => { try { const soClient = appContextService.getInternalUserSOClient(request); + const esClient = context.core.elasticsearch.client.asInternalUser; const { apiKeyId } = APIKeyService.parseApiKeyFromHeaders(request.headers); - const enrollmentAPIKey = await APIKeyService.getEnrollmentAPIKeyById(soClient, apiKeyId); + const enrollmentAPIKey = await APIKeyService.getEnrollmentAPIKeyById( + soClient, + esClient, + apiKeyId + ); if (!enrollmentAPIKey || !enrollmentAPIKey.active) { return response.unauthorized({ @@ -311,21 +317,16 @@ export const postBulkAgentsReassignHandler: RequestHandler< const soClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asInternalUser; try { - // Reassign by array of IDs - const result = Array.isArray(request.body.agents) - ? await AgentService.reassignAgents( - soClient, - esClient, - { agentIds: request.body.agents }, - request.body.policy_id - ) - : await AgentService.reassignAgents( - soClient, - esClient, - { kuery: request.body.agents }, - request.body.policy_id - ); - const body: PostBulkAgentReassignResponse = result.saved_objects.reduce((acc, so) => { + const results = await AgentService.reassignAgents( + soClient, + esClient, + Array.isArray(request.body.agents) + ? { agentIds: request.body.agents } + : { kuery: request.body.agents }, + request.body.policy_id + ); + + const body: PostBulkAgentReassignResponse = results.items.reduce((acc, so) => { return { ...acc, [so.id]: { diff --git a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts index 0215b8f27b3932..086a9411f20b8d 100644 --- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts @@ -8,18 +8,13 @@ import { RequestHandler } from 'src/core/server'; import { TypeOf } from '@kbn/config-schema'; import semverCoerce from 'semver/functions/coerce'; -import { - AgentSOAttributes, - PostAgentUpgradeResponse, - PostBulkAgentUpgradeResponse, -} from '../../../common/types'; +import { PostAgentUpgradeResponse, PostBulkAgentUpgradeResponse } from '../../../common/types'; import { PostAgentUpgradeRequestSchema, PostBulkAgentUpgradeRequestSchema } from '../../types'; import * as AgentService from '../../services/agents'; import { appContextService } from '../../services'; import { defaultIngestErrorHandler } from '../../errors'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; -import { savedObjectToAgent } from '../../services/agents/saved_objects'; import { isAgentUpgradeable } from '../../../common/services'; +import { getAgent } from '../../services/agents'; export const postAgentUpgradeHandler: RequestHandler< TypeOf, @@ -27,6 +22,7 @@ export const postAgentUpgradeHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asInternalUser; const { version, source_uri: sourceUri, force } = request.body; const kibanaVersion = appContextService.getKibanaVersion(); try { @@ -39,12 +35,8 @@ export const postAgentUpgradeHandler: RequestHandler< }, }); } - - const agentSO = await soClient.get( - AGENT_SAVED_OBJECT_TYPE, - request.params.agentId - ); - if (agentSO.attributes.unenrollment_started_at || agentSO.attributes.unenrolled_at) { + const agent = await getAgent(soClient, esClient, request.params.agentId); + if (agent.unenrollment_started_at || agent.unenrolled_at) { return response.customError({ statusCode: 400, body: { @@ -53,7 +45,6 @@ export const postAgentUpgradeHandler: RequestHandler< }); } - const agent = savedObjectToAgent(agentSO); if (!force && !isAgentUpgradeable(agent, kibanaVersion)) { return response.customError({ statusCode: 400, @@ -66,6 +57,7 @@ export const postAgentUpgradeHandler: RequestHandler< try { await AgentService.sendUpgradeAgentAction({ soClient, + esClient, agentId: request.params.agentId, version, sourceUri, diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index dfe5c19bc417b5..ca131efeff68cc 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -488,17 +488,17 @@ class AgentPolicyService { soClient: SavedObjectsClientContract, agentPolicyId: string ) { - return appContextService.getConfig()?.agents.fleetServerEnabled - ? this.createFleetPolicyChangeFleetServer( - soClient, - appContextService.getInternalUserESClient(), - agentPolicyId - ) - : this.createFleetPolicyChangeActionSO(soClient, agentPolicyId); + const esClient = appContextService.getInternalUserESClient(); + if (appContextService.getConfig()?.agents?.fleetServerEnabled) { + await this.createFleetPolicyChangeFleetServer(soClient, esClient, agentPolicyId); + } + + return this.createFleetPolicyChangeActionSO(soClient, esClient, agentPolicyId); } public async createFleetPolicyChangeActionSO( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentPolicyId: string ) { // If Agents is not setup skip the creation of POLICY_CHANGE agent actions @@ -518,7 +518,7 @@ class AgentPolicyService { return acc; }, []); - await createAgentPolicyAction(soClient, { + await createAgentPolicyAction(soClient, esClient, { type: 'POLICY_CHANGE', data: { policy }, ack_data: { packages }, diff --git a/x-pack/plugins/fleet/server/services/agents/acks.test.ts b/x-pack/plugins/fleet/server/services/agents/acks.test.ts index c1a6067195c979..5aec696f5e1440 100644 --- a/x-pack/plugins/fleet/server/services/agents/acks.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/acks.test.ts @@ -106,19 +106,19 @@ describe('test agent acks services', () => { } as AgentEvent, ] ); - expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(1); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0][0]).toMatchInlineSnapshot(` - Object { - "attributes": Object { + expect(mockSavedObjectsClient.bulkUpdate).not.toBeCalled(); + expect(mockSavedObjectsClient.update).toBeCalled(); + expect(mockSavedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "fleet-agents", + "id", + Object { "packages": Array [ "system", ], "policy_revision": 4, }, - "id": "id", - "type": "fleet-agents", - } + ] `); }); @@ -168,19 +168,19 @@ describe('test agent acks services', () => { } as AgentEvent, ] ); - expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(1); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0][0]).toMatchInlineSnapshot(` - Object { - "attributes": Object { + expect(mockSavedObjectsClient.bulkUpdate).not.toBeCalled(); + expect(mockSavedObjectsClient.update).toBeCalled(); + expect(mockSavedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "fleet-agents", + "id", + Object { "packages": Array [ "system", ], "policy_revision": 4, }, - "id": "id", - "type": "fleet-agents", - } + ] `); }); @@ -230,8 +230,8 @@ describe('test agent acks services', () => { } as AgentEvent, ] ); - expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(0); + expect(mockSavedObjectsClient.bulkUpdate).not.toBeCalled(); + expect(mockSavedObjectsClient.update).not.toBeCalled(); }); it('should not update config field on the agent if a policy change for an old revision is acknowledged', async () => { @@ -277,8 +277,8 @@ describe('test agent acks services', () => { } as AgentEvent, ] ); - expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(0); + expect(mockSavedObjectsClient.bulkUpdate).not.toBeCalled(); + expect(mockSavedObjectsClient.update).not.toBeCalled(); }); it('should fail for actions that cannot be found on agent actions list', async () => { diff --git a/x-pack/plugins/fleet/server/services/agents/acks.ts b/x-pack/plugins/fleet/server/services/agents/acks.ts index a09107b90a0156..c639a9b0332ac6 100644 --- a/x-pack/plugins/fleet/server/services/agents/acks.ts +++ b/x-pack/plugins/fleet/server/services/agents/acks.ts @@ -24,14 +24,11 @@ import { AgentSOAttributes, AgentActionSOAttributes, } from '../../types'; -import { - AGENT_EVENT_SAVED_OBJECT_TYPE, - AGENT_SAVED_OBJECT_TYPE, - AGENT_ACTION_SAVED_OBJECT_TYPE, -} from '../../constants'; +import { AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../constants'; import { getAgentActionByIds } from './actions'; import { forceUnenrollAgent } from './unenroll'; import { ackAgentUpgraded } from './upgrade'; +import { updateAgent } from './crud'; const ALLOWED_ACKNOWLEDGEMENT_TYPE: string[] = ['ACTION_RESULT']; @@ -87,26 +84,23 @@ export async function acknowledgeAgentActions( const upgradeAction = actions.find((action) => action.type === 'UPGRADE'); if (upgradeAction) { - await ackAgentUpgraded(soClient, upgradeAction); + await ackAgentUpgraded(soClient, esClient, upgradeAction); } const configChangeAction = getLatestConfigChangePolicyActionIfUpdated(agent, actions); - await soClient.bulkUpdate([ - ...(configChangeAction - ? [ - { - type: AGENT_SAVED_OBJECT_TYPE, - id: agent.id, - attributes: { - policy_revision: configChangeAction.policy_revision, - packages: configChangeAction?.ack_data?.packages, - }, - }, - ] - : []), - ...buildUpdateAgentActionSentAt(agentActionsIds), - ]); + if (configChangeAction) { + await updateAgent(soClient, esClient, agent.id, { + policy_revision: configChangeAction.policy_revision, + packages: configChangeAction?.ack_data?.packages, + }); + } + + if (agentActionsIds.length > 0) { + await soClient.bulkUpdate([ + ...buildUpdateAgentActionSentAt(agentActionsIds), + ]); + } return actions; } @@ -206,6 +200,7 @@ export interface AcksService { authenticateAgentWithAccessToken: ( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, request: KibanaRequest ) => Promise; diff --git a/x-pack/plugins/fleet/server/services/agents/actions.test.ts b/x-pack/plugins/fleet/server/services/agents/actions.test.ts index 5b3c2ea5ce7088..3d391cc89a7e31 100644 --- a/x-pack/plugins/fleet/server/services/agents/actions.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/actions.test.ts @@ -8,12 +8,12 @@ import { createAgentAction } from './actions'; import { SavedObject } from 'kibana/server'; import { AgentAction } from '../../../common/types/models'; -import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { savedObjectsClientMock, elasticsearchServiceMock } from 'src/core/server/mocks'; describe('test agent actions services', () => { it('should create a new action', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); - + const mockedEsClient = elasticsearchServiceMock.createInternalClient(); const newAgentAction: Omit = { agent_id: 'agentid', type: 'POLICY_CHANGE', @@ -32,7 +32,7 @@ describe('test agent actions services', () => { }, } as SavedObject) ); - await createAgentAction(mockSavedObjectsClient, newAgentAction); + await createAgentAction(mockSavedObjectsClient, mockedEsClient, newAgentAction); const createdAction = (mockSavedObjectsClient.create.mock .calls[0][1] as unknown) as AgentAction; diff --git a/x-pack/plugins/fleet/server/services/agents/actions.ts b/x-pack/plugins/fleet/server/services/agents/actions.ts index b45b7836eb46f9..8dfeac11dacf3b 100644 --- a/x-pack/plugins/fleet/server/services/agents/actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/actions.ts @@ -13,8 +13,9 @@ import { BaseAgentActionSOAttributes, AgentActionSOAttributes, AgentPolicyActionSOAttributes, + FleetServerAgentAction, } from '../../../common/types/models'; -import { AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../../common/constants'; +import { AGENT_ACTION_SAVED_OBJECT_TYPE, AGENT_ACTIONS_INDEX } from '../../../common/constants'; import { isAgentActionSavedObject, isPolicyActionSavedObject, @@ -23,37 +24,45 @@ import { import { appContextService } from '../app_context'; import { nodeTypes } from '../../../../../../src/plugins/data/common'; +const ONE_MONTH_IN_MS = 2592000000; + export async function createAgentAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit ): Promise { - return createAction(soClient, newAgentAction); + return createAction(soClient, esClient, newAgentAction); } export async function bulkCreateAgentActions( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentActions: Array> ): Promise { - return bulkCreateActions(soClient, newAgentActions); + return bulkCreateActions(soClient, esClient, newAgentActions); } export function createAgentPolicyAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit ): Promise { - return createAction(soClient, newAgentAction); + return createAction(soClient, esClient, newAgentAction); } async function createAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit ): Promise; async function createAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit ): Promise; async function createAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit | Omit ): Promise { const actionSO = await soClient.create( @@ -65,6 +74,27 @@ async function createAction( } ); + if ( + appContextService.getConfig()?.agents?.fleetServerEnabled && + isAgentActionSavedObject(actionSO) + ) { + const body: FleetServerAgentAction = { + '@timestamp': new Date().toISOString(), + expiration: new Date(Date.now() + ONE_MONTH_IN_MS).toISOString(), + agents: [actionSO.attributes.agent_id], + action_id: actionSO.id, + data: newAgentAction.data, + type: newAgentAction.type, + }; + + await esClient.create({ + index: AGENT_ACTIONS_INDEX, + id: actionSO.id, + body, + refresh: 'wait_for', + }); + } + if (isAgentActionSavedObject(actionSO)) { const agentAction = savedObjectToAgentAction(actionSO); // Action `data` is encrypted, so is not returned from the saved object @@ -84,14 +114,17 @@ async function createAction( async function bulkCreateActions( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentActions: Array> ): Promise; async function bulkCreateActions( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentActions: Array> ): Promise; async function bulkCreateActions( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentActions: Array | Omit> ): Promise> { const { saved_objects: actionSOs } = await soClient.bulkCreate( @@ -105,6 +138,34 @@ async function bulkCreateActions( })) ); + if (appContextService.getConfig()?.agents?.fleetServerEnabled) { + await esClient.bulk({ + index: AGENT_ACTIONS_INDEX, + body: actionSOs.flatMap((actionSO) => { + if (!isAgentActionSavedObject(actionSO)) { + return []; + } + const body: FleetServerAgentAction = { + '@timestamp': new Date().toISOString(), + expiration: new Date(Date.now() + ONE_MONTH_IN_MS).toISOString(), + agents: [actionSO.attributes.agent_id], + action_id: actionSO.id, + data: actionSO.attributes.data ? JSON.parse(actionSO.attributes.data) : undefined, + type: actionSO.type, + }; + + return [ + { + create: { + _id: actionSO.id, + }, + }, + body, + ]; + }), + }); + } + return actionSOs.map((actionSO) => { if (isAgentActionSavedObject(actionSO)) { const agentAction = savedObjectToAgentAction(actionSO); @@ -316,6 +377,7 @@ export interface ActionsService { createAgentAction: ( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, newAgentAction: Omit ) => Promise; } diff --git a/x-pack/plugins/fleet/server/services/agents/authenticate.test.ts b/x-pack/plugins/fleet/server/services/agents/authenticate.test.ts index c59e2decebd992..5a1e86c15c0024 100644 --- a/x-pack/plugins/fleet/server/services/agents/authenticate.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/authenticate.test.ts @@ -6,10 +6,12 @@ */ import { KibanaRequest } from 'kibana/server'; -import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { savedObjectsClientMock, elasticsearchServiceMock } from 'src/core/server/mocks'; import { authenticateAgentWithAccessToken } from './authenticate'; +const mockEsClient = elasticsearchServiceMock.createInternalClient(); + describe('test agent autenticate services', () => { it('should succeed with a valid API key and an active agent', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); @@ -32,7 +34,7 @@ describe('test agent autenticate services', () => { ], }) ); - await authenticateAgentWithAccessToken(mockSavedObjectsClient, { + await authenticateAgentWithAccessToken(mockSavedObjectsClient, mockEsClient, { auth: { isAuthenticated: true }, headers: { authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', @@ -62,7 +64,7 @@ describe('test agent autenticate services', () => { }) ); expect( - authenticateAgentWithAccessToken(mockSavedObjectsClient, { + authenticateAgentWithAccessToken(mockSavedObjectsClient, mockEsClient, { auth: { isAuthenticated: false }, headers: { authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', @@ -93,7 +95,7 @@ describe('test agent autenticate services', () => { }) ); expect( - authenticateAgentWithAccessToken(mockSavedObjectsClient, { + authenticateAgentWithAccessToken(mockSavedObjectsClient, mockEsClient, { auth: { isAuthenticated: true }, headers: { authorization: 'aaaa', @@ -124,7 +126,7 @@ describe('test agent autenticate services', () => { }) ); expect( - authenticateAgentWithAccessToken(mockSavedObjectsClient, { + authenticateAgentWithAccessToken(mockSavedObjectsClient, mockEsClient, { auth: { isAuthenticated: true }, headers: { authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', @@ -144,7 +146,7 @@ describe('test agent autenticate services', () => { }) ); expect( - authenticateAgentWithAccessToken(mockSavedObjectsClient, { + authenticateAgentWithAccessToken(mockSavedObjectsClient, mockEsClient, { auth: { isAuthenticated: true }, headers: { authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', diff --git a/x-pack/plugins/fleet/server/services/agents/authenticate.ts b/x-pack/plugins/fleet/server/services/agents/authenticate.ts index a773173b1ddc10..a03c35bdc6e737 100644 --- a/x-pack/plugins/fleet/server/services/agents/authenticate.ts +++ b/x-pack/plugins/fleet/server/services/agents/authenticate.ts @@ -6,13 +6,14 @@ */ import Boom from '@hapi/boom'; -import { KibanaRequest, SavedObjectsClientContract } from 'src/core/server'; +import { KibanaRequest, SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; import { Agent } from '../../types'; import * as APIKeyService from '../api_keys'; import { getAgentByAccessAPIKeyId } from './crud'; export async function authenticateAgentWithAccessToken( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, request: KibanaRequest ): Promise { if (!request.auth.isAuthenticated) { @@ -25,7 +26,7 @@ export async function authenticateAgentWithAccessToken( throw Boom.unauthorized(err.message); } - const agent = await getAgentByAccessAPIKeyId(soClient, res.apiKeyId); + const agent = await getAgentByAccessAPIKeyId(soClient, esClient, res.apiKeyId); return agent; } diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/index.ts b/x-pack/plugins/fleet/server/services/agents/checkin/index.ts index 9a60abdc69423d..bcebedae2e07a1 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/index.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/index.ts @@ -19,9 +19,10 @@ import { AgentEventSOAttributes, } from '../../../types'; -import { AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../../constants'; +import { AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../../constants'; import { agentCheckinState } from './state'; import { getAgentActionsForCheckin } from '../actions'; +import { updateAgent } from '../crud'; export async function agentCheckin( soClient: SavedObjectsClientContract, @@ -35,13 +36,7 @@ export async function agentCheckin( options?: { signal: AbortSignal } ) { const updateData: Partial = {}; - const { updatedErrorEvents } = await processEventsForCheckin(soClient, agent, data.events); - if ( - updatedErrorEvents && - !(updatedErrorEvents.length === 0 && agent.current_error_events.length === 0) - ) { - updateData.current_error_events = JSON.stringify(updatedErrorEvents); - } + await processEventsForCheckin(soClient, agent, data.events); if (data.localMetadata && !deepEqual(data.localMetadata, agent.local_metadata)) { updateData.local_metadata = data.localMetadata; } @@ -50,9 +45,8 @@ export async function agentCheckin( } // Update agent only if something changed if (Object.keys(updateData).length > 0) { - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agent.id, updateData); + await updateAgent(soClient, esClient, agent.id, updateData); } - // Check if some actions are not acknowledged let actions = await getAgentActionsForCheckin(soClient, agent.id); if (actions.length > 0) { diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/state_connected_agents.ts b/x-pack/plugins/fleet/server/services/agents/checkin/state_connected_agents.ts index 83fd139a1e8e82..6156212a632032 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/state_connected_agents.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/state_connected_agents.ts @@ -5,10 +5,9 @@ * 2.0. */ -import { KibanaRequest, SavedObjectsBulkUpdateObject } from 'src/core/server'; +import { KibanaRequest } from 'src/core/server'; import { appContextService } from '../../app_context'; -import { AgentSOAttributes } from '../../../types'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; +import { bulkUpdateAgents } from '../crud'; function getInternalUserSOClient() { const fakeRequest = ({ @@ -57,20 +56,17 @@ export function agentCheckinStateConnectedAgentsFactory() { if (agentToUpdate.size === 0) { return; } + const esClient = appContextService.getInternalUserESClient(); const internalSOClient = getInternalUserSOClient(); const now = new Date().toISOString(); - const updates: Array> = [ - ...agentToUpdate.values(), - ].map((agentId) => ({ - type: AGENT_SAVED_OBJECT_TYPE, - id: agentId, - attributes: { + const updates = [...agentToUpdate.values()].map((agentId) => ({ + agentId, + data: { last_checkin: now, }, })); - agentToUpdate = new Set([...connectedAgentsIds.values()]); - await internalSOClient.bulkUpdate(updates, { refresh: false }); + await bulkUpdateAgents(internalSOClient, esClient, updates); } return { diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.test.ts b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.test.ts index ca8378c117b7df..cd6e0ef61e3f08 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import { savedObjectsClientMock } from 'src/core/server/mocks'; import { take } from 'rxjs/operators'; import { @@ -17,6 +18,7 @@ import { outputType } from '../../../../common/constants'; jest.mock('../../app_context', () => ({ appContextService: { + getConfig: () => ({}), getInternalUserSOClient: () => { return {}; }, @@ -42,6 +44,8 @@ function getMockedNewActionSince() { return getNewActionsSince as jest.MockedFunction; } +const mockedEsClient = {} as ElasticsearchClient; + describe('test agent checkin new action services', () => { describe('newAgetActionObservable', () => { beforeEach(() => { @@ -161,12 +165,18 @@ describe('test agent checkin new action services', () => { ]; expect( - await createAgentActionFromPolicyAction(mockSavedObjectsClient, mockAgent, mockPolicyAction) + await createAgentActionFromPolicyAction( + mockSavedObjectsClient, + mockedEsClient, + mockAgent, + mockPolicyAction + ) ).toEqual(expectedResult); expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.10.0-SNAPSHOT' } } } }, mockPolicyAction ) @@ -175,6 +185,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.10.2' } } } }, mockPolicyAction ) @@ -183,6 +194,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '8.0.0' } } } }, mockPolicyAction ) @@ -191,6 +203,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '8.0.0-SNAPSHOT' } } } }, mockPolicyAction ) @@ -218,6 +231,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.9.0' } } } }, mockPolicyAction ) @@ -226,6 +240,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.9.3' } } } }, mockPolicyAction ) @@ -234,6 +249,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.9.1-SNAPSHOT' } } } }, mockPolicyAction ) @@ -242,6 +258,7 @@ describe('test agent checkin new action services', () => { expect( await createAgentActionFromPolicyAction( mockSavedObjectsClient, + mockedEsClient, { ...mockAgent, local_metadata: { elastic: { agent: { version: '7.8.2' } } } }, mockPolicyAction ) diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts index 624b7bbcae5721..01759c2015cdf6 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts @@ -45,7 +45,7 @@ import { } from '../actions'; import { appContextService } from '../../app_context'; import { toPromiseAbortable, AbortError, createRateLimiter } from './rxjs_utils'; -import { getAgent } from '../crud'; +import { getAgent, updateAgent } from '../crud'; function getInternalUserSOClient() { const fakeRequest = ({ @@ -106,31 +106,45 @@ function createAgentPolicyActionSharedObservable(agentPolicyId: string) { ); } -async function getOrCreateAgentDefaultOutputAPIKey( +async function getAgentDefaultOutputAPIKey( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agent: Agent -): Promise { - const { - attributes: { default_api_key: defaultApiKey }, - } = await appContextService - .getEncryptedSavedObjects() - .getDecryptedAsInternalUser(AGENT_SAVED_OBJECT_TYPE, agent.id); +) { + if (appContextService.getConfig()?.agents?.fleetServerEnabled) { + return agent.default_api_key; + } else { + const { + attributes: { default_api_key: defaultApiKey }, + } = await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser(AGENT_SAVED_OBJECT_TYPE, agent.id); - if (defaultApiKey) { return defaultApiKey; } +} + +async function getOrCreateAgentDefaultOutputAPIKey( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agent: Agent +): Promise { + const defaultAPIKey = await getAgentDefaultOutputAPIKey(soClient, esClient, agent); + if (defaultAPIKey) { + return defaultAPIKey; + } const outputAPIKey = await APIKeysService.generateOutputApiKey(soClient, 'default', agent.id); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agent.id, { + await updateAgent(soClient, esClient, agent.id, { default_api_key: outputAPIKey.key, default_api_key_id: outputAPIKey.id, }); - return outputAPIKey.key; } export async function createAgentActionFromPolicyAction( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agent: Agent, policyAction: AgentPolicyAction ) { @@ -168,7 +182,7 @@ export async function createAgentActionFromPolicyAction( ); // Mutate the policy to set the api token for this agent - const apiKey = await getOrCreateAgentDefaultOutputAPIKey(soClient, agent); + const apiKey = await getOrCreateAgentDefaultOutputAPIKey(soClient, esClient, agent); if (newAgentAction.data.policy) { newAgentAction.data.policy.outputs.default.api_key = apiKey; } @@ -249,7 +263,9 @@ export function agentCheckinStateNewActionsFactory() { (!agent.policy_revision || action.policy_revision > agent.policy_revision) ), rateLimiter(), - concatMap((policyAction) => createAgentActionFromPolicyAction(soClient, agent, policyAction)), + concatMap((policyAction) => + createAgentActionFromPolicyAction(soClient, esClient, agent, policyAction) + ), merge(newActions$), concatMap((data: AgentAction[] | undefined) => { if (data === undefined) { @@ -274,7 +290,7 @@ export function agentCheckinStateNewActionsFactory() { }), rateLimiter(), concatMap((policyAction) => - createAgentActionFromPolicyAction(soClient, agent, policyAction) + createAgentActionFromPolicyAction(soClient, esClient, agent, policyAction) ) ); } diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 36506d05905958..c80fd77fc11ecc 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -5,13 +5,8 @@ * 2.0. */ -import Boom from '@hapi/boom'; import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; - -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { AgentSOAttributes, Agent, ListWithKuery } from '../../types'; -import { escapeSearchQueryPhrase } from '../saved_object'; -import { savedObjectToAgent } from './saved_objects'; import { appContextService, agentPolicyService } from '../../services'; import * as crudServiceSO from './crud_so'; import * as crudServiceFleetServer from './crud_fleet_server'; @@ -75,15 +70,15 @@ export async function getAgent( : crudServiceSO.getAgent(soClient, agentId); } -export async function getAgents(soClient: SavedObjectsClientContract, agentIds: string[]) { - const agentSOs = await soClient.bulkGet( - agentIds.map((agentId) => ({ - id: agentId, - type: AGENT_SAVED_OBJECT_TYPE, - })) - ); - const agents = agentSOs.saved_objects.map(savedObjectToAgent); - return agents; +export async function getAgents( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agentIds: string[] +) { + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return fleetServerEnabled + ? crudServiceFleetServer.getAgents(esClient, agentIds) + : crudServiceSO.getAgents(soClient, agentIds); } export async function getAgentPolicyForAgent( @@ -104,38 +99,39 @@ export async function getAgentPolicyForAgent( export async function getAgentByAccessAPIKeyId( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, accessAPIKeyId: string ): Promise { - const response = await soClient.find({ - type: AGENT_SAVED_OBJECT_TYPE, - searchFields: ['access_api_key_id'], - search: escapeSearchQueryPhrase(accessAPIKeyId), - }); - const [agent] = response.saved_objects.map(savedObjectToAgent); - - if (!agent) { - throw Boom.notFound('Agent not found'); - } - if (agent.access_api_key_id !== accessAPIKeyId) { - throw new Error('Agent api key id is not matching'); - } - if (!agent.active) { - throw Boom.forbidden('Agent inactive'); - } - - return agent; + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return fleetServerEnabled + ? crudServiceFleetServer.getAgentByAccessAPIKeyId(esClient, accessAPIKeyId) + : crudServiceSO.getAgentByAccessAPIKeyId(soClient, accessAPIKeyId); } export async function updateAgent( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentId: string, - data: { - userProvidedMetatada: any; - } + data: Partial +) { + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return fleetServerEnabled + ? crudServiceFleetServer.updateAgent(esClient, agentId, data) + : crudServiceSO.updateAgent(soClient, agentId, data); +} + +export async function bulkUpdateAgents( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + data: Array<{ + agentId: string; + data: Partial; + }> ) { - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { - user_provided_metadata: data.userProvidedMetatada, - }); + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return fleetServerEnabled + ? crudServiceFleetServer.bulkUpdateAgents(esClient, data) + : crudServiceSO.bulkUpdateAgents(soClient, data); } export async function deleteAgent( diff --git a/x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts b/x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts index c9aa221edf4d29..caff15efff68c2 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts @@ -6,31 +6,46 @@ */ import Boom from '@hapi/boom'; -import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; +import { SearchResponse } from 'elasticsearch'; +import { ElasticsearchClient } from 'src/core/server'; -import { isAgentUpgradeable, SO_SEARCH_LIMIT } from '../../../common'; +import { FleetServerAgent, isAgentUpgradeable, SO_SEARCH_LIMIT } from '../../../common'; import { AGENT_SAVED_OBJECT_TYPE, AGENTS_INDEX } from '../../constants'; import { ESSearchHit } from '../../../../../typings/elasticsearch'; import { AgentSOAttributes, Agent, ListWithKuery } from '../../types'; import { escapeSearchQueryPhrase, normalizeKuery } from '../saved_object'; -import { savedObjectToAgent } from './saved_objects'; -import { searchHitToAgent } from './helpers'; +import { searchHitToAgent, agentSOAttributesToFleetServerAgentDoc } from './helpers'; import { appContextService } from '../../services'; +import { esKuery, KueryNode } from '../../../../../../src/plugins/data/server'; const ACTIVE_AGENT_CONDITION = 'active:true'; const INACTIVE_AGENT_CONDITION = `NOT (${ACTIVE_AGENT_CONDITION})`; -function _joinFilters(filters: string[], operator = 'AND') { - return filters.reduce((acc: string | undefined, filter) => { - if (acc) { - return `${acc} ${operator} (${filter})`; - } +function _joinFilters(filters: Array): KueryNode | undefined { + return filters + .filter((filter) => filter !== undefined) + .reduce((acc: KueryNode | undefined, kuery: string | KueryNode | undefined): + | KueryNode + | undefined => { + if (kuery === undefined) { + return acc; + } + const kueryNode: KueryNode = + typeof kuery === 'string' ? esKuery.fromKueryExpression(removeSOAttributes(kuery)) : kuery; - return `(${filter})`; - }, undefined); + if (!acc) { + return kueryNode; + } + + return { + type: 'function', + function: 'and', + arguments: [acc, kueryNode], + }; + }, undefined as KueryNode | undefined); } -function removeSOAttributes(kuery: string) { +export function removeSOAttributes(kuery: string) { return kuery.replace(/attributes\./g, '').replace(/fleet-agents\./g, ''); } @@ -57,20 +72,23 @@ export async function listAgents( const filters = []; if (kuery && kuery !== '') { - filters.push(removeSOAttributes(kuery)); + filters.push(kuery); } if (showInactive === false) { filters.push(ACTIVE_AGENT_CONDITION); } + const kueryNode = _joinFilters(filters); + const body = kueryNode ? { query: esKuery.toElasticsearchQuery(kueryNode) } : {}; + const res = await esClient.search({ index: AGENTS_INDEX, from: (page - 1) * perPage, size: perPage, sort: `${sortField}:${sortOrder}`, track_total_hits: true, - q: _joinFilters(filters), + body, }); let agentResults: Agent[] = res.body.hits.hits.map(searchHitToAgent); @@ -121,18 +139,20 @@ export async function countInactiveAgents( filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); } + const kueryNode = _joinFilters(filters); + const body = kueryNode ? { query: esKuery.toElasticsearchQuery(kueryNode) } : {}; + const res = await esClient.search({ index: AGENTS_INDEX, size: 0, track_total_hits: true, - q: _joinFilters(filters), + body, }); - return res.body.hits.total.value; } export async function getAgent(esClient: ElasticsearchClient, agentId: string) { - const agentHit = await esClient.get>({ + const agentHit = await esClient.get>({ index: AGENTS_INDEX, id: agentId, }); @@ -141,27 +161,31 @@ export async function getAgent(esClient: ElasticsearchClient, agentId: string) { return agent; } -export async function getAgents(soClient: SavedObjectsClientContract, agentIds: string[]) { - const agentSOs = await soClient.bulkGet( - agentIds.map((agentId) => ({ - id: agentId, - type: AGENT_SAVED_OBJECT_TYPE, - })) - ); - const agents = agentSOs.saved_objects.map(savedObjectToAgent); +export async function getAgents( + esClient: ElasticsearchClient, + agentIds: string[] +): Promise { + const body = { docs: agentIds.map((_id) => ({ _id })) }; + + const res = await esClient.mget({ + body, + index: AGENTS_INDEX, + }); + + const agents = res.body.docs.map(searchHitToAgent); return agents; } export async function getAgentByAccessAPIKeyId( - soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, accessAPIKeyId: string ): Promise { - const response = await soClient.find({ - type: AGENT_SAVED_OBJECT_TYPE, - searchFields: ['access_api_key_id'], - search: escapeSearchQueryPhrase(accessAPIKeyId), + const res = await esClient.search>({ + index: AGENTS_INDEX, + q: `access_api_key_id:${escapeSearchQueryPhrase(accessAPIKeyId)}`, }); - const [agent] = response.saved_objects.map(savedObjectToAgent); + + const [agent] = res.body.hits.hits.map(searchHitToAgent); if (!agent) { throw Boom.notFound('Agent not found'); @@ -177,15 +201,49 @@ export async function getAgentByAccessAPIKeyId( } export async function updateAgent( - soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentId: string, - data: { - userProvidedMetatada: any; - } + data: Partial ) { - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { - user_provided_metadata: data.userProvidedMetatada, + await esClient.update({ + id: agentId, + index: AGENTS_INDEX, + body: { doc: agentSOAttributesToFleetServerAgentDoc(data) }, + refresh: 'wait_for', + }); +} + +export async function bulkUpdateAgents( + esClient: ElasticsearchClient, + updateData: Array<{ + agentId: string; + data: Partial; + }> +) { + const body = updateData.flatMap(({ agentId, data }) => [ + { + update: { + _id: agentId, + }, + }, + { + doc: { ...agentSOAttributesToFleetServerAgentDoc(data) }, + }, + ]); + + const res = await esClient.bulk({ + body, + index: AGENTS_INDEX, + refresh: 'wait_for', }); + + return { + items: res.body.items.map((item: { update: { _id: string; error?: Error } }) => ({ + id: item.update._id, + success: !item.update.error, + error: item.update.error, + })), + }; } export async function deleteAgent(esClient: ElasticsearchClient, agentId: string) { @@ -193,7 +251,7 @@ export async function deleteAgent(esClient: ElasticsearchClient, agentId: string id: agentId, index: AGENT_SAVED_OBJECT_TYPE, body: { - active: false, + doc: { active: false }, }, }); } diff --git a/x-pack/plugins/fleet/server/services/agents/crud_so.ts b/x-pack/plugins/fleet/server/services/agents/crud_so.ts index 11991a971829a0..c3ceb4b7502e26 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud_so.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud_so.ts @@ -6,7 +6,7 @@ */ import Boom from '@hapi/boom'; -import { SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsBulkUpdateObject, SavedObjectsClientContract } from 'src/core/server'; import { isAgentUpgradeable } from '../../../common'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; @@ -197,13 +197,35 @@ export async function getAgentByAccessAPIKeyId( export async function updateAgent( soClient: SavedObjectsClientContract, agentId: string, - data: { - userProvidedMetatada: any; - } + data: Partial ) { - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { - user_provided_metadata: data.userProvidedMetatada, - }); + await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, data); +} + +export async function bulkUpdateAgents( + soClient: SavedObjectsClientContract, + updateData: Array<{ + agentId: string; + data: Partial; + }> +) { + const updates: Array> = updateData.map( + ({ agentId, data }) => ({ + type: AGENT_SAVED_OBJECT_TYPE, + id: agentId, + attributes: data, + }) + ); + + const res = await soClient.bulkUpdate(updates); + + return { + items: res.saved_objects.map((so) => ({ + id: so.id, + success: !so.error, + error: so.error, + })), + }; } export async function deleteAgent(soClient: SavedObjectsClientContract, agentId: string) { diff --git a/x-pack/plugins/fleet/server/services/agents/enroll.ts b/x-pack/plugins/fleet/server/services/agents/enroll.ts index b8be02af101b42..c984a84ceea014 100644 --- a/x-pack/plugins/fleet/server/services/agents/enroll.ts +++ b/x-pack/plugins/fleet/server/services/agents/enroll.ts @@ -6,14 +6,15 @@ */ import Boom from '@hapi/boom'; +import uuid from 'uuid/v4'; import semverParse from 'semver/functions/parse'; import semverDiff from 'semver/functions/diff'; import semverLte from 'semver/functions/lte'; import { SavedObjectsClientContract } from 'src/core/server'; -import { AgentType, Agent, AgentSOAttributes } from '../../types'; +import { AgentType, Agent, AgentSOAttributes, FleetServerAgent } from '../../types'; import { savedObjectToAgent } from './saved_objects'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { AGENT_SAVED_OBJECT_TYPE, AGENTS_INDEX } from '../../constants'; import * as APIKeyService from '../api_keys'; import { appContextService } from '../app_context'; @@ -26,6 +27,36 @@ export async function enroll( const agentVersion = metadata?.local?.elastic?.agent?.version; validateAgentVersion(agentVersion); + if (appContextService.getConfig()?.agents?.fleetServerEnabled) { + const esClient = appContextService.getInternalUserESClient(); + + const agentId = uuid(); + const accessAPIKey = await APIKeyService.generateAccessApiKey(soClient, agentId); + const fleetServerAgent: FleetServerAgent = { + active: true, + policy_id: agentPolicyId, + type, + enrolled_at: new Date().toISOString(), + user_provided_metadata: metadata?.userProvided ?? {}, + local_metadata: metadata?.local ?? {}, + access_api_key_id: accessAPIKey.id, + }; + await esClient.create({ + index: AGENTS_INDEX, + body: fleetServerAgent, + id: agentId, + refresh: 'wait_for', + }); + + return { + id: agentId, + current_error_events: [], + packages: [], + ...fleetServerAgent, + access_api_key: accessAPIKey.key, + } as Agent; + } + const agentData: AgentSOAttributes = { active: true, policy_id: agentPolicyId, diff --git a/x-pack/plugins/fleet/server/services/agents/helpers.ts b/x-pack/plugins/fleet/server/services/agents/helpers.ts index 1000a1b1459328..90d85e98ecd679 100644 --- a/x-pack/plugins/fleet/server/services/agents/helpers.ts +++ b/x-pack/plugins/fleet/server/services/agents/helpers.ts @@ -6,17 +6,30 @@ */ import { ESSearchHit } from '../../../../../typings/elasticsearch'; -import { Agent, AgentSOAttributes } from '../../types'; +import { Agent, AgentSOAttributes, FleetServerAgent } from '../../types'; -export function searchHitToAgent(hit: ESSearchHit): Agent { +export function searchHitToAgent(hit: ESSearchHit): Agent { return { id: hit._id, ...hit._source, - current_error_events: hit._source.current_error_events - ? JSON.parse(hit._source.current_error_events) - : [], + policy_revision: hit._source.policy_revision_idx, + current_error_events: [], access_api_key: undefined, status: undefined, packages: hit._source.packages ?? [], }; } + +export function agentSOAttributesToFleetServerAgentDoc( + data: Partial +): Partial> { + const { policy_revision: policyRevison, ...rest } = data; + + const doc: Partial> = { ...rest }; + + if (policyRevison !== undefined) { + doc.policy_revision_idx = policyRevison; + } + + return doc; +} diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.test.ts b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts index 7338c440483ea6..466870bead71ce 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts @@ -107,6 +107,9 @@ function createClientMock() { saved_objects: [await soClientMock.create(type, attributes)], }; }); + soClientMock.bulkUpdate.mockResolvedValue({ + saved_objects: [], + }); soClientMock.get.mockImplementation(async (_, id) => { switch (id) { diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.ts b/x-pack/plugins/fleet/server/services/agents/reassign.ts index 9f4373ab553ecf..62d59aada3b7ba 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.ts @@ -7,11 +7,16 @@ import type { SavedObjectsClientContract, ElasticsearchClient } from 'kibana/server'; import Boom from '@hapi/boom'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; -import type { AgentSOAttributes } from '../../types'; -import { AgentReassignmentError } from '../../errors'; import { agentPolicyService } from '../agent_policy'; -import { getAgentPolicyForAgent, getAgents, listAllAgents } from './crud'; +import { + getAgents, + getAgentPolicyForAgent, + listAllAgents, + updateAgent, + bulkUpdateAgents, +} from './crud'; +import { AgentReassignmentError } from '../../errors'; + import { createAgentAction, bulkCreateAgentActions } from './actions'; export async function reassignAgent( @@ -27,12 +32,12 @@ export async function reassignAgent( await reassignAgentIsAllowed(soClient, esClient, agentId, newAgentPolicyId); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + await updateAgent(soClient, esClient, agentId, { policy_id: newAgentPolicyId, policy_revision: null, }); - await createAgentAction(soClient, { + await createAgentAction(soClient, esClient, { agent_id: agentId, created_at: new Date().toISOString(), type: 'INTERNAL_POLICY_REASSIGN', @@ -73,7 +78,7 @@ export async function reassignAgents( kuery: string; }, newAgentPolicyId: string -) { +): Promise<{ items: Array<{ id: string; sucess: boolean; error?: Error }> }> { const agentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); if (!agentPolicy) { throw Boom.notFound(`Agent policy not found: ${newAgentPolicyId}`); @@ -82,7 +87,7 @@ export async function reassignAgents( // Filter to agents that do not already use the new agent policy ID const agents = 'agentIds' in options - ? await getAgents(soClient, options.agentIds) + ? await getAgents(soClient, esClient, options.agentIds) : ( await listAllAgents(soClient, esClient, { kuery: options.kuery, @@ -99,20 +104,22 @@ export async function reassignAgents( (agent, index) => settled[index].status === 'fulfilled' && agent.policy_id !== newAgentPolicyId ); - // Update the necessary agents - const res = await soClient.bulkUpdate( + const res = await bulkUpdateAgents( + soClient, + esClient, agentsToUpdate.map((agent) => ({ - type: AGENT_SAVED_OBJECT_TYPE, - id: agent.id, - attributes: { + agentId: agent.id, + data: { policy_id: newAgentPolicyId, policy_revision: null, }, })) ); + const now = new Date().toISOString(); await bulkCreateAgentActions( soClient, + esClient, agentsToUpdate.map((agent) => ({ agent_id: agent.id, created_at: now, diff --git a/x-pack/plugins/fleet/server/services/agents/status.ts b/x-pack/plugins/fleet/server/services/agents/status.ts index c75b91b3fbd11c..42d3aff2b0d702 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.ts @@ -14,6 +14,8 @@ import { AgentStatus } from '../../types'; import { AgentStatusKueryHelper } from '../../../common/services'; import { esKuery, KueryNode } from '../../../../../../src/plugins/data/server'; import { normalizeKuery } from '../saved_object'; +import { appContextService } from '../app_context'; +import { removeSOAttributes } from './crud_fleet_server'; export async function getAgentStatusById( soClient: SavedObjectsClientContract, @@ -27,6 +29,8 @@ export async function getAgentStatusById( export const getAgentStatus = AgentStatusKueryHelper.getAgentStatus; function joinKuerys(...kuerys: Array) { + const isFleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return kuerys .filter((kuery) => kuery !== undefined) .reduce((acc: KueryNode | undefined, kuery: string | undefined): KueryNode | undefined => { @@ -34,7 +38,9 @@ function joinKuerys(...kuerys: Array) { return acc; } const normalizedKuery: KueryNode = esKuery.fromKueryExpression( - normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery || '') + isFleetServerEnabled + ? removeSOAttributes(kuery || '') + : normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery || '') ); if (!acc) { diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts index b8c1b7befb443c..cd46cff0f8a174 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts @@ -73,6 +73,7 @@ describe('unenrollAgents (plural)', () => { }); it('cannot unenroll from a managed policy', async () => { const soClient = createClientMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const idsToUnenroll = [agentInUnmanagedSO.id, agentInManagedSO.id, agentInUnmanagedSO2.id]; await unenrollAgents(soClient, esClient, { agentIds: idsToUnenroll }); @@ -98,6 +99,9 @@ function createClientMock() { saved_objects: [await soClientMock.create(type, attributes)], }; }); + soClientMock.bulkUpdate.mockResolvedValue({ + saved_objects: [], + }); soClientMock.get.mockImplementation(async (_, id) => { switch (id) { diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.ts index e2fa83cf32b637..72d551a1229801 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.ts @@ -4,13 +4,19 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; -import type { AgentSOAttributes } from '../../types'; -import { AgentUnenrollmentError } from '../../errors'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; + +import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; import * as APIKeyService from '../api_keys'; import { createAgentAction, bulkCreateAgentActions } from './actions'; -import { getAgent, getAgentPolicyForAgent, getAgents, listAllAgents } from './crud'; +import { + getAgent, + updateAgent, + getAgentPolicyForAgent, + getAgents, + listAllAgents, + bulkUpdateAgents, +} from './crud'; +import { AgentUnenrollmentError } from '../../errors'; async function unenrollAgentIsAllowed( soClient: SavedObjectsClientContract, @@ -35,12 +41,12 @@ export async function unenrollAgent( await unenrollAgentIsAllowed(soClient, esClient, agentId); const now = new Date().toISOString(); - await createAgentAction(soClient, { + await createAgentAction(soClient, esClient, { agent_id: agentId, created_at: now, type: 'UNENROLL', }); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + await updateAgent(soClient, esClient, agentId, { unenrollment_started_at: now, }); } @@ -58,7 +64,7 @@ export async function unenrollAgents( ) { const agents = 'agentIds' in options - ? await getAgents(soClient, options.agentIds) + ? await getAgents(soClient, esClient, options.agentIds) : ( await listAllAgents(soClient, esClient, { kuery: options.kuery, @@ -83,6 +89,7 @@ export async function unenrollAgents( // Create unenroll action for each agent await bulkCreateAgentActions( soClient, + esClient, agentsToUpdate.map((agent) => ({ agent_id: agent.id, created_at: now, @@ -91,11 +98,12 @@ export async function unenrollAgents( ); // Update the necessary agents - return await soClient.bulkUpdate( + return bulkUpdateAgents( + soClient, + esClient, agentsToUpdate.map((agent) => ({ - type: AGENT_SAVED_OBJECT_TYPE, - id: agent.id, - attributes: { + agentId: agent.id, + data: { unenrollment_started_at: now, }, })) @@ -118,7 +126,7 @@ export async function forceUnenrollAgent( : undefined, ]); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + await updateAgent(soClient, esClient, agentId, { active: false, unenrolled_at: new Date().toISOString(), }); @@ -138,7 +146,7 @@ export async function forceUnenrollAgents( // Filter to agents that are not already unenrolled const agents = 'agentIds' in options - ? await getAgents(soClient, options.agentIds) + ? await getAgents(soClient, esClient, options.agentIds) : ( await listAllAgents(soClient, esClient, { kuery: options.kuery, @@ -163,13 +171,13 @@ export async function forceUnenrollAgents( if (apiKeys.length) { APIKeyService.invalidateAPIKeys(soClient, apiKeys); } - // Update the necessary agents - return await soClient.bulkUpdate( + return bulkUpdateAgents( + soClient, + esClient, agentsToUpdate.map((agent) => ({ - type: AGENT_SAVED_OBJECT_TYPE, - id: agent.id, - attributes: { + agentId: agent.id, + data: { active: false, unenrolled_at: now, }, diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.ts index 7475ad49681427..5105e145309827 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.ts @@ -6,20 +6,22 @@ */ import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; -import { AgentSOAttributes, AgentAction, AgentActionSOAttributes } from '../../types'; -import { AGENT_ACTION_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { AgentAction, AgentActionSOAttributes } from '../../types'; +import { AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../constants'; import { bulkCreateAgentActions, createAgentAction } from './actions'; -import { getAgents, listAllAgents } from './crud'; +import { getAgents, listAllAgents, updateAgent, bulkUpdateAgents } from './crud'; import { isAgentUpgradeable } from '../../../common/services'; import { appContextService } from '../app_context'; export async function sendUpgradeAgentAction({ soClient, + esClient, agentId, version, sourceUri, }: { soClient: SavedObjectsClientContract; + esClient: ElasticsearchClient; agentId: string; version: string; sourceUri: string | undefined; @@ -29,21 +31,22 @@ export async function sendUpgradeAgentAction({ version, source_uri: sourceUri, }; - await createAgentAction(soClient, { + await createAgentAction(soClient, esClient, { agent_id: agentId, created_at: now, data, ack_data: data, type: 'UPGRADE', }); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { - upgraded_at: undefined, + await updateAgent(soClient, esClient, agentId, { + upgraded_at: null, upgrade_started_at: now, }); } export async function ackAgentUpgraded( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentAction: AgentAction ) { const { @@ -52,9 +55,9 @@ export async function ackAgentUpgraded( if (!ackData) throw new Error('data missing from UPGRADE action'); const { version } = JSON.parse(ackData); if (!version) throw new Error('version missing from UPGRADE action'); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentAction.agent_id, { + await updateAgent(soClient, esClient, agentAction.agent_id, { upgraded_at: new Date().toISOString(), - upgrade_started_at: undefined, + upgrade_started_at: null, }); } @@ -79,7 +82,7 @@ export async function sendUpgradeAgentsActions( // Filter out agents currently unenrolling, agents unenrolled, and agents not upgradeable const agents = 'agentIds' in options - ? await getAgents(soClient, options.agentIds) + ? await getAgents(soClient, esClient, options.agentIds) : ( await listAllAgents(soClient, esClient, { kuery: options.kuery, @@ -97,6 +100,7 @@ export async function sendUpgradeAgentsActions( // Create upgrade action for each agent await bulkCreateAgentActions( soClient, + esClient, agentsToUpdate.map((agent) => ({ agent_id: agent.id, created_at: now, @@ -106,12 +110,13 @@ export async function sendUpgradeAgentsActions( })) ); - return await soClient.bulkUpdate( + return await bulkUpdateAgents( + soClient, + esClient, agentsToUpdate.map((agent) => ({ - type: AGENT_SAVED_OBJECT_TYPE, - id: agent.id, - attributes: { - upgraded_at: undefined, + agentId: agent.id, + data: { + upgraded_at: null, upgrade_started_at: now, }, })) diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts index 97d48702cf4c6e..85812fee3885c8 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts @@ -79,3 +79,15 @@ export async function generateEnrollmentAPIKey( return enrollmentApiKeyServiceSO.generateEnrollmentAPIKey(soClient, data); } } + +export async function getEnrollmentAPIKeyById( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + apiKeyId: string +) { + if (appContextService.getConfig()?.agents?.fleetServerEnabled === true) { + return enrollmentApiKeyServiceFleetServer.getEnrollmentAPIKeyById(esClient, apiKeyId); + } else { + return enrollmentApiKeyServiceSO.getEnrollmentAPIKeyById(soClient, apiKeyId); + } +} diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts index d42cb19a340bdd..f5d0015297daa0 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts @@ -7,41 +7,15 @@ import uuid from 'uuid'; import Boom from '@hapi/boom'; +import { GetResponse } from 'elasticsearch'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; +import { ESSearchResponse as SearchResponse } from '../../../../../typings/elasticsearch'; import { EnrollmentAPIKey, FleetServerEnrollmentAPIKey } from '../../types'; import { ENROLLMENT_API_KEYS_INDEX } from '../../constants'; import { createAPIKey, invalidateAPIKeys } from './security'; import { agentPolicyService } from '../agent_policy'; - -// TODO Move these types to another file -interface SearchResponse { - took: number; - timed_out: boolean; - _scroll_id?: string; - hits: { - total: { - value: number; - relation: string; - }; - max_score: number; - hits: Array<{ - _index: string; - _type: string; - _id: string; - _score: number; - _source: T; - _version?: number; - fields?: any; - highlight?: any; - inner_hits?: any; - matched_queries?: string[]; - sort?: string[]; - }>; - }; -} - -type SearchHit = SearchResponse['hits']['hits'][0]; +import { escapeSearchQueryPhrase } from '../saved_object'; export async function listEnrollmentApiKeys( esClient: ElasticsearchClient, @@ -54,7 +28,7 @@ export async function listEnrollmentApiKeys( ): Promise<{ items: EnrollmentAPIKey[]; total: any; page: any; perPage: any }> { const { page = 1, perPage = 20, kuery } = options; - const res = await esClient.search>({ + const res = await esClient.search>({ index: ENROLLMENT_API_KEYS_INDEX, from: (page - 1) * perPage, size: perPage, @@ -78,7 +52,7 @@ export async function getEnrollmentAPIKey( id: string ): Promise { try { - const res = await esClient.get>({ + const res = await esClient.get>({ index: ENROLLMENT_API_KEYS_INDEX, id, }); @@ -185,6 +159,21 @@ export async function generateEnrollmentAPIKey( }; } +export async function getEnrollmentAPIKeyById(esClient: ElasticsearchClient, apiKeyId: string) { + const res = await esClient.search>({ + index: ENROLLMENT_API_KEYS_INDEX, + q: `api_key_id:${escapeSearchQueryPhrase(apiKeyId)}`, + }); + + const [enrollmentAPIKey] = res.body.hits.hits.map(esDocToEnrollmentApiKey); + + if (enrollmentAPIKey?.api_key_id !== apiKeyId) { + throw new Error('find enrollmentKeyById returned an incorrect key'); + } + + return enrollmentAPIKey; +} + async function validateAgentPolicyId(soClient: SavedObjectsClientContract, agentPolicyId: string) { try { await agentPolicyService.get(soClient, agentPolicyId); @@ -196,7 +185,10 @@ async function validateAgentPolicyId(soClient: SavedObjectsClientContract, agent } } -function esDocToEnrollmentApiKey(doc: SearchHit): EnrollmentAPIKey { +function esDocToEnrollmentApiKey(doc: { + _id: string; + _source: FleetServerEnrollmentAPIKey; +}): EnrollmentAPIKey { return { id: doc._id, ...doc._source, diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts index b3beab546c811b..014bc58e747ea4 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts @@ -13,7 +13,7 @@ import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; import { createAPIKey, invalidateAPIKeys } from './security'; import { agentPolicyService } from '../agent_policy'; import { appContextService } from '../app_context'; -import { normalizeKuery } from '../saved_object'; +import { normalizeKuery, escapeSearchQueryPhrase } from '../saved_object'; export async function listEnrollmentApiKeys( soClient: SavedObjectsClientContract, @@ -159,6 +159,25 @@ async function validateAgentPolicyId(soClient: SavedObjectsClientContract, agent } } +export async function getEnrollmentAPIKeyById( + soClient: SavedObjectsClientContract, + apiKeyId: string +) { + const [enrollmentAPIKey] = ( + await soClient.find({ + type: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + searchFields: ['api_key_id'], + search: escapeSearchQueryPhrase(apiKeyId), + }) + ).saved_objects.map(savedObjectToEnrollmentApiKey); + + if (enrollmentAPIKey?.api_key_id !== apiKeyId) { + throw new Error('find enrollmentKeyById returned an incorrect key'); + } + + return enrollmentAPIKey; +} + function savedObjectToEnrollmentApiKey({ error, attributes, diff --git a/x-pack/plugins/fleet/server/services/api_keys/index.ts b/x-pack/plugins/fleet/server/services/api_keys/index.ts index 5cdadeb0c82d84..65051163c78c3a 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/index.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/index.ts @@ -5,11 +5,8 @@ * 2.0. */ -import { SavedObjectsClientContract, SavedObject, KibanaRequest } from 'src/core/server'; -import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; -import { EnrollmentAPIKeySOAttributes, EnrollmentAPIKey } from '../../types'; +import { SavedObjectsClientContract, KibanaRequest } from 'src/core/server'; import { createAPIKey } from './security'; -import { escapeSearchQueryPhrase } from '../saved_object'; export { invalidateAPIKeys } from './security'; export * from './enrollment_api_key'; @@ -70,25 +67,6 @@ export async function generateAccessApiKey(soClient: SavedObjectsClientContract, return { id: key.id, key: Buffer.from(`${key.id}:${key.api_key}`).toString('base64') }; } -export async function getEnrollmentAPIKeyById( - soClient: SavedObjectsClientContract, - apiKeyId: string -) { - const [enrollmentAPIKey] = ( - await soClient.find({ - type: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, - searchFields: ['api_key_id'], - search: escapeSearchQueryPhrase(apiKeyId), - }) - ).saved_objects.map(_savedObjectToEnrollmentApiKey); - - if (enrollmentAPIKey?.api_key_id !== apiKeyId) { - throw new Error('find enrollmentKeyById returned an incorrect key'); - } - - return enrollmentAPIKey; -} - export function parseApiKeyFromHeaders(headers: KibanaRequest['headers']) { const authorizationHeader = headers.authorization; @@ -117,18 +95,3 @@ export function parseApiKey(apiKey: string) { apiKeyId, }; } - -function _savedObjectToEnrollmentApiKey({ - error, - attributes, - id, -}: SavedObject): EnrollmentAPIKey { - if (error) { - throw new Error(error.message); - } - - return { - id, - ...attributes, - }; -} diff --git a/x-pack/plugins/fleet/server/services/fleet_server_migration.ts b/x-pack/plugins/fleet/server/services/fleet_server_migration.ts index f982332886e943..170bec54983c0e 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server_migration.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server_migration.ts @@ -5,11 +5,18 @@ * 2.0. */ +import { isBoom } from '@hapi/boom'; import { KibanaRequest } from 'src/core/server'; import { ENROLLMENT_API_KEYS_INDEX, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + AGENT_POLICY_INDEX, + AGENTS_INDEX, FleetServerEnrollmentAPIKey, + AGENT_SAVED_OBJECT_TYPE, + AgentSOAttributes, + FleetServerAgent, + SO_SEARCH_LIMIT, FLEET_SERVER_PACKAGE, FLEET_SERVER_INDICES, } from '../../common'; @@ -17,6 +24,9 @@ import { listEnrollmentApiKeys, getEnrollmentAPIKey } from './api_keys/enrollmen import { appContextService } from './app_context'; import { getInstallation } from './epm/packages'; +import { isAgentsSetup } from './agents'; +import { agentPolicyService } from './agent_policy'; + export async function isFleetServerSetup() { const pkgInstall = await getInstallation({ savedObjectsClient: getInternalUserSOClient(), @@ -28,7 +38,6 @@ export async function isFleetServerSetup() { } const esClient = appContextService.getInternalUserESClient(); - const exists = await Promise.all( FLEET_SERVER_INDICES.map(async (index) => { const res = await esClient.indices.exists({ @@ -42,7 +51,11 @@ export async function isFleetServerSetup() { } export async function runFleetServerMigration() { - await migrateEnrollmentApiKeys(); + // If Agents are not setup skip as there is nothing to migrate + if (!(await isAgentsSetup(getInternalUserSOClient()))) { + return; + } + await Promise.all([migrateEnrollmentApiKeys(), migrateAgentPolicies(), migrateAgents()]); } function getInternalUserSOClient() { @@ -64,6 +77,65 @@ function getInternalUserSOClient() { return appContextService.getInternalUserSOClient(fakeRequest); } +async function migrateAgents() { + const esClient = appContextService.getInternalUserESClient(); + const soClient = getInternalUserSOClient(); + let hasMore = true; + while (hasMore) { + const res = await soClient.find({ + type: AGENT_SAVED_OBJECT_TYPE, + page: 1, + perPage: 100, + }); + + if (res.total === 0) { + hasMore = false; + } + for (const so of res.saved_objects) { + try { + const { + attributes, + } = await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser(AGENT_SAVED_OBJECT_TYPE, so.id); + + const body: FleetServerAgent = { + type: attributes.type, + active: attributes.active, + enrolled_at: attributes.enrolled_at, + unenrolled_at: attributes.unenrolled_at, + unenrollment_started_at: attributes.unenrollment_started_at, + upgraded_at: attributes.upgraded_at, + upgrade_started_at: attributes.upgrade_started_at, + access_api_key_id: attributes.access_api_key_id, + user_provided_metadata: attributes.user_provided_metadata, + local_metadata: attributes.local_metadata, + policy_id: attributes.policy_id, + policy_revision_idx: attributes.policy_revision || undefined, + last_checkin: attributes.last_checkin, + last_checkin_status: attributes.last_checkin_status, + default_api_key_id: attributes.default_api_key_id, + default_api_key: attributes.default_api_key, + packages: attributes.packages, + }; + await esClient.create({ + index: AGENTS_INDEX, + body, + id: so.id, + refresh: 'wait_for', + }); + + await soClient.delete(AGENT_SAVED_OBJECT_TYPE, so.id); + } catch (error) { + // swallow 404 error has multiple Kibana can run the migration at the same time + if (!is404Error(error)) { + throw error; + } + } + } + } +} + async function migrateEnrollmentApiKeys() { const esClient = appContextService.getInternalUserESClient(); const soClient = getInternalUserSOClient(); @@ -77,24 +149,61 @@ async function migrateEnrollmentApiKeys() { hasMore = false; } for (const item of res.items) { - const key = await getEnrollmentAPIKey(soClient, item.id); - - const body: FleetServerEnrollmentAPIKey = { - api_key: key.api_key, - api_key_id: key.api_key_id, - active: key.active, - created_at: key.created_at, - name: key.name, - policy_id: key.policy_id, - }; - await esClient.create({ - index: ENROLLMENT_API_KEYS_INDEX, - body, - id: key.id, - refresh: 'wait_for', - }); + try { + const key = await getEnrollmentAPIKey(soClient, item.id); + + const body: FleetServerEnrollmentAPIKey = { + api_key: key.api_key, + api_key_id: key.api_key_id, + active: key.active, + created_at: key.created_at, + name: key.name, + policy_id: key.policy_id, + }; + await esClient.create({ + index: ENROLLMENT_API_KEYS_INDEX, + body, + id: key.id, + refresh: 'wait_for', + }); - await soClient.delete(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, key.id); + await soClient.delete(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, key.id); + } catch (error) { + // swallow 404 error has multiple Kibana can run the migration at the same time + if (!is404Error(error)) { + throw error; + } + } } } } + +async function migrateAgentPolicies() { + const esClient = appContextService.getInternalUserESClient(); + const soClient = getInternalUserSOClient(); + const { items: agentPolicies } = await agentPolicyService.list(soClient, { + perPage: SO_SEARCH_LIMIT, + }); + + await Promise.all( + agentPolicies.map(async (agentPolicy) => { + const res = await esClient.search({ + index: AGENT_POLICY_INDEX, + q: `policy_id:${agentPolicy.id}`, + track_total_hits: true, + }); + + if (res.body.hits.total.value === 0) { + return agentPolicyService.createFleetPolicyChangeFleetServer( + soClient, + esClient, + agentPolicy.id + ); + } + }) + ); +} + +function is404Error(error: any) { + return isBoom(error) && error.output.statusCode === 404; +} diff --git a/x-pack/plugins/fleet/server/services/index.ts b/x-pack/plugins/fleet/server/services/index.ts index 9999ab91e31b22..77ce882275b6bf 100644 --- a/x-pack/plugins/fleet/server/services/index.ts +++ b/x-pack/plugins/fleet/server/services/index.ts @@ -49,6 +49,7 @@ export interface AgentService { */ authenticateAgentWithAccessToken( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, request: KibanaRequest ): Promise; /** diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index ab24c26e0cdfae..f19ad4e7fe417d 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -93,6 +93,16 @@ async function createSetupSideEffects( await runFleetServerMigration(); } + if (appContextService.getConfig()?.agents?.fleetServerEnabled) { + await ensureInstalledPackage({ + savedObjectsClient: soClient, + pkgName: FLEET_SERVER_PACKAGE, + callCluster, + }); + await ensureFleetServerIndicesCreated(esClient); + await runFleetServerMigration(); + } + // If we just created the default policy, ensure default packages are added to it if (defaultAgentPolicyCreated) { const agentPolicyWithPackagePolicies = await agentPolicyService.get( diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index 0c35fc29e01cd7..fda1568c56e0e6 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -81,6 +81,9 @@ export { dataTypes, // Fleet Server types FleetServerEnrollmentAPIKey, + FleetServerAgent, + FleetServerAgentAction, + FleetServerPolicy, } from '../../common'; export type CallESAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser']; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts index 6b58ca71f7f4e7..a2aff41b68df70 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts @@ -174,6 +174,9 @@ describe('test alerts route', () => { savedObjects: { client: mockSavedObjectClient, }, + elasticsearch: { + client: { asInternalUser: elasticsearchServiceMock.createInternalClient() }, + }, }, } as unknown) as SecuritySolutionRequestHandlerContext, mockRequest, @@ -218,6 +221,9 @@ describe('test alerts route', () => { savedObjects: { client: mockSavedObjectClient, }, + elasticsearch: { + client: { asInternalUser: elasticsearchServiceMock.createInternalClient() }, + }, }, } as unknown) as SecuritySolutionRequestHandlerContext, mockRequest, @@ -252,6 +258,9 @@ describe('test alerts route', () => { savedObjects: { client: mockSavedObjectClient, }, + elasticsearch: { + client: { asInternalUser: elasticsearchServiceMock.createInternalClient() }, + }, }, } as unknown) as SecuritySolutionRequestHandlerContext, mockRequest, @@ -280,6 +289,9 @@ describe('test alerts route', () => { savedObjects: { client: mockSavedObjectClient, }, + elasticsearch: { + client: { asInternalUser: elasticsearchServiceMock.createInternalClient() }, + }, }, } as unknown) as SecuritySolutionRequestHandlerContext, mockRequest, @@ -314,6 +326,9 @@ describe('test alerts route', () => { savedObjects: { client: mockSavedObjectClient, }, + elasticsearch: { + client: { asInternalUser: elasticsearchServiceMock.createInternalClient() }, + }, }, } as unknown) as SecuritySolutionRequestHandlerContext, mockRequest, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts index 95563c7c48ef58..3dbaa137bb9281 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts @@ -54,7 +54,11 @@ export function registerDownloadExceptionListRoute( // The ApiKey must be associated with an enrolled Fleet agent try { scopedSOClient = endpointContext.service.getScopedSavedObjectsClient(req); - await authenticateAgentWithAccessToken(scopedSOClient, req); + await authenticateAgentWithAccessToken( + scopedSOClient, + context.core.elasticsearch.client.asInternalUser, + req + ); } catch (err) { if ((err.isBoom ? err.output.statusCode : err.statusCode) === 401) { return res.unauthorized(); From e221992da39734638708dfbb8dfcf19490e6b526 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Sun, 7 Feb 2021 19:11:45 -0500 Subject: [PATCH 15/51] [actions] improve email action doc (#90020) resolves https://github.com/elastic/kibana/issues/88333 Fixed: - add note that `secure: false` will use TLS, but after an initial connection with TCP; we have been getting questions from customers who believed that `secure: false` implied TLS was not used at all. - added a link to the nodemailer "well-known services" module, to allow customers to see examples of other email service configurations - updated the Outlook config example to use the current nodemailer values - couple of other small tweaks --- docs/user/alerting/action-types/email.asciidoc | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/user/alerting/action-types/email.asciidoc b/docs/user/alerting/action-types/email.asciidoc index 83e7edc5a016a6..d7a9373a6e2a99 100644 --- a/docs/user/alerting/action-types/email.asciidoc +++ b/docs/user/alerting/action-types/email.asciidoc @@ -14,7 +14,7 @@ Name:: The name of the connector. The name is used to identify a connector Sender:: The from address for all emails sent with this connector, specified in `user@host-name` format. Host:: Host name of the service provider. If you are using the <> setting, make sure this hostname is added to the allowed hosts. Port:: The port to connect to on the service provider. -Secure:: If true the connection will use TLS when connecting to the service provider. See https://nodemailer.com/smtp/#tls-options[nodemailer TLS documentation] for more information. +Secure:: If true, the connection will use TLS when connecting to the service provider. Refer to the https://nodemailer.com/smtp/#tls-options[Nodemailer TLS documentation] for more information. If not true, the connection will initially connect over TCP, then attempt to switch to TLS via the SMTP STARTTLS command. Username:: username for 'login' type authentication. Password:: password for 'login' type authentication. @@ -92,6 +92,8 @@ systems, refer to: * <> * <> +For other email servers, you can check the list of well-known services that Nodemailer supports in the JSON file https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json[well-known/services.json]. The properties of the objects in those files — `host`, `port`, and `secure` — correspond to the same email action configuration properties. A missing `secure` property in the "well-known/services.json" file is considered `false`. Typically, `port: 465` uses `secure: true`, and `port: 25` and `port: 587` use `secure: false`. + [float] [[gmail]] ===== Sending email from Gmail @@ -109,7 +111,6 @@ https://mail.google.com[Gmail] SMTP service: user: password: -------------------------------------------------- -// CONSOLE If you get an authentication error that indicates that you need to continue the sign-in process from a web browser when the action attempts to send email, you need @@ -131,9 +132,9 @@ https://www.outlook.com/[Outlook.com] SMTP service: [source,text] -------------------------------------------------- config: - host: smtp-mail.outlook.com - port: 465 - secure: true + host: smtp.office365.com + port: 587 + secure: false secrets: user: password: @@ -163,7 +164,7 @@ secrets: user: password: -------------------------------------------------- -<1> `smtp.host` varies depending on the region +<1> `config.host` varies depending on the region NOTE: You must use your Amazon SES SMTP credentials to send email through Amazon SES. For more information, see From c6fc80c748a200572b6b9bf8d19ce93695aab91f Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Sun, 7 Feb 2021 22:48:01 -0500 Subject: [PATCH 16/51] skip flaky suite (#64473) --- .../apis/management/index_management/indices.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/management/index_management/indices.js b/x-pack/test/api_integration/apis/management/index_management/indices.js index cef1bdbba754b8..3653d9916466d0 100644 --- a/x-pack/test/api_integration/apis/management/index_management/indices.js +++ b/x-pack/test/api_integration/apis/management/index_management/indices.js @@ -34,7 +34,8 @@ export default function ({ getService }) { clearCache, } = registerHelpers({ supertest }); - describe('indices', () => { + // Failing: See https://github.com/elastic/kibana/issues/64473 + describe.skip('indices', () => { after(() => Promise.all([cleanUpEsResources()])); describe('clear cache', () => { From 2279c06d1e0732ffea9de6f2ef1824eb00613ad5 Mon Sep 17 00:00:00 2001 From: spalger Date: Sun, 7 Feb 2021 23:24:04 -0700 Subject: [PATCH 17/51] skip flaky suite (#90555) --- x-pack/test/accessibility/apps/uptime.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/accessibility/apps/uptime.ts b/x-pack/test/accessibility/apps/uptime.ts index ec1f37ca02be2f..d7a9cfc0d08b40 100644 --- a/x-pack/test/accessibility/apps/uptime.ts +++ b/x-pack/test/accessibility/apps/uptime.ts @@ -18,7 +18,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const es = getService('es'); - describe('uptime', () => { + // FLAKY: https://github.com/elastic/kibana/issues/90555 + describe.skip('uptime', () => { before(async () => { await esArchiver.load('uptime/blank'); await makeChecks(es, A11Y_TEST_MONITOR_ID, 150, 1, 1000, { From 3b3327dbc3c3041c9681e0cd86bd31cf411dc460 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Mon, 8 Feb 2021 10:19:54 +0100 Subject: [PATCH 18/51] Migrate most plugins to synchronous lifecycle (#89562) * first pass * migrate more plugins * migrate yet more plugins * more oss plugins * fix test file * change Plugin signature on the client-side too * fix test types * migrate OSS client-side plugins * migrate OSS client-side test plugins * migrate xpack client-side plugins * revert fix attempt on fleet plugin * fix presentation start signature * fix yet another signature * add warnings for server-side async plugins in dev mode * remove unused import * fix isPromise * Add client-side deprecations * update migration examples * update generated doc * fix xpack unit tests * nit * (will be reverted) explicitly await for license to be ready in the auth hook * Revert "(will be reverted) explicitly await for license to be ready in the auth hook" This reverts commit fdf73feb * restore await on on promise contracts * Revert "(will be reverted) explicitly await for license to be ready in the auth hook" This reverts commit fdf73feb * Revert "restore await on on promise contracts" This reverts commit c5f2fe51 * add delay before starting tests in FTR * update deprecation ts doc * add explicit contract for monitoring setup * migrate monitoring plugin to sync * change plugin timeout to 10sec * use delay instead of silence --- ...migrating-legacy-plugins-examples.asciidoc | 12 +- .../kibana-plugin-core-public.asyncplugin.md | 27 ++++ ...na-plugin-core-public.asyncplugin.setup.md | 23 +++ ...na-plugin-core-public.asyncplugin.start.md | 23 +++ ...ana-plugin-core-public.asyncplugin.stop.md | 15 ++ .../core/public/kibana-plugin-core-public.md | 1 + .../kibana-plugin-core-public.plugin.setup.md | 4 +- .../kibana-plugin-core-public.plugin.start.md | 4 +- ...na-plugin-core-public.plugininitializer.md | 2 +- .../kibana-plugin-core-server.asyncplugin.md | 27 ++++ ...na-plugin-core-server.asyncplugin.setup.md | 23 +++ ...na-plugin-core-server.asyncplugin.start.md | 23 +++ ...ana-plugin-core-server.asyncplugin.stop.md | 15 ++ .../core/server/kibana-plugin-core-server.md | 1 + .../kibana-plugin-core-server.plugin.setup.md | 4 +- .../kibana-plugin-core-server.plugin.start.md | 4 +- ...na-plugin-core-server.plugininitializer.md | 2 +- packages/kbn-std/src/index.ts | 2 +- packages/kbn-std/src/promise.test.ts | 29 +++- packages/kbn-std/src/promise.ts | 4 + .../kbn-test/src/functional_tests/tasks.js | 6 + src/core/public/index.ts | 9 +- src/core/public/mocks.ts | 8 +- src/core/public/plugins/index.ts | 2 +- src/core/public/plugins/plugin.test.ts | 12 +- src/core/public/plugins/plugin.ts | 57 +++++-- .../plugins/plugins_service.test.mocks.ts | 7 +- .../public/plugins/plugins_service.test.ts | 144 +++++++++++++++-- src/core/public/plugins/plugins_service.ts | 63 +++++--- src/core/public/public.api.md | 16 +- src/core/server/index.ts | 1 + .../integration_tests/plugins_service.test.ts | 4 +- src/core/server/plugins/plugin.test.ts | 36 ++--- src/core/server/plugins/plugin.ts | 27 +++- .../server/plugins/plugins_system.test.ts | 152 +++++++++++++++--- src/core/server/plugins/plugins_system.ts | 56 +++++-- src/core/server/plugins/types.ts | 23 ++- src/core/server/server.api.md | 24 ++- src/plugins/apm_oss/server/plugin.ts | 7 +- src/plugins/console/server/plugin.ts | 7 +- src/plugins/inspector/public/plugin.tsx | 2 +- src/plugins/legacy_export/server/plugin.ts | 7 +- src/plugins/maps_legacy/server/index.ts | 8 +- .../presentation_util/public/plugin.ts | 4 +- src/plugins/region_map/public/plugin.ts | 2 +- .../saved_objects_management/server/plugin.ts | 4 +- src/plugins/share/server/plugin.ts | 2 +- src/plugins/tile_map/public/plugin.ts | 2 +- src/plugins/usage_collection/server/plugin.ts | 12 +- src/plugins/vis_type_table/public/plugin.ts | 5 +- src/plugins/vis_type_timelion/server/index.ts | 4 +- .../vis_type_timelion/server/plugin.ts | 15 +- .../vis_type_timeseries/public/plugin.ts | 4 +- src/plugins/vis_type_vega/public/plugin.ts | 4 +- src/plugins/vis_type_vislib/public/plugin.ts | 2 +- src/plugins/vis_type_xy/public/plugin.ts | 2 +- src/plugins/visualize/public/plugin.ts | 2 +- .../plugins/app_link_test/public/plugin.ts | 2 +- .../plugins/core_plugin_b/public/plugin.tsx | 2 +- x-pack/plugins/actions/server/plugin.ts | 45 +++--- x-pack/plugins/apm/server/plugin.ts | 7 +- .../plugins/beats_management/server/plugin.ts | 18 ++- x-pack/plugins/canvas/server/plugin.ts | 7 +- x-pack/plugins/case/server/plugin.ts | 9 +- x-pack/plugins/cloud/public/plugin.ts | 2 +- x-pack/plugins/cloud/server/plugin.ts | 17 +- x-pack/plugins/code/server/plugin.ts | 10 +- .../encrypted_saved_objects/server/index.ts | 4 +- .../server/plugin.test.ts | 18 +-- .../encrypted_saved_objects/server/plugin.ts | 23 ++- .../enterprise_search/server/plugin.ts | 12 +- x-pack/plugins/event_log/server/plugin.ts | 22 +-- x-pack/plugins/event_log/server/types.ts | 2 - x-pack/plugins/features/server/index.ts | 4 +- x-pack/plugins/features/server/plugin.test.ts | 12 +- x-pack/plugins/features/server/plugin.ts | 9 +- x-pack/plugins/fleet/public/plugin.ts | 2 +- x-pack/plugins/fleet/server/plugin.ts | 4 +- x-pack/plugins/global_search/server/plugin.ts | 11 +- x-pack/plugins/graph/server/plugin.ts | 2 +- .../server/plugin.ts | 13 +- x-pack/plugins/infra/server/plugin.ts | 18 +-- x-pack/plugins/licensing/public/plugin.ts | 2 +- x-pack/plugins/licensing/server/plugin.ts | 12 +- x-pack/plugins/lists/server/create_config.ts | 18 --- x-pack/plugins/lists/server/plugin.ts | 8 +- x-pack/plugins/maps/server/plugin.ts | 8 +- x-pack/plugins/monitoring/public/plugin.ts | 2 +- x-pack/plugins/monitoring/server/index.ts | 6 +- .../plugins/monitoring/server/plugin.test.ts | 56 +------ x-pack/plugins/monitoring/server/plugin.ts | 19 +-- x-pack/plugins/monitoring/server/types.ts | 4 + x-pack/plugins/observability/server/plugin.ts | 7 +- .../plugins/osquery/server/create_config.ts | 8 +- x-pack/plugins/osquery/server/plugin.ts | 7 +- x-pack/plugins/painless_lab/server/plugin.ts | 2 +- .../plugins/remote_clusters/server/plugin.ts | 11 +- .../plugins/searchprofiler/server/plugin.ts | 2 +- x-pack/plugins/security/server/index.ts | 4 +- x-pack/plugins/security/server/plugin.test.ts | 6 +- x-pack/plugins/security/server/plugin.ts | 10 +- .../security_solution/server/config.ts | 9 +- .../security_solution/server/plugin.ts | 15 +- .../plugins/snapshot_restore/server/plugin.ts | 10 +- x-pack/plugins/spaces/server/index.ts | 4 +- x-pack/plugins/spaces/server/plugin.test.ts | 12 +- x-pack/plugins/spaces/server/plugin.ts | 4 +- x-pack/plugins/stack_alerts/server/plugin.ts | 9 +- .../task_manager/server/plugin.test.ts | 2 +- x-pack/plugins/task_manager/server/plugin.ts | 12 +- .../triggers_actions_ui/server/plugin.ts | 4 +- x-pack/plugins/uptime/public/apps/plugin.ts | 5 +- x-pack/plugins/watcher/server/plugin.ts | 2 +- x-pack/plugins/xpack_legacy/server/plugin.ts | 8 +- 114 files changed, 1000 insertions(+), 550 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.asyncplugin.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.asyncplugin.setup.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.asyncplugin.start.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.asyncplugin.stop.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.asyncplugin.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.asyncplugin.setup.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.asyncplugin.start.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.asyncplugin.stop.md delete mode 100644 x-pack/plugins/lists/server/create_config.ts diff --git a/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc b/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc index a033bbd26a1a78..92a624649d3c50 100644 --- a/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc +++ b/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc @@ -71,22 +71,20 @@ export function plugin(initializerContext: PluginInitializerContext) { *plugins/my_plugin/(public|server)/plugin.ts* [source,typescript] ---- -import type { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { CoreSetup, Logger, Plugin, PluginInitializerContext, PluginName } from 'kibana/server'; import type { MyPluginConfig } from './config'; export class MyPlugin implements Plugin { - private readonly config$: Observable; + private readonly config: MyPluginConfig; private readonly log: Logger; constructor(private readonly initializerContext: PluginInitializerContext) { this.log = initializerContext.logger.get(); - this.config$ = initializerContext.config.create(); + this.config = initializerContext.config.get(); } - public async setup(core: CoreSetup, deps: Record) { - const isEnabled = await this.config$.pipe(first()).toPromise(); + public setup(core: CoreSetup, deps: Record) { + const { someConfigValue } = this.config; } } ---- @@ -96,7 +94,7 @@ Additionally, some plugins need to access the runtime env configuration. [source,typescript] ---- export class MyPlugin implements Plugin { - public async setup(core: CoreSetup, deps: Record) { + public setup(core: CoreSetup, deps: Record) { const { mode: { dev }, packageInfo: { version } } = this.initializerContext.env } ---- diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.md new file mode 100644 index 00000000000000..cf315e1fd337e3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) + +## AsyncPlugin interface + +> Warning: This API is now obsolete. +> +> Asynchronous lifecycles are deprecated, and should be migrated to sync [plugin](./kibana-plugin-core-public.plugin.md) +> + +A plugin with asynchronous lifecycle methods. + +Signature: + +```typescript +export interface AsyncPlugin +``` + +## Methods + +| Method | Description | +| --- | --- | +| [setup(core, plugins)](./kibana-plugin-core-public.asyncplugin.setup.md) | | +| [start(core, plugins)](./kibana-plugin-core-public.asyncplugin.start.md) | | +| [stop()](./kibana-plugin-core-public.asyncplugin.stop.md) | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.setup.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.setup.md new file mode 100644 index 00000000000000..54507b44cdd72a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.setup.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) > [setup](./kibana-plugin-core-public.asyncplugin.setup.md) + +## AsyncPlugin.setup() method + +Signature: + +```typescript +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreSetup<TPluginsStart, TStart> | | +| plugins | TPluginsSetup | | + +Returns: + +`TSetup | Promise` + diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.start.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.start.md new file mode 100644 index 00000000000000..f16d3c46bf8499 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.start.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) > [start](./kibana-plugin-core-public.asyncplugin.start.md) + +## AsyncPlugin.start() method + +Signature: + +```typescript +start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreStart | | +| plugins | TPluginsStart | | + +Returns: + +`TStart | Promise` + diff --git a/docs/development/core/public/kibana-plugin-core-public.asyncplugin.stop.md b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.stop.md new file mode 100644 index 00000000000000..f809f75783c26c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.asyncplugin.stop.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) > [stop](./kibana-plugin-core-public.asyncplugin.stop.md) + +## AsyncPlugin.stop() method + +Signature: + +```typescript +stop?(): void; +``` +Returns: + +`void` + diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index efd499823ffadc..e307b5c9971b0b 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -39,6 +39,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ApplicationStart](./kibana-plugin-core-public.applicationstart.md) | | | [AppMeta](./kibana-plugin-core-public.appmeta.md) | Input type for meta data for an application.Meta fields include keywords and searchDeepLinks Keywords is an array of string with which to associate the app, must include at least one unique string as an array. searchDeepLinks is an array of links that represent secondary in-app locations for the app. | | [AppMountParameters](./kibana-plugin-core-public.appmountparameters.md) | | +| [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) | A plugin with asynchronous lifecycle methods. | | [Capabilities](./kibana-plugin-core-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. | | [ChromeBadge](./kibana-plugin-core-public.chromebadge.md) | | | [ChromeBrand](./kibana-plugin-core-public.chromebrand.md) | | diff --git a/docs/development/core/public/kibana-plugin-core-public.plugin.setup.md b/docs/development/core/public/kibana-plugin-core-public.plugin.setup.md index 7fa05588a33012..232851cd342cee 100644 --- a/docs/development/core/public/kibana-plugin-core-public.plugin.setup.md +++ b/docs/development/core/public/kibana-plugin-core-public.plugin.setup.md @@ -7,7 +7,7 @@ Signature: ```typescript -setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; ``` ## Parameters @@ -19,5 +19,5 @@ setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Returns: -`TSetup | Promise` +`TSetup` diff --git a/docs/development/core/public/kibana-plugin-core-public.plugin.start.md b/docs/development/core/public/kibana-plugin-core-public.plugin.start.md index 0d3c19a8217a67..ec5ed211a9d2ba 100644 --- a/docs/development/core/public/kibana-plugin-core-public.plugin.start.md +++ b/docs/development/core/public/kibana-plugin-core-public.plugin.start.md @@ -7,7 +7,7 @@ Signature: ```typescript -start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; +start(core: CoreStart, plugins: TPluginsStart): TStart; ``` ## Parameters @@ -19,5 +19,5 @@ start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; Returns: -`TStart | Promise` +`TStart` diff --git a/docs/development/core/public/kibana-plugin-core-public.plugininitializer.md b/docs/development/core/public/kibana-plugin-core-public.plugininitializer.md index 1fcc2999dfd2ed..b7c3e11e492bd7 100644 --- a/docs/development/core/public/kibana-plugin-core-public.plugininitializer.md +++ b/docs/development/core/public/kibana-plugin-core-public.plugininitializer.md @@ -9,5 +9,5 @@ The `plugin` export at the root of a plugin's `public` directory should conform Signature: ```typescript -export declare type PluginInitializer = (core: PluginInitializerContext) => Plugin; +export declare type PluginInitializer = (core: PluginInitializerContext) => Plugin | AsyncPlugin; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.md new file mode 100644 index 00000000000000..1ad1d87220b748 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) + +## AsyncPlugin interface + +> Warning: This API is now obsolete. +> +> Asynchronous lifecycles are deprecated, and should be migrated to sync [plugin](./kibana-plugin-core-server.plugin.md) +> + +A plugin with asynchronous lifecycle methods. + +Signature: + +```typescript +export interface AsyncPlugin +``` + +## Methods + +| Method | Description | +| --- | --- | +| [setup(core, plugins)](./kibana-plugin-core-server.asyncplugin.setup.md) | | +| [start(core, plugins)](./kibana-plugin-core-server.asyncplugin.start.md) | | +| [stop()](./kibana-plugin-core-server.asyncplugin.stop.md) | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.setup.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.setup.md new file mode 100644 index 00000000000000..1d033b7b88b051 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.setup.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) > [setup](./kibana-plugin-core-server.asyncplugin.setup.md) + +## AsyncPlugin.setup() method + +Signature: + +```typescript +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreSetup | | +| plugins | TPluginsSetup | | + +Returns: + +`TSetup | Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.start.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.start.md new file mode 100644 index 00000000000000..3cce90f01603bb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.start.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) > [start](./kibana-plugin-core-server.asyncplugin.start.md) + +## AsyncPlugin.start() method + +Signature: + +```typescript +start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreStart | | +| plugins | TPluginsStart | | + +Returns: + +`TStart | Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.asyncplugin.stop.md b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.stop.md new file mode 100644 index 00000000000000..9272fc2c4eba06 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.asyncplugin.stop.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) > [stop](./kibana-plugin-core-server.asyncplugin.stop.md) + +## AsyncPlugin.stop() method + +Signature: + +```typescript +stop?(): void; +``` +Returns: + +`void` + diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 82f4a285409c95..5fe5eda7a81729 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -49,6 +49,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AppCategory](./kibana-plugin-core-server.appcategory.md) | A category definition for nav links to know where to sort them in the left hand nav | | [AssistanceAPIResponse](./kibana-plugin-core-server.assistanceapiresponse.md) | | | [AssistantAPIClientParams](./kibana-plugin-core-server.assistantapiclientparams.md) | | +| [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) | A plugin with asynchronous lifecycle methods. | | [Authenticated](./kibana-plugin-core-server.authenticated.md) | | | [AuthNotHandled](./kibana-plugin-core-server.authnothandled.md) | | | [AuthRedirected](./kibana-plugin-core-server.authredirected.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.plugin.setup.md b/docs/development/core/server/kibana-plugin-core-server.plugin.setup.md index b4e6623098736d..a8b0aae28d251b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugin.setup.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugin.setup.md @@ -7,7 +7,7 @@ Signature: ```typescript -setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; ``` ## Parameters @@ -19,5 +19,5 @@ setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; Returns: -`TSetup | Promise` +`TSetup` diff --git a/docs/development/core/server/kibana-plugin-core-server.plugin.start.md b/docs/development/core/server/kibana-plugin-core-server.plugin.start.md index 03e889a018b6f0..851f84474fe11f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugin.start.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugin.start.md @@ -7,7 +7,7 @@ Signature: ```typescript -start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; +start(core: CoreStart, plugins: TPluginsStart): TStart; ``` ## Parameters @@ -19,5 +19,5 @@ start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; Returns: -`TStart | Promise` +`TStart` diff --git a/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md b/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md index 839eabff29a189..fe55e131065ddd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md +++ b/docs/development/core/server/kibana-plugin-core-server.plugininitializer.md @@ -9,5 +9,5 @@ The `plugin` export at the root of a plugin's `server` directory should conform Signature: ```typescript -export declare type PluginInitializer = (core: PluginInitializerContext) => Plugin; +export declare type PluginInitializer = (core: PluginInitializerContext) => Plugin | AsyncPlugin; ``` diff --git a/packages/kbn-std/src/index.ts b/packages/kbn-std/src/index.ts index f3d9e0f77fa19a..d79594c97cec78 100644 --- a/packages/kbn-std/src/index.ts +++ b/packages/kbn-std/src/index.ts @@ -12,7 +12,7 @@ export { get } from './get'; export { mapToObject } from './map_to_object'; export { merge } from './merge'; export { pick } from './pick'; -export { withTimeout } from './promise'; +export { withTimeout, isPromise } from './promise'; export { isRelativeUrl, modifyUrl, getUrlOrigin, URLMeaningfulParts } from './url'; export { unset } from './unset'; export { getFlattenedObject } from './get_flattened_object'; diff --git a/packages/kbn-std/src/promise.test.ts b/packages/kbn-std/src/promise.test.ts index 61197a2a8bf70a..f7c119acd0c7a4 100644 --- a/packages/kbn-std/src/promise.test.ts +++ b/packages/kbn-std/src/promise.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { withTimeout } from './promise'; +import { withTimeout, isPromise } from './promise'; const delay = (ms: number, resolveValue?: any) => new Promise((resolve) => setTimeout(resolve, ms, resolveValue)); @@ -50,3 +50,30 @@ describe('withTimeout', () => { ).rejects.toMatchInlineSnapshot(`[Error: from-promise]`); }); }); + +describe('isPromise', () => { + it('returns true when arg is a Promise', () => { + expect(isPromise(Promise.resolve('foo'))).toEqual(true); + expect(isPromise(Promise.reject('foo').catch(() => undefined))).toEqual(true); + }); + + it('returns false when arg is not a Promise', () => { + expect(isPromise(12)).toEqual(false); + expect(isPromise('foo')).toEqual(false); + expect(isPromise({ hello: 'dolly' })).toEqual(false); + expect(isPromise([1, 2, 3])).toEqual(false); + }); + + it('returns false for objects with a non-function `then` property', () => { + expect(isPromise({ then: 'bar' })).toEqual(false); + }); + + it('returns false for null and undefined', () => { + expect(isPromise(null)).toEqual(false); + expect(isPromise(undefined)).toEqual(false); + }); + + it('returns true for Promise-Like objects', () => { + expect(isPromise({ then: () => 12 })).toEqual(true); + }); +}); diff --git a/packages/kbn-std/src/promise.ts b/packages/kbn-std/src/promise.ts index ce4e50bf9b2ac7..9d8f7703c026dc 100644 --- a/packages/kbn-std/src/promise.ts +++ b/packages/kbn-std/src/promise.ts @@ -20,3 +20,7 @@ export function withTimeout({ new Promise((resolve, reject) => setTimeout(() => reject(new Error(errorMessage)), timeout)), ]) as Promise; } + +export function isPromise(maybePromise: T | Promise): maybePromise is Promise { + return maybePromise ? typeof (maybePromise as Promise).then === 'function' : false; +} diff --git a/packages/kbn-test/src/functional_tests/tasks.js b/packages/kbn-test/src/functional_tests/tasks.js index 099963545a2dc1..02c55b6af91dcc 100644 --- a/packages/kbn-test/src/functional_tests/tasks.js +++ b/packages/kbn-test/src/functional_tests/tasks.js @@ -95,6 +95,8 @@ export async function runTests(options) { try { es = await runElasticsearch({ config, options: opts }); await runKibanaServer({ procs, config, options: opts }); + // workaround until https://github.com/elastic/kibana/issues/89828 is addressed + await delay(5000); await runFtr({ configPath, options: opts }); } finally { try { @@ -160,3 +162,7 @@ async function silence(log, milliseconds) { ) .toPromise(); } + +async function delay(ms) { + await new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/src/core/public/index.ts b/src/core/public/index.ts index afa129adc061f2..a1cb036ce38f8f 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -53,7 +53,13 @@ import { HttpSetup, HttpStart } from './http'; import { I18nStart } from './i18n'; import { NotificationsSetup, NotificationsStart } from './notifications'; import { OverlayStart } from './overlays'; -import { Plugin, PluginInitializer, PluginInitializerContext, PluginOpaqueId } from './plugins'; +import { + Plugin, + AsyncPlugin, + PluginInitializer, + PluginInitializerContext, + PluginOpaqueId, +} from './plugins'; import { UiSettingsState, IUiSettingsClient } from './ui_settings'; import { ApplicationSetup, Capabilities, ApplicationStart } from './application'; import { DocLinksStart } from './doc_links'; @@ -304,6 +310,7 @@ export { NotificationsSetup, NotificationsStart, Plugin, + AsyncPlugin, PluginInitializer, PluginInitializerContext, SavedObjectsStart, diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index d208ea76c48fe3..e47de84ea12b2f 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -110,14 +110,14 @@ function pluginInitializerContextMock(config: any = {}) { return mock; } -function createCoreContext(): CoreContext { +function createCoreContext({ production = false }: { production?: boolean } = {}): CoreContext { return { coreId: Symbol('core context mock'), env: { mode: { - dev: true, - name: 'development', - prod: false, + dev: !production, + name: production ? 'production' : 'development', + prod: production, }, packageInfo: { version: 'version', diff --git a/src/core/public/plugins/index.ts b/src/core/public/plugins/index.ts index 76811d4908d22a..be805c6a521ce8 100644 --- a/src/core/public/plugins/index.ts +++ b/src/core/public/plugins/index.ts @@ -7,6 +7,6 @@ */ export * from './plugins_service'; -export { Plugin, PluginInitializer } from './plugin'; +export { Plugin, AsyncPlugin, PluginInitializer } from './plugin'; export { PluginInitializerContext } from './plugin_context'; export { PluginOpaqueId } from '../../server/types'; diff --git a/src/core/public/plugins/plugin.test.ts b/src/core/public/plugins/plugin.test.ts index e8e930a5befca6..ef919018f120b3 100644 --- a/src/core/public/plugins/plugin.test.ts +++ b/src/core/public/plugins/plugin.test.ts @@ -39,16 +39,16 @@ beforeEach(() => { }); describe('PluginWrapper', () => { - test('`setup` fails if plugin.setup is not a function', async () => { + test('`setup` fails if plugin.setup is not a function', () => { mockInitializer.mockReturnValueOnce({ start: jest.fn() } as any); - await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( + expect(() => plugin.setup({} as any, {} as any)).toThrowErrorMatchingInlineSnapshot( `"Instance of plugin \\"plugin-a\\" does not define \\"setup\\" function."` ); }); - test('`setup` fails if plugin.start is not a function', async () => { + test('`setup` fails if plugin.start is not a function', () => { mockInitializer.mockReturnValueOnce({ setup: jest.fn() } as any); - await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( + expect(() => plugin.setup({} as any, {} as any)).toThrowErrorMatchingInlineSnapshot( `"Instance of plugin \\"plugin-a\\" does not define \\"start\\" function."` ); }); @@ -65,8 +65,8 @@ describe('PluginWrapper', () => { expect(mockPlugin.setup).toHaveBeenCalledWith(context, deps); }); - test('`start` fails if setup is not called first', async () => { - await expect(plugin.start({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( + test('`start` fails if setup is not called first', () => { + expect(() => plugin.start({} as any, {} as any)).toThrowErrorMatchingInlineSnapshot( `"Plugin \\"plugin-a\\" can't be started since it isn't set up."` ); }); diff --git a/src/core/public/plugins/plugin.ts b/src/core/public/plugins/plugin.ts index af95e831a64721..a08a6cf0b431a0 100644 --- a/src/core/public/plugins/plugin.ts +++ b/src/core/public/plugins/plugin.ts @@ -8,6 +8,7 @@ import { Subject } from 'rxjs'; import { first } from 'rxjs/operators'; +import { isPromise } from '@kbn/std'; import { DiscoveredPlugin, PluginOpaqueId } from '../../server'; import { PluginInitializerContext } from './plugin_context'; import { read } from './plugin_reader'; @@ -23,6 +24,23 @@ export interface Plugin< TStart = void, TPluginsSetup extends object = object, TPluginsStart extends object = object +> { + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; + start(core: CoreStart, plugins: TPluginsStart): TStart; + stop?(): void; +} + +/** + * A plugin with asynchronous lifecycle methods. + * + * @deprecated Asynchronous lifecycles are deprecated, and should be migrated to sync {@link Plugin | plugin} + * @public + */ +export interface AsyncPlugin< + TSetup = void, + TStart = void, + TPluginsSetup extends object = object, + TPluginsStart extends object = object > { setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; @@ -40,7 +58,11 @@ export type PluginInitializer< TStart, TPluginsSetup extends object = object, TPluginsStart extends object = object -> = (core: PluginInitializerContext) => Plugin; +> = ( + core: PluginInitializerContext +) => + | Plugin + | AsyncPlugin; /** * Lightweight wrapper around discovered plugin that is responsible for instantiating @@ -58,7 +80,9 @@ export class PluginWrapper< public readonly configPath: DiscoveredPlugin['configPath']; public readonly requiredPlugins: DiscoveredPlugin['requiredPlugins']; public readonly optionalPlugins: DiscoveredPlugin['optionalPlugins']; - private instance?: Plugin; + private instance?: + | Plugin + | AsyncPlugin; private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart, TStart]>(); public readonly startDependencies = this.startDependencies$.pipe(first()).toPromise(); @@ -81,10 +105,12 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `setup` function. */ - public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) { - this.instance = await this.createPluginInstance(); - - return await this.instance.setup(setupContext, plugins); + public setup( + setupContext: CoreSetup, + plugins: TPluginsSetup + ): TSetup | Promise { + this.instance = this.createPluginInstance(); + return this.instance.setup(setupContext, plugins); } /** @@ -94,16 +120,21 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `start` function. */ - public async start(startContext: CoreStart, plugins: TPluginsStart) { + public start(startContext: CoreStart, plugins: TPluginsStart) { if (this.instance === undefined) { throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`); } - const startContract = await this.instance.start(startContext, plugins); - - this.startDependencies$.next([startContext, plugins, startContract]); - - return startContract; + const startContract = this.instance.start(startContext, plugins); + if (isPromise(startContract)) { + return startContract.then((resolvedContract) => { + this.startDependencies$.next([startContext, plugins, resolvedContract]); + return resolvedContract; + }); + } else { + this.startDependencies$.next([startContext, plugins, startContract]); + return startContract; + } } /** @@ -121,7 +152,7 @@ export class PluginWrapper< this.instance = undefined; } - private async createPluginInstance() { + private createPluginInstance() { const initializer = read(this.name) as PluginInitializer< TSetup, TStart, diff --git a/src/core/public/plugins/plugins_service.test.mocks.ts b/src/core/public/plugins/plugins_service.test.mocks.ts index d44657f9039a35..1f85482569dbc2 100644 --- a/src/core/public/plugins/plugins_service.test.mocks.ts +++ b/src/core/public/plugins/plugins_service.test.mocks.ts @@ -7,9 +7,12 @@ */ import { PluginName } from 'kibana/server'; -import { Plugin } from './plugin'; +import { Plugin, AsyncPlugin } from './plugin'; -export type MockedPluginInitializer = jest.Mock>, any>; +export type MockedPluginInitializer = jest.Mock< + Plugin | AsyncPlugin, + any +>; export const mockPluginInitializerProvider: jest.Mock< MockedPluginInitializer, diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index a22d48c50247a4..e70b78f237d757 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -146,16 +146,16 @@ describe('PluginsService', () => { it('returns dependency tree of symbols', () => { const pluginsService = new PluginsService(mockCoreContext, plugins); expect(pluginsService.getOpaqueIds()).toMatchInlineSnapshot(` - Map { - Symbol(pluginA) => Array [], - Symbol(pluginB) => Array [ - Symbol(pluginA), - ], - Symbol(pluginC) => Array [ - Symbol(pluginA), - ], - } - `); + Map { + Symbol(pluginA) => Array [], + Symbol(pluginB) => Array [ + Symbol(pluginA), + ], + Symbol(pluginC) => Array [ + Symbol(pluginA), + ], + } + `); }); }); @@ -264,7 +264,7 @@ describe('PluginsService', () => { jest.runAllTimers(); // setup plugins await expect(promise).rejects.toMatchInlineSnapshot( - `[Error: Setup lifecycle of "pluginA" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + `[Error: Setup lifecycle of "pluginA" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.]` ); }); }); @@ -344,7 +344,7 @@ describe('PluginsService', () => { jest.runAllTimers(); await expect(promise).rejects.toMatchInlineSnapshot( - `[Error: Start lifecycle of "pluginA" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + `[Error: Start lifecycle of "pluginA" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.]` ); }); }); @@ -366,4 +366,124 @@ describe('PluginsService', () => { expect(pluginCInstance.stop).toHaveBeenCalled(); }); }); + + describe('asynchronous plugins', () => { + let consoleSpy: jest.SpyInstance; + + beforeEach(() => { + consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined); + }); + + afterEach(() => { + consoleSpy.mockRestore(); + }); + + const runScenario = async ({ + production, + asyncSetup, + asyncStart, + }: { + production: boolean; + asyncSetup: boolean; + asyncStart: boolean; + }) => { + const coreContext = coreMock.createCoreContext({ production }); + + const syncPlugin = { id: 'sync-plugin', plugin: createManifest('sync-plugin') }; + mockPluginInitializers.set( + 'sync-plugin', + jest.fn(() => ({ + setup: jest.fn(() => 'setup-sync'), + start: jest.fn(() => 'start-sync'), + stop: jest.fn(), + })) + ); + + const asyncPlugin = { id: 'async-plugin', plugin: createManifest('async-plugin') }; + mockPluginInitializers.set( + 'async-plugin', + jest.fn(() => ({ + setup: jest.fn(() => (asyncSetup ? Promise.resolve('setup-async') : 'setup-sync')), + start: jest.fn(() => (asyncStart ? Promise.resolve('start-async') : 'start-sync')), + stop: jest.fn(), + })) + ); + + const pluginsService = new PluginsService(coreContext, [syncPlugin, asyncPlugin]); + + await pluginsService.setup(mockSetupDeps); + await pluginsService.start(mockStartDeps); + }; + + it('logs a warning if a plugin returns a promise from its setup contract in dev mode', async () => { + await runScenario({ + production: false, + asyncSetup: true, + asyncStart: false, + }); + + expect(consoleSpy.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + + it('does not log warnings if a plugin returns a promise from its setup contract in prod mode', async () => { + await runScenario({ + production: true, + asyncSetup: true, + asyncStart: false, + }); + + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + it('logs a warning if a plugin returns a promise from its start contract in dev mode', async () => { + await runScenario({ + production: false, + asyncSetup: false, + asyncStart: true, + }); + + expect(consoleSpy.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + + it('does not log warnings if a plugin returns a promise from its start contract in prod mode', async () => { + await runScenario({ + production: true, + asyncSetup: false, + asyncStart: true, + }); + + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + it('logs multiple warnings if both `setup` and `start` return promises', async () => { + await runScenario({ + production: false, + asyncSetup: true, + asyncStart: true, + }); + + expect(consoleSpy.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + Array [ + "Plugin async-plugin is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + }); }); diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index 7a10ce1cdfc772..57fbe4cbecd12f 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { withTimeout } from '@kbn/std'; +import { withTimeout, isPromise } from '@kbn/std'; import { PluginName, PluginOpaqueId } from '../../server'; import { CoreService } from '../../types'; import { CoreContext } from '../core_system'; @@ -98,16 +98,29 @@ export class PluginsService implements CoreService ); - const contract = await withTimeout({ - promise: plugin.setup( - createPluginSetupContext(this.coreContext, deps, plugin), - pluginDepContracts - ), - timeout: 30 * Sec, - errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, - }); - contracts.set(pluginName, contract); + let contract: unknown; + const contractOrPromise = plugin.setup( + createPluginSetupContext(this.coreContext, deps, plugin), + pluginDepContracts + ); + if (isPromise(contractOrPromise)) { + if (this.coreContext.env.mode.dev) { + // eslint-disable-next-line no-console + console.log( + `Plugin ${pluginName} is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.` + ); + } + + contract = await withTimeout({ + promise: contractOrPromise, + timeout: 10 * Sec, + errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + }); + } else { + contract = contractOrPromise; + } + contracts.set(pluginName, contract); this.satupPlugins.push(pluginName); } @@ -132,14 +145,28 @@ export class PluginsService implements CoreService ); - const contract = await withTimeout({ - promise: plugin.start( - createPluginStartContext(this.coreContext, deps, plugin), - pluginDepContracts - ), - timeout: 30 * Sec, - errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, - }); + let contract: unknown; + const contractOrPromise = plugin.start( + createPluginStartContext(this.coreContext, deps, plugin), + pluginDepContracts + ); + if (isPromise(contractOrPromise)) { + if (this.coreContext.env.mode.dev) { + // eslint-disable-next-line no-console + console.log( + `Plugin ${pluginName} is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.` + ); + } + + contract = await withTimeout({ + promise: contractOrPromise, + timeout: 10 * Sec, + errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + }); + } else { + contract = contractOrPromise; + } + contracts.set(pluginName, contract); } diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 75ed9aa5f150f6..99579ada8ec588 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -194,6 +194,16 @@ export type AppUpdatableFields = Pick Partial | undefined; +// @public @deprecated +export interface AsyncPlugin { + // (undocumented) + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + // (undocumented) + start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + // (undocumented) + stop?(): void; +} + // @public export interface Capabilities { [key: string]: Record>; @@ -990,15 +1000,15 @@ export { PackageInfo } // @public export interface Plugin { // (undocumented) - setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; // (undocumented) - start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + start(core: CoreStart, plugins: TPluginsStart): TStart; // (undocumented) stop?(): void; } // @public -export type PluginInitializer = (core: PluginInitializerContext) => Plugin; +export type PluginInitializer = (core: PluginInitializerContext) => Plugin | AsyncPlugin; // @public export interface PluginInitializerContext { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 382a694bd2e418..6f478004c204ef 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -235,6 +235,7 @@ export { export { DiscoveredPlugin, Plugin, + AsyncPlugin, PluginConfigDescriptor, PluginConfigSchema, PluginInitializer, diff --git a/src/core/server/plugins/integration_tests/plugins_service.test.ts b/src/core/server/plugins/integration_tests/plugins_service.test.ts index dda947972737a6..a29fb01fbc0092 100644 --- a/src/core/server/plugins/integration_tests/plugins_service.test.ts +++ b/src/core/server/plugins/integration_tests/plugins_service.test.ts @@ -20,7 +20,7 @@ import { config } from '../plugins_config'; import { loggingSystemMock } from '../../logging/logging_system.mock'; import { environmentServiceMock } from '../../environment/environment_service.mock'; import { coreMock } from '../../mocks'; -import { Plugin } from '../types'; +import { AsyncPlugin } from '../types'; import { PluginWrapper } from '../plugin'; describe('PluginsService', () => { @@ -138,7 +138,7 @@ describe('PluginsService', () => { expect(startDependenciesResolved).toBe(false); return pluginStartContract; }, - } as Plugin); + } as AsyncPlugin); jest.doMock( join(pluginPath, 'server'), diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts index 68fdfdf62c30b0..c90d2e804225c7 100644 --- a/src/core/server/plugins/plugin.test.ts +++ b/src/core/server/plugins/plugin.test.ts @@ -100,7 +100,7 @@ test('`constructor` correctly initializes plugin instance', () => { expect(plugin.optionalPlugins).toEqual(['some-optional-dep']); }); -test('`setup` fails if `plugin` initializer is not exported', async () => { +test('`setup` fails if `plugin` initializer is not exported', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -115,14 +115,14 @@ test('`setup` fails if `plugin` initializer is not exported', async () => { ), }); - await expect( + expect(() => plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) - ).rejects.toMatchInlineSnapshot( - `[Error: Plugin "some-plugin-id" does not export "plugin" definition (plugin-without-initializer-path).]` + ).toThrowErrorMatchingInlineSnapshot( + `"Plugin \\"some-plugin-id\\" does not export \\"plugin\\" definition (plugin-without-initializer-path)."` ); }); -test('`setup` fails if plugin initializer is not a function', async () => { +test('`setup` fails if plugin initializer is not a function', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -137,14 +137,14 @@ test('`setup` fails if plugin initializer is not a function', async () => { ), }); - await expect( + expect(() => plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) - ).rejects.toMatchInlineSnapshot( - `[Error: Definition of plugin "some-plugin-id" should be a function (plugin-with-wrong-initializer-path).]` + ).toThrowErrorMatchingInlineSnapshot( + `"Definition of plugin \\"some-plugin-id\\" should be a function (plugin-with-wrong-initializer-path)."` ); }); -test('`setup` fails if initializer does not return object', async () => { +test('`setup` fails if initializer does not return object', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -161,14 +161,14 @@ test('`setup` fails if initializer does not return object', async () => { mockPluginInitializer.mockReturnValue(null); - await expect( + expect(() => plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) - ).rejects.toMatchInlineSnapshot( - `[Error: Initializer for plugin "some-plugin-id" is expected to return plugin instance, but returned "null".]` + ).toThrowErrorMatchingInlineSnapshot( + `"Initializer for plugin \\"some-plugin-id\\" is expected to return plugin instance, but returned \\"null\\"."` ); }); -test('`setup` fails if object returned from initializer does not define `setup` function', async () => { +test('`setup` fails if object returned from initializer does not define `setup` function', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -186,10 +186,10 @@ test('`setup` fails if object returned from initializer does not define `setup` const mockPluginInstance = { run: jest.fn() }; mockPluginInitializer.mockReturnValue(mockPluginInstance); - await expect( + expect(() => plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) - ).rejects.toMatchInlineSnapshot( - `[Error: Instance of plugin "some-plugin-id" does not define "setup" function.]` + ).toThrowErrorMatchingInlineSnapshot( + `"Instance of plugin \\"some-plugin-id\\" does not define \\"setup\\" function."` ); }); @@ -223,7 +223,7 @@ test('`setup` initializes plugin and calls appropriate lifecycle hook', async () expect(mockPluginInstance.setup).toHaveBeenCalledWith(setupContext, setupDependencies); }); -test('`start` fails if setup is not called first', async () => { +test('`start` fails if setup is not called first', () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); const plugin = new PluginWrapper({ @@ -238,7 +238,7 @@ test('`start` fails if setup is not called first', async () => { ), }); - await expect(plugin.start({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( + expect(() => plugin.start({} as any, {} as any)).toThrowErrorMatchingInlineSnapshot( `"Plugin \\"some-plugin-id\\" can't be started since it isn't set up."` ); }); diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts index 83b3fb53689a75..ca7f11e28de75f 100644 --- a/src/core/server/plugins/plugin.ts +++ b/src/core/server/plugins/plugin.ts @@ -10,11 +10,13 @@ import { join } from 'path'; import typeDetect from 'type-detect'; import { Subject } from 'rxjs'; import { first } from 'rxjs/operators'; +import { isPromise } from '@kbn/std'; import { isConfigSchema } from '@kbn/config-schema'; import { Logger } from '../logging'; import { Plugin, + AsyncPlugin, PluginInitializerContext, PluginManifest, PluginInitializer, @@ -49,7 +51,9 @@ export class PluginWrapper< private readonly log: Logger; private readonly initializerContext: PluginInitializerContext; - private instance?: Plugin; + private instance?: + | Plugin + | AsyncPlugin; private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart, TStart]>(); public readonly startDependencies = this.startDependencies$.pipe(first()).toPromise(); @@ -83,9 +87,11 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `setup` function. */ - public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) { + public setup( + setupContext: CoreSetup, + plugins: TPluginsSetup + ): TSetup | Promise { this.instance = this.createPluginInstance(); - return this.instance.setup(setupContext, plugins); } @@ -96,14 +102,21 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `start` function. */ - public async start(startContext: CoreStart, plugins: TPluginsStart) { + public start(startContext: CoreStart, plugins: TPluginsStart): TStart | Promise { if (this.instance === undefined) { throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`); } - const startContract = await this.instance.start(startContext, plugins); - this.startDependencies$.next([startContext, plugins, startContract]); - return startContract; + const startContract = this.instance.start(startContext, plugins); + if (isPromise(startContract)) { + return startContract.then((resolvedContract) => { + this.startDependencies$.next([startContext, plugins, resolvedContract]); + return resolvedContract; + }); + } else { + this.startDependencies$.next([startContext, plugins, startContract]); + return startContract; + } } /** diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts index 1b5994c40c0410..5c38deeb5cf6ec 100644 --- a/src/core/server/plugins/plugins_system.test.ts +++ b/src/core/server/plugins/plugins_system.test.ts @@ -25,7 +25,6 @@ import { PluginsSystem } from './plugins_system'; import { coreMock } from '../mocks'; import { Logger } from '../logging'; -const logger = loggingSystemMock.create(); function createPlugin( id: string, { @@ -34,8 +33,8 @@ function createPlugin( server = true, ui = true, }: { required?: string[]; optional?: string[]; server?: boolean; ui?: boolean } = {} -) { - return new PluginWrapper({ +): PluginWrapper { + return new PluginWrapper({ path: 'some-path', manifest: { id, @@ -53,27 +52,27 @@ function createPlugin( }); } +const setupDeps = coreMock.createInternalSetup(); +const startDeps = coreMock.createInternalStart(); + let pluginsSystem: PluginsSystem; -const configService = configServiceMock.create(); -configService.atPath.mockReturnValue(new BehaviorSubject({ initialize: true })); +let configService: ReturnType; +let logger: ReturnType; let env: Env; let coreContext: CoreContext; -const setupDeps = coreMock.createInternalSetup(); -const startDeps = coreMock.createInternalStart(); - beforeEach(() => { + logger = loggingSystemMock.create(); env = Env.createDefault(REPO_ROOT, getEnvOptions()); + configService = configServiceMock.create(); + configService.atPath.mockReturnValue(new BehaviorSubject({ initialize: true })); + coreContext = { coreId: Symbol(), env, logger, configService: configService as any }; pluginsSystem = new PluginsSystem(coreContext); }); -afterEach(() => { - jest.clearAllMocks(); -}); - test('can be setup even without plugins', async () => { const pluginsSetup = await pluginsSystem.setupPlugins(setupDeps); @@ -208,7 +207,7 @@ test('correctly orders plugins and returns exposed values for "setup" and "start start: { 'order-2': 'started-as-2' }, }, ], - ] as Array<[PluginWrapper, Contracts]>); + ] as Array<[PluginWrapper, Contracts]>); const setupContextMap = new Map(); const startContextMap = new Map(); @@ -434,7 +433,7 @@ describe('setup', () => { afterAll(() => { jest.useRealTimers(); }); - it('throws timeout error if "setup" was not completed in 30 sec.', async () => { + it('throws timeout error if "setup" was not completed in 10 sec.', async () => { const plugin: PluginWrapper = createPlugin('timeout-setup'); jest.spyOn(plugin, 'setup').mockImplementation(() => new Promise((i) => i)); pluginsSystem.addPlugin(plugin); @@ -444,7 +443,7 @@ describe('setup', () => { jest.runAllTimers(); await expect(promise).rejects.toMatchInlineSnapshot( - `[Error: Setup lifecycle of "timeout-setup" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + `[Error: Setup lifecycle of "timeout-setup" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.]` ); }); @@ -471,8 +470,8 @@ describe('start', () => { afterAll(() => { jest.useRealTimers(); }); - it('throws timeout error if "start" was not completed in 30 sec.', async () => { - const plugin: PluginWrapper = createPlugin('timeout-start'); + it('throws timeout error if "start" was not completed in 10 sec.', async () => { + const plugin = createPlugin('timeout-start'); jest.spyOn(plugin, 'setup').mockResolvedValue({}); jest.spyOn(plugin, 'start').mockImplementation(() => new Promise((i) => i)); @@ -485,7 +484,7 @@ describe('start', () => { jest.runAllTimers(); await expect(promise).rejects.toMatchInlineSnapshot( - `[Error: Start lifecycle of "timeout-start" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]` + `[Error: Start lifecycle of "timeout-start" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.]` ); }); @@ -505,3 +504,120 @@ describe('start', () => { expect(log.info).toHaveBeenCalledWith(`Starting [2] plugins: [order-1,order-0]`); }); }); + +describe('asynchronous plugins', () => { + const runScenario = async ({ + production, + asyncSetup, + asyncStart, + }: { + production: boolean; + asyncSetup: boolean; + asyncStart: boolean; + }) => { + env = Env.createDefault( + REPO_ROOT, + getEnvOptions({ + cliArgs: { + dev: !production, + envName: production ? 'production' : 'development', + }, + }) + ); + coreContext = { coreId: Symbol(), env, logger, configService: configService as any }; + pluginsSystem = new PluginsSystem(coreContext); + + const syncPlugin = createPlugin('sync-plugin'); + jest.spyOn(syncPlugin, 'setup').mockReturnValue('setup-sync'); + jest.spyOn(syncPlugin, 'start').mockReturnValue('start-sync'); + pluginsSystem.addPlugin(syncPlugin); + + const asyncPlugin = createPlugin('async-plugin'); + jest + .spyOn(asyncPlugin, 'setup') + .mockReturnValue(asyncSetup ? Promise.resolve('setup-async') : 'setup-sync'); + jest + .spyOn(asyncPlugin, 'start') + .mockReturnValue(asyncStart ? Promise.resolve('start-async') : 'start-sync'); + pluginsSystem.addPlugin(asyncPlugin); + + await pluginsSystem.setupPlugins(setupDeps); + await pluginsSystem.startPlugins(startDeps); + }; + + it('logs a warning if a plugin returns a promise from its setup contract in dev mode', async () => { + await runScenario({ + production: false, + asyncSetup: true, + asyncStart: false, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + + it('does not log warnings if a plugin returns a promise from its setup contract in prod mode', async () => { + await runScenario({ + production: true, + asyncSetup: true, + asyncStart: false, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn).not.toHaveBeenCalled(); + }); + + it('logs a warning if a plugin returns a promise from its start contract in dev mode', async () => { + await runScenario({ + production: false, + asyncSetup: false, + asyncStart: true, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); + + it('does not log warnings if a plugin returns a promise from its start contract in prod mode', async () => { + await runScenario({ + production: true, + asyncSetup: false, + asyncStart: true, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn).not.toHaveBeenCalled(); + }); + + it('logs multiple warnings if both `setup` and `start` return promises', async () => { + await runScenario({ + production: false, + asyncSetup: true, + asyncStart: true, + }); + + const log = logger.get.mock.results[0].value as jest.Mocked; + expect(log.warn.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Plugin async-plugin is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + Array [ + "Plugin async-plugin is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.", + ], + ] + `); + }); +}); diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index 1b5e3bbb06e71d..b7b8c297ea5717 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { withTimeout } from '@kbn/std'; +import { withTimeout, isPromise } from '@kbn/std'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { PluginWrapper } from './plugin'; @@ -94,14 +94,25 @@ export class PluginsSystem { return depContracts; }, {} as Record); - const contract = await withTimeout({ - promise: plugin.setup( - createPluginSetupContext(this.coreContext, deps, plugin), - pluginDepContracts - ), - timeout: 30 * Sec, - errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, - }); + let contract: unknown; + const contractOrPromise = plugin.setup( + createPluginSetupContext(this.coreContext, deps, plugin), + pluginDepContracts + ); + if (isPromise(contractOrPromise)) { + if (this.coreContext.env.mode.dev) { + this.log.warn( + `Plugin ${pluginName} is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.` + ); + } + contract = await withTimeout({ + promise: contractOrPromise, + timeout: 10 * Sec, + errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + }); + } else { + contract = contractOrPromise; + } contracts.set(pluginName, contract); this.satupPlugins.push(pluginName); @@ -132,14 +143,25 @@ export class PluginsSystem { return depContracts; }, {} as Record); - const contract = await withTimeout({ - promise: plugin.start( - createPluginStartContext(this.coreContext, deps, plugin), - pluginDepContracts - ), - timeout: 30 * Sec, - errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`, - }); + let contract: unknown; + const contractOrPromise = plugin.start( + createPluginStartContext(this.coreContext, deps, plugin), + pluginDepContracts + ); + if (isPromise(contractOrPromise)) { + if (this.coreContext.env.mode.dev) { + this.log.warn( + `Plugin ${pluginName} is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.` + ); + } + contract = await withTimeout({ + promise: contractOrPromise, + timeout: 10 * Sec, + errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + }); + } else { + contract = contractOrPromise; + } contracts.set(pluginName, contract); } diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 91ccc2dedf272c..45db98201b7587 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -242,6 +242,23 @@ export interface Plugin< TStart = void, TPluginsSetup extends object = object, TPluginsStart extends object = object +> { + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; + start(core: CoreStart, plugins: TPluginsStart): TStart; + stop?(): void; +} + +/** + * A plugin with asynchronous lifecycle methods. + * + * @deprecated Asynchronous lifecycles are deprecated, and should be migrated to sync {@link Plugin | plugin} + * @public + */ +export interface AsyncPlugin< + TSetup = void, + TStart = void, + TPluginsSetup extends object = object, + TPluginsStart extends object = object > { setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; @@ -383,4 +400,8 @@ export type PluginInitializer< TStart, TPluginsSetup extends object = object, TPluginsStart extends object = object -> = (core: PluginInitializerContext) => Plugin; +> = ( + core: PluginInitializerContext +) => + | Plugin + | AsyncPlugin; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index f3191c5625f8d9..09207608908a45 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -203,6 +203,16 @@ export interface AssistantAPIClientParams extends GenericParams { path: '/_migration/assistance'; } +// @public @deprecated +export interface AsyncPlugin { + // (undocumented) + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + // (undocumented) + start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + // (undocumented) + stop?(): void; +} + // @public (undocumented) export interface Authenticated extends AuthResultParams { // (undocumented) @@ -1815,9 +1825,9 @@ export { PackageInfo } // @public export interface Plugin { // (undocumented) - setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; // (undocumented) - start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + start(core: CoreStart, plugins: TPluginsStart): TStart; // (undocumented) stop?(): void; } @@ -1836,7 +1846,7 @@ export interface PluginConfigDescriptor { export type PluginConfigSchema = Type; // @public -export type PluginInitializer = (core: PluginInitializerContext) => Plugin; +export type PluginInitializer = (core: PluginInitializerContext) => Plugin | AsyncPlugin; // @public export interface PluginInitializerContext { @@ -3141,9 +3151,9 @@ export const validBodyOutput: readonly ["data", "stream"]; // Warnings were encountered during analysis: // // src/core/server/http/router/response.ts:306:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:263:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:263:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:371:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" +// src/core/server/plugins/types.ts:280:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:280:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:283:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:388:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" ``` diff --git a/src/plugins/apm_oss/server/plugin.ts b/src/plugins/apm_oss/server/plugin.ts index fc3d105da50247..e504d5f0b9a9fa 100644 --- a/src/plugins/apm_oss/server/plugin.ts +++ b/src/plugins/apm_oss/server/plugin.ts @@ -8,7 +8,6 @@ import { Plugin, CoreSetup, PluginInitializerContext } from 'src/core/server'; import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; import { APMOSSConfig } from './'; import { HomeServerPluginSetup, TutorialProvider } from '../../home/server'; import { tutorialProvider } from './tutorial'; @@ -17,10 +16,10 @@ export class APMOSSPlugin implements Plugin { constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; } - public async setup(core: CoreSetup, plugins: { home: HomeServerPluginSetup }) { + public setup(core: CoreSetup, plugins: { home: HomeServerPluginSetup }) { const config$ = this.initContext.config.create(); - const config = await config$.pipe(take(1)).toPromise(); + const config = this.initContext.config.get(); const apmTutorialProvider = tutorialProvider({ indexPatternTitle: config.indexPattern, @@ -35,6 +34,7 @@ export class APMOSSPlugin implements Plugin { plugins.home.tutorials.registerTutorial(apmTutorialProvider); return { + config, config$, getRegisteredTutorialProvider: () => apmTutorialProvider, }; @@ -45,6 +45,7 @@ export class APMOSSPlugin implements Plugin { } export interface APMOSSPluginSetup { + config: APMOSSConfig; config$: Observable; getRegisteredTutorialProvider(): TutorialProvider; } diff --git a/src/plugins/console/server/plugin.ts b/src/plugins/console/server/plugin.ts index b2f43b315aa9b6..a5f1ca6107600e 100644 --- a/src/plugins/console/server/plugin.ts +++ b/src/plugins/console/server/plugin.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import { first } from 'rxjs/operators'; import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; import { ProxyConfigCollection } from './lib'; @@ -28,7 +27,7 @@ export class ConsoleServerPlugin implements Plugin { this.log = this.ctx.logger.get(); } - async setup({ http, capabilities, getStartServices, elasticsearch }: CoreSetup) { + setup({ http, capabilities, getStartServices, elasticsearch }: CoreSetup) { capabilities.registerProvider(() => ({ dev_tools: { show: true, @@ -36,8 +35,8 @@ export class ConsoleServerPlugin implements Plugin { }, })); - const config = await this.ctx.config.create().pipe(first()).toPromise(); - const globalConfig = await this.ctx.config.legacy.globalConfig$.pipe(first()).toPromise(); + const config = this.ctx.config.get(); + const globalConfig = this.ctx.config.legacy.get(); const proxyPathFilters = config.proxyFilter.map((str: string) => new RegExp(str)); this.esLegacyConfigService.setup(elasticsearch.legacy.config$); diff --git a/src/plugins/inspector/public/plugin.tsx b/src/plugins/inspector/public/plugin.tsx index 6aee8b75757c2e..93ffaa93cd80e9 100644 --- a/src/plugins/inspector/public/plugin.tsx +++ b/src/plugins/inspector/public/plugin.tsx @@ -56,7 +56,7 @@ export class InspectorPublicPlugin implements Plugin { constructor(initializerContext: PluginInitializerContext) {} - public async setup(core: CoreSetup) { + public setup(core: CoreSetup) { this.views = new InspectorViewRegistry(); this.views.register(getRequestsViewDescription()); diff --git a/src/plugins/legacy_export/server/plugin.ts b/src/plugins/legacy_export/server/plugin.ts index 3433d076ee800a..ac38f300bd02b5 100644 --- a/src/plugins/legacy_export/server/plugin.ts +++ b/src/plugins/legacy_export/server/plugin.ts @@ -7,16 +7,13 @@ */ import { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/server'; -import { first } from 'rxjs/operators'; import { registerRoutes } from './routes'; export class LegacyExportPlugin implements Plugin<{}, {}> { constructor(private readonly initContext: PluginInitializerContext) {} - public async setup({ http }: CoreSetup) { - const globalConfig = await this.initContext.config.legacy.globalConfig$ - .pipe(first()) - .toPromise(); + public setup({ http }: CoreSetup) { + const globalConfig = this.initContext.config.legacy.get(); const router = http.createRouter(); registerRoutes( diff --git a/src/plugins/maps_legacy/server/index.ts b/src/plugins/maps_legacy/server/index.ts index 00d51da501834c..4f35c1c1e5fc1b 100644 --- a/src/plugins/maps_legacy/server/index.ts +++ b/src/plugins/maps_legacy/server/index.ts @@ -8,7 +8,6 @@ import { Plugin, PluginConfigDescriptor } from 'kibana/server'; import { CoreSetup, PluginInitializerContext } from 'src/core/server'; -import { Observable } from 'rxjs'; import { configSchema, MapsLegacyConfig } from '../config'; import { getUiSettings } from './ui_settings'; @@ -30,7 +29,7 @@ export const config: PluginConfigDescriptor = { }; export interface MapsLegacyPluginSetup { - config$: Observable; + config: MapsLegacyConfig; } export class MapsLegacyPlugin implements Plugin { @@ -43,10 +42,9 @@ export class MapsLegacyPlugin implements Plugin { public setup(core: CoreSetup) { core.uiSettings.register(getUiSettings()); - // @ts-ignore - const config$ = this._initializerContext.config.create(); + const pluginConfig = this._initializerContext.config.get(); return { - config$, + config: pluginConfig, }; } diff --git a/src/plugins/presentation_util/public/plugin.ts b/src/plugins/presentation_util/public/plugin.ts index 15efbf38e7b93a..6f74198bb56ab7 100644 --- a/src/plugins/presentation_util/public/plugin.ts +++ b/src/plugins/presentation_util/public/plugin.ts @@ -31,10 +31,10 @@ export class PresentationUtilPlugin return {}; } - public async start( + public start( coreStart: CoreStart, startPlugins: PresentationUtilPluginStartDeps - ): Promise { + ): PresentationUtilPluginStart { pluginServices.setRegistry(registry.start({ coreStart, startPlugins })); return { diff --git a/src/plugins/region_map/public/plugin.ts b/src/plugins/region_map/public/plugin.ts index d5d57da400a519..a3a2331cf8f76f 100644 --- a/src/plugins/region_map/public/plugin.ts +++ b/src/plugins/region_map/public/plugin.ts @@ -79,7 +79,7 @@ export class RegionMapPlugin implements Plugin { this.logger = this.initializerContext.logger.get(); } - public async setup(core: CoreSetup) { - const config = await this.initializerContext.config - .create() - .pipe(first()) - .toPromise(); + public setup(core: CoreSetup) { + const config = this.initializerContext.config.get(); const collectorSet = new CollectorSet({ logger: this.logger.get('collector-set'), maximumWaitTimeForAllCollectorsInS: config.maximumWaitTimeForAllCollectorsInS, }); - const globalConfig = await this.initializerContext.config.legacy.globalConfig$ - .pipe(first()) - .toPromise(); + const globalConfig = this.initializerContext.config.legacy.get(); const router = core.http.createRouter(); setupRoutes({ diff --git a/src/plugins/vis_type_table/public/plugin.ts b/src/plugins/vis_type_table/public/plugin.ts index 4792ceefde536e..0a9d477c266914 100644 --- a/src/plugins/vis_type_table/public/plugin.ts +++ b/src/plugins/vis_type_table/public/plugin.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { PluginInitializerContext, CoreSetup, CoreStart, AsyncPlugin } from 'kibana/public'; import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; import { VisualizationsSetup } from '../../visualizations/public'; import { UsageCollectionSetup } from '../../usage_collection/public'; @@ -34,8 +34,7 @@ export interface TablePluginStartDependencies { /** @internal */ export class TableVisPlugin - implements - Plugin, void, TablePluginSetupDependencies, TablePluginStartDependencies> { + implements AsyncPlugin { initializerContext: PluginInitializerContext; constructor(initializerContext: PluginInitializerContext) { diff --git a/src/plugins/vis_type_timelion/server/index.ts b/src/plugins/vis_type_timelion/server/index.ts index e31aae7fcdda72..1dcb7263c48182 100644 --- a/src/plugins/vis_type_timelion/server/index.ts +++ b/src/plugins/vis_type_timelion/server/index.ts @@ -8,7 +8,7 @@ import { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server'; import { configSchema, ConfigSchema } from '../config'; -import { Plugin } from './plugin'; +import { TimelionPlugin } from './plugin'; export { PluginSetupContract } from './plugin'; @@ -25,4 +25,4 @@ export const config: PluginConfigDescriptor = { ], }; export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); + new TimelionPlugin(initializerContext); diff --git a/src/plugins/vis_type_timelion/server/plugin.ts b/src/plugins/vis_type_timelion/server/plugin.ts index 2bb8f7214f9045..c1800a09ba35c9 100644 --- a/src/plugins/vis_type_timelion/server/plugin.ts +++ b/src/plugins/vis_type_timelion/server/plugin.ts @@ -7,13 +7,12 @@ */ import { i18n } from '@kbn/i18n'; -import { first } from 'rxjs/operators'; import { TypeOf, schema } from '@kbn/config-schema'; import { RecursiveReadonly } from '@kbn/utility-types'; import { deepFreeze } from '@kbn/std'; import type { PluginStart, DataRequestHandlerContext } from '../../../../src/plugins/data/server'; -import { CoreSetup, PluginInitializerContext } from '../../../../src/core/server'; +import { CoreSetup, PluginInitializerContext, Plugin } from '../../../../src/core/server'; import { configSchema } from '../config'; import loadFunctions from './lib/load_functions'; import { functionsRoute } from './routes/functions'; @@ -39,16 +38,12 @@ export interface TimelionPluginStartDeps { /** * Represents Timelion Plugin instance that will be managed by the Kibana plugin system. */ -export class Plugin { +export class TimelionPlugin + implements Plugin, void, TimelionPluginStartDeps> { constructor(private readonly initializerContext: PluginInitializerContext) {} - public async setup( - core: CoreSetup - ): Promise> { - const config = await this.initializerContext.config - .create>() - .pipe(first()) - .toPromise(); + public setup(core: CoreSetup): RecursiveReadonly { + const config = this.initializerContext.config.get>(); const configManager = new ConfigManager(this.initializerContext.config); diff --git a/src/plugins/vis_type_timeseries/public/plugin.ts b/src/plugins/vis_type_timeseries/public/plugin.ts index 59ae89300705ec..6900630ffa9710 100644 --- a/src/plugins/vis_type_timeseries/public/plugin.ts +++ b/src/plugins/vis_type_timeseries/public/plugin.ts @@ -43,14 +43,14 @@ export interface MetricsPluginStartDependencies { } /** @internal */ -export class MetricsPlugin implements Plugin, void> { +export class MetricsPlugin implements Plugin { initializerContext: PluginInitializerContext; constructor(initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; } - public async setup( + public setup( core: CoreSetup, { expressions, visualizations, charts, visualize }: MetricsPluginSetupDependencies ) { diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index a01af7484ea991..7cc70f31589c7c 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -54,14 +54,14 @@ export interface VegaPluginStartDependencies { } /** @internal */ -export class VegaPlugin implements Plugin, void> { +export class VegaPlugin implements Plugin { initializerContext: PluginInitializerContext; constructor(initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; } - public async setup( + public setup( core: CoreSetup, { inspector, data, expressions, visualizations, mapsLegacy }: VegaPluginSetupDependencies ) { diff --git a/src/plugins/vis_type_vislib/public/plugin.ts b/src/plugins/vis_type_vislib/public/plugin.ts index b266a681f80315..9d329c92bede0c 100644 --- a/src/plugins/vis_type_vislib/public/plugin.ts +++ b/src/plugins/vis_type_vislib/public/plugin.ts @@ -46,7 +46,7 @@ export class VisTypeVislibPlugin Plugin { constructor(public initializerContext: PluginInitializerContext) {} - public async setup( + public setup( core: VisTypeVislibCoreSetup, { expressions, visualizations, charts }: VisTypeVislibPluginSetupDependencies ) { diff --git a/src/plugins/vis_type_xy/public/plugin.ts b/src/plugins/vis_type_xy/public/plugin.ts index 5be971a085d3ce..75a2f4fb6895c1 100644 --- a/src/plugins/vis_type_xy/public/plugin.ts +++ b/src/plugins/vis_type_xy/public/plugin.ts @@ -59,7 +59,7 @@ export class VisTypeXyPlugin VisTypeXyPluginSetupDependencies, VisTypeXyPluginStartDependencies > { - public async setup( + public setup( core: VisTypeXyCoreSetup, { expressions, visualizations, charts, usageCollection }: VisTypeXyPluginSetupDependencies ) { diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 1cad0ca7ca3968..3d82e6c60a1b6e 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -84,7 +84,7 @@ export class VisualizePlugin constructor(private initializerContext: PluginInitializerContext) {} - public async setup( + public setup( core: CoreSetup, { home, urlForwarding, data }: VisualizePluginSetupDependencies ) { diff --git a/test/plugin_functional/plugins/app_link_test/public/plugin.ts b/test/plugin_functional/plugins/app_link_test/public/plugin.ts index 7f92cdccd7243d..8d75cb09469bc8 100644 --- a/test/plugin_functional/plugins/app_link_test/public/plugin.ts +++ b/test/plugin_functional/plugins/app_link_test/public/plugin.ts @@ -10,7 +10,7 @@ import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; import { renderApp } from './app'; export class CoreAppLinkPlugin implements Plugin { - public async setup(core: CoreSetup, deps: {}) { + public setup(core: CoreSetup, deps: {}) { core.application.register({ id: 'applink_start', title: 'AppLink Start', diff --git a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx index 6a167b17befd13..48c8d85b21dac9 100644 --- a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx +++ b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx @@ -42,7 +42,7 @@ export class CorePluginBPlugin }; } - public async start(core: CoreStart, deps: {}) { + public start(core: CoreStart, deps: {}) { return { sendSystemRequest: async (asSystemRequest: boolean) => { const response = await core.http.post('/core_plugin_b/system_request', { diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 9797a55fa0e3d0..8fbacc71d30cb3 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -6,9 +6,7 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { first } from 'rxjs/operators'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { Observable } from 'rxjs'; import { PluginInitializerContext, Plugin, @@ -136,11 +134,9 @@ const includedHiddenTypes = [ ALERT_SAVED_OBJECT_TYPE, ]; -export class ActionsPlugin implements Plugin, PluginStartContract> { - private readonly config: Promise; - +export class ActionsPlugin implements Plugin { private readonly logger: Logger; - private actionsConfig?: ActionsConfig; + private readonly actionsConfig: ActionsConfig; private taskRunnerFactory?: TaskRunnerFactory; private actionTypeRegistry?: ActionTypeRegistry; private actionExecutor?: ActionExecutor; @@ -151,20 +147,20 @@ export class ActionsPlugin implements Plugin, Plugi private isESOUsingEphemeralEncryptionKey?: boolean; private readonly telemetryLogger: Logger; private readonly preconfiguredActions: PreConfiguredAction[]; - private readonly kibanaIndexConfig: Observable<{ kibana: { index: string } }>; + private readonly kibanaIndexConfig: { kibana: { index: string } }; constructor(initContext: PluginInitializerContext) { - this.config = initContext.config.create().pipe(first()).toPromise(); + this.actionsConfig = initContext.config.get(); this.logger = initContext.logger.get('actions'); this.telemetryLogger = initContext.logger.get('usage'); this.preconfiguredActions = []; - this.kibanaIndexConfig = initContext.config.legacy.globalConfig$; + this.kibanaIndexConfig = initContext.config.legacy.get(); } - public async setup( + public setup( core: CoreSetup, plugins: ActionsPluginsSetup - ): Promise { + ): PluginSetupContract { this.licenseState = new LicenseState(plugins.licensing.license$); this.isESOUsingEphemeralEncryptionKey = plugins.encryptedSavedObjects.usingEphemeralEncryptionKey; @@ -190,7 +186,6 @@ export class ActionsPlugin implements Plugin, Plugi // get executions count const taskRunnerFactory = new TaskRunnerFactory(actionExecutor); - this.actionsConfig = (await this.config) as ActionsConfig; const actionsConfigUtils = getActionsConfigurationUtilities(this.actionsConfig); for (const preconfiguredId of Object.keys(this.actionsConfig.preconfigured)) { @@ -229,20 +224,18 @@ export class ActionsPlugin implements Plugin, Plugi ); } - this.kibanaIndexConfig.subscribe((config) => { - core.http.registerRouteHandlerContext( - 'actions', - this.createRouteHandlerContext(core, config.kibana.index) + core.http.registerRouteHandlerContext( + 'actions', + this.createRouteHandlerContext(core, this.kibanaIndexConfig.kibana.index) + ); + if (usageCollection) { + initializeActionsTelemetry( + this.telemetryLogger, + plugins.taskManager, + core, + this.kibanaIndexConfig.kibana.index ); - if (usageCollection) { - initializeActionsTelemetry( - this.telemetryLogger, - plugins.taskManager, - core, - config.kibana.index - ); - } - }); + } // Routes const router = core.http.createRouter(); @@ -304,7 +297,7 @@ export class ActionsPlugin implements Plugin, Plugi request ); - const kibanaIndex = (await kibanaIndexConfig.pipe(first()).toPromise()).kibana.index; + const kibanaIndex = kibanaIndexConfig.kibana.index; return new ActionsClient({ unsecuredSavedObjectsClient, diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index e2840dbdf5ef7a..49fded8649c469 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -61,7 +61,7 @@ export class APMPlugin implements Plugin { this.initContext = initContext; } - public async setup( + public setup( core: CoreSetup, plugins: { apmOss: APMOSSPluginSetup; @@ -98,7 +98,10 @@ export class APMPlugin implements Plugin { }); } - this.currentConfig = await mergedConfig$.pipe(take(1)).toPromise(); + this.currentConfig = mergeConfigs( + plugins.apmOss.config, + this.initContext.config.get() + ); if ( plugins.taskManager && diff --git a/x-pack/plugins/beats_management/server/plugin.ts b/x-pack/plugins/beats_management/server/plugin.ts index 6a814f68a67f49..3093d5d9b8d299 100644 --- a/x-pack/plugins/beats_management/server/plugin.ts +++ b/x-pack/plugins/beats_management/server/plugin.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { take } from 'rxjs/operators'; -import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext, Logger } from 'src/core/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { SecurityPluginSetup } from '../../security/server'; import { LicensingPluginStart } from '../../licensing/server'; @@ -27,14 +26,17 @@ interface StartDeps { } export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDeps> { + private readonly logger: Logger; private securitySetup?: SecurityPluginSetup; private beatsLibs?: CMServerLibs; constructor( private readonly initializerContext: PluginInitializerContext - ) {} + ) { + this.logger = initializerContext.logger.get(); + } - public async setup(core: CoreSetup, { features, security }: SetupDeps) { + public setup(core: CoreSetup, { features, security }: SetupDeps) { this.securitySetup = security; const router = core.http.createRouter(); @@ -64,8 +66,8 @@ export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDep return {}; } - public async start({ elasticsearch }: CoreStart, { licensing }: StartDeps) { - const config = await this.initializerContext.config.create().pipe(take(1)).toPromise(); + public start({ elasticsearch }: CoreStart, { licensing }: StartDeps) { + const config = this.initializerContext.config.get(); const logger = this.initializerContext.logger.get(); const kibanaVersion = this.initializerContext.env.packageInfo.version; @@ -78,7 +80,9 @@ export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDep kibanaVersion, }); - await this.beatsLibs.database.putTemplate(INDEX_NAMES.BEATS, beatsIndexTemplate); + this.beatsLibs.database.putTemplate(INDEX_NAMES.BEATS, beatsIndexTemplate).catch((e) => { + this.logger.error(`Error create beats template: ${e.message}`); + }); return {}; } diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index 7387ae1a203c20..345f6099009fc3 100644 --- a/x-pack/plugins/canvas/server/plugin.ts +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { CoreSetup, PluginInitializerContext, Plugin, Logger, CoreStart } from 'src/core/server'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { BfetchServerSetup } from 'src/plugins/bfetch/server'; @@ -34,7 +33,7 @@ export class CanvasPlugin implements Plugin { this.logger = initializerContext.logger.get(); } - public async setup(coreSetup: CoreSetup, plugins: PluginsSetup) { + public setup(coreSetup: CoreSetup, plugins: PluginsSetup) { coreSetup.savedObjects.registerType(customElementType); coreSetup.savedObjects.registerType(workpadType); coreSetup.savedObjects.registerType(workpadTemplateType); @@ -84,9 +83,7 @@ export class CanvasPlugin implements Plugin { ); // we need the kibana index provided by global config for the Canvas usage collector - const globalConfig = await this.initializerContext.config.legacy.globalConfig$ - .pipe(first()) - .toPromise(); + const globalConfig = this.initializerContext.config.legacy.get(); registerCanvasUsageCollector(plugins.usageCollection, globalConfig.kibana.index); setupInterpreter(plugins.expressions); diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index 589093461a5e0d..8b4fdc73dab44e 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first, map } from 'rxjs/operators'; import { IContextProvider, KibanaRequest, Logger, PluginInitializerContext } from 'kibana/server'; import { CoreSetup, CoreStart } from 'src/core/server'; @@ -38,8 +37,8 @@ import { createCaseClient } from './client'; import { registerConnectors } from './connectors'; import type { CasesRequestHandlerContext } from './types'; -function createConfig$(context: PluginInitializerContext) { - return context.config.create().pipe(map((config) => config)); +function createConfig(context: PluginInitializerContext) { + return context.config.get(); } export interface PluginsSetup { @@ -60,7 +59,7 @@ export class CasePlugin { } public async setup(core: CoreSetup, plugins: PluginsSetup) { - const config = await createConfig$(this.initializerContext).pipe(first()).toPromise(); + const config = createConfig(this.initializerContext); if (!config.enabled) { return; @@ -118,7 +117,7 @@ export class CasePlugin { }); } - public async start(core: CoreStart) { + public start(core: CoreStart) { this.log.debug(`Starting Case Workflow`); this.alertsService!.initialize(core.elasticsearch.client); diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts index eeb295b264f608..4c12aa3d92b47b 100644 --- a/x-pack/plugins/cloud/public/plugin.ts +++ b/x-pack/plugins/cloud/public/plugin.ts @@ -45,7 +45,7 @@ export class CloudPlugin implements Plugin { this.isCloudEnabled = false; } - public async setup(core: CoreSetup, { home }: CloudSetupDependencies) { + public setup(core: CoreSetup, { home }: CloudSetupDependencies) { const { id, resetPasswordUrl, deploymentUrl } = this.config; this.isCloudEnabled = getIsCloudEnabled(id); diff --git a/x-pack/plugins/cloud/server/plugin.ts b/x-pack/plugins/cloud/server/plugin.ts index 55ed72ca01957e..6abfb864d1cd08 100644 --- a/x-pack/plugins/cloud/server/plugin.ts +++ b/x-pack/plugins/cloud/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; -import { Observable } from 'rxjs'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; import { CloudConfigType } from './config'; @@ -28,25 +26,24 @@ export interface CloudSetup { export class CloudPlugin implements Plugin { private readonly logger: Logger; - private readonly config$: Observable; + private readonly config: CloudConfigType; constructor(private readonly context: PluginInitializerContext) { this.logger = this.context.logger.get(); - this.config$ = this.context.config.create(); + this.config = this.context.config.get(); } - public async setup(core: CoreSetup, { usageCollection }: PluginsSetup) { + public setup(core: CoreSetup, { usageCollection }: PluginsSetup) { this.logger.debug('Setting up Cloud plugin'); - const config = await this.config$.pipe(first()).toPromise(); - const isCloudEnabled = getIsCloudEnabled(config.id); + const isCloudEnabled = getIsCloudEnabled(this.config.id); registerCloudUsageCollector(usageCollection, { isCloudEnabled }); return { - cloudId: config.id, + cloudId: this.config.id, isCloudEnabled, apm: { - url: config.apm?.url, - secretToken: config.apm?.secret_token, + url: this.config.apm?.url, + secretToken: this.config.apm?.secret_token, }, }; } diff --git a/x-pack/plugins/code/server/plugin.ts b/x-pack/plugins/code/server/plugin.ts index c9197a30b5214e..eb7481d12387d0 100644 --- a/x-pack/plugins/code/server/plugin.ts +++ b/x-pack/plugins/code/server/plugin.ts @@ -5,22 +5,18 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { TypeOf } from '@kbn/config-schema'; -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext, Plugin } from 'src/core/server'; import { CodeConfigSchema } from './config'; /** * Represents Code Plugin instance that will be managed by the Kibana plugin system. */ -export class CodePlugin { +export class CodePlugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} public async setup() { - const config = await this.initializerContext.config - .create>() - .pipe(first()) - .toPromise(); + const config = this.initializerContext.config.get>(); if (config && Object.keys(config).length > 0) { this.initializerContext.logger diff --git a/x-pack/plugins/encrypted_saved_objects/server/index.ts b/x-pack/plugins/encrypted_saved_objects/server/index.ts index 8097c22cfbabc6..53b020e5b82411 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/index.ts @@ -7,7 +7,7 @@ import { PluginInitializerContext } from 'src/core/server'; import { ConfigSchema } from './config'; -import { Plugin } from './plugin'; +import { EncryptedSavedObjectsPlugin } from './plugin'; export { EncryptedSavedObjectTypeRegistration, EncryptionError } from './crypto'; export { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart } from './plugin'; @@ -15,4 +15,4 @@ export { EncryptedSavedObjectsClient } from './saved_objects'; export const config = { schema: ConfigSchema }; export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); + new EncryptedSavedObjectsPlugin(initializerContext); diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts index 2324c31b13d004..823a6b0afa9dc8 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Plugin } from './plugin'; +import { EncryptedSavedObjectsPlugin } from './plugin'; import { ConfigSchema } from './config'; import { coreMock } from 'src/core/server/mocks'; @@ -13,12 +13,12 @@ import { securityMock } from '../../security/server/mocks'; describe('EncryptedSavedObjects Plugin', () => { describe('setup()', () => { - it('exposes proper contract', async () => { - const plugin = new Plugin( + it('exposes proper contract', () => { + const plugin = new EncryptedSavedObjectsPlugin( coreMock.createPluginInitializerContext(ConfigSchema.validate({}, { dist: true })) ); - await expect(plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() })) - .resolves.toMatchInlineSnapshot(` + expect(plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() })) + .toMatchInlineSnapshot(` Object { "createMigration": [Function], "registerType": [Function], @@ -29,14 +29,14 @@ describe('EncryptedSavedObjects Plugin', () => { }); describe('start()', () => { - it('exposes proper contract', async () => { - const plugin = new Plugin( + it('exposes proper contract', () => { + const plugin = new EncryptedSavedObjectsPlugin( coreMock.createPluginInitializerContext(ConfigSchema.validate({}, { dist: true })) ); - await plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() }); + plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() }); const startContract = plugin.start(); - await expect(startContract).toMatchInlineSnapshot(` + expect(startContract).toMatchInlineSnapshot(` Object { "getClient": [Function], "isEncryptionError": [Function], diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts index bfc757accaa82f..e846b133c26e0b 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts @@ -5,9 +5,8 @@ * 2.0. */ -import { first, map } from 'rxjs/operators'; import nodeCrypto from '@elastic/node-crypto'; -import { Logger, PluginInitializerContext, CoreSetup } from 'src/core/server'; +import { Logger, PluginInitializerContext, CoreSetup, Plugin } from 'src/core/server'; import { TypeOf } from '@kbn/config-schema'; import { SecurityPluginSetup } from '../../security/server'; import { createConfig, ConfigSchema } from './config'; @@ -40,7 +39,9 @@ export interface EncryptedSavedObjectsPluginStart { /** * Represents EncryptedSavedObjects Plugin instance that will be managed by the Kibana plugin system. */ -export class Plugin { +export class EncryptedSavedObjectsPlugin + implements + Plugin { private readonly logger: Logger; private savedObjectsSetup!: ClientInstanciator; @@ -48,17 +49,11 @@ export class Plugin { this.logger = this.initializerContext.logger.get(); } - public async setup( - core: CoreSetup, - deps: PluginsSetup - ): Promise { - const config = await this.initializerContext.config - .create>() - .pipe( - map((rawConfig) => createConfig(rawConfig, this.initializerContext.logger.get('config'))) - ) - .pipe(first()) - .toPromise(); + public setup(core: CoreSetup, deps: PluginsSetup): EncryptedSavedObjectsPluginSetup { + const config = createConfig( + this.initializerContext.config.get>(), + this.initializerContext.logger.get('config') + ); const auditLogger = new EncryptedSavedObjectsAuditLogger( deps.security?.audit.getLogger('encryptedSavedObjects') ); diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 4ea8ef2c089e4a..569479f921cddc 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { Plugin, PluginInitializerContext, @@ -66,19 +64,19 @@ export interface RouteDependencies { } export class EnterpriseSearchPlugin implements Plugin { - private config: Observable; - private logger: Logger; + private readonly config: ConfigType; + private readonly logger: Logger; constructor(initializerContext: PluginInitializerContext) { - this.config = initializerContext.config.create(); + this.config = initializerContext.config.get(); this.logger = initializerContext.logger.get(); } - public async setup( + public setup( { capabilities, http, savedObjects, getStartServices }: CoreSetup, { usageCollection, security, features }: PluginsSetup ) { - const config = await this.config.pipe(first()).toPromise(); + const config = this.config; const log = this.logger; /** diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index 04be4ce67c12d7..9cc874735cc0e4 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { CoreSetup, CoreStart, @@ -24,7 +22,6 @@ import type { IEventLogConfig, IEventLogService, IEventLogger, - IEventLogConfig$, IEventLogClientService, } from './types'; import { findRoute } from './routes'; @@ -48,32 +45,29 @@ interface PluginStartDeps { } export class Plugin implements CorePlugin { - private readonly config$: IEventLogConfig$; + private readonly config: IEventLogConfig; private systemLogger: Logger; private eventLogService?: EventLogService; private esContext?: EsContext; private eventLogger?: IEventLogger; - private globalConfig$: Observable; + private globalConfig: SharedGlobalConfig; private eventLogClientService?: EventLogClientService; private savedObjectProviderRegistry: SavedObjectProviderRegistry; private kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; constructor(private readonly context: PluginInitializerContext) { this.systemLogger = this.context.logger.get(); - this.config$ = this.context.config.create(); - this.globalConfig$ = this.context.config.legacy.globalConfig$; + this.config = this.context.config.get(); + this.globalConfig = this.context.config.legacy.get(); this.savedObjectProviderRegistry = new SavedObjectProviderRegistry(); this.kibanaVersion = this.context.env.packageInfo.version; } - async setup(core: CoreSetup): Promise { - const globalConfig = await this.globalConfig$.pipe(first()).toPromise(); - const kibanaIndex = globalConfig.kibana.index; + setup(core: CoreSetup): IEventLogService { + const kibanaIndex = this.globalConfig.kibana.index; this.systemLogger.debug('setting up plugin'); - const config = await this.config$.pipe(first()).toPromise(); - this.esContext = createEsContext({ logger: this.systemLogger, // TODO: get index prefix from config.get(kibana.index) @@ -85,7 +79,7 @@ export class Plugin implements CorePlugin { + start(core: CoreStart, { spaces }: PluginStartDeps): IEventLogClientService { this.systemLogger.debug('starting plugin'); if (!this.esContext) throw new Error('esContext not initialized'); diff --git a/x-pack/plugins/event_log/server/types.ts b/x-pack/plugins/event_log/server/types.ts index 786f5ba587d267..0e5e62b591290c 100644 --- a/x-pack/plugins/event_log/server/types.ts +++ b/x-pack/plugins/event_log/server/types.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; import { schema, TypeOf } from '@kbn/config-schema'; import type { IRouter, KibanaRequest, RequestHandlerContext } from 'src/core/server'; @@ -25,7 +24,6 @@ export const ConfigSchema = schema.object({ }); export type IEventLogConfig = TypeOf; -export type IEventLogConfig$ = Observable>; // the object exposed by plugin.setup() export interface IEventLogService { diff --git a/x-pack/plugins/features/server/index.ts b/x-pack/plugins/features/server/index.ts index 111f294b6ad55e..0890274fed950c 100644 --- a/x-pack/plugins/features/server/index.ts +++ b/x-pack/plugins/features/server/index.ts @@ -6,7 +6,7 @@ */ import { PluginInitializerContext } from '../../../../src/core/server'; -import { Plugin } from './plugin'; +import { FeaturesPlugin } from './plugin'; // These exports are part of public Features plugin contract, any change in signature of exported // functions or removal of exports should be considered as a breaking change. Ideally we should @@ -25,4 +25,4 @@ export { export { PluginSetupContract, PluginStartContract } from './plugin'; export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); + new FeaturesPlugin(initializerContext); diff --git a/x-pack/plugins/features/server/plugin.test.ts b/x-pack/plugins/features/server/plugin.test.ts index 4462edeed9510c..0de03e54e1f790 100644 --- a/x-pack/plugins/features/server/plugin.test.ts +++ b/x-pack/plugins/features/server/plugin.test.ts @@ -6,7 +6,7 @@ */ import { coreMock, savedObjectsServiceMock } from 'src/core/server/mocks'; -import { Plugin } from './plugin'; +import { FeaturesPlugin } from './plugin'; describe('Features Plugin', () => { let initContext: ReturnType; @@ -31,7 +31,7 @@ describe('Features Plugin', () => { }); it('returns OSS + registered kibana features', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); const { registerKibanaFeature } = await plugin.setup(coreSetup, {}); registerKibanaFeature({ id: 'baz', @@ -58,7 +58,7 @@ describe('Features Plugin', () => { }); it('returns OSS + registered kibana features with timelion when available', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); const { registerKibanaFeature: registerFeature } = await plugin.setup(coreSetup, { visTypeTimelion: { uiEnabled: true }, }); @@ -88,7 +88,7 @@ describe('Features Plugin', () => { }); it('registers kibana features with not hidden saved objects types', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); await plugin.setup(coreSetup, {}); const { getKibanaFeatures } = plugin.start(coreStart); @@ -101,7 +101,7 @@ describe('Features Plugin', () => { }); it('returns registered elasticsearch features', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); const { registerElasticsearchFeature } = await plugin.setup(coreSetup, {}); registerElasticsearchFeature({ id: 'baz', @@ -123,7 +123,7 @@ describe('Features Plugin', () => { }); it('registers a capabilities provider', async () => { - const plugin = new Plugin(initContext); + const plugin = new FeaturesPlugin(initContext); await plugin.setup(coreSetup, {}); expect(coreSetup.capabilities.registerProvider).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index e96c257516b98d..6a9fd1da826a6a 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -12,6 +12,7 @@ import { CoreStart, SavedObjectsServiceStart, Logger, + Plugin, PluginInitializerContext, } from '../../../../src/core/server'; import { Capabilities as UICapabilities } from '../../../../src/core/server'; @@ -59,7 +60,9 @@ interface TimelionSetupContract { /** * Represents Features Plugin instance that will be managed by the Kibana plugin system. */ -export class Plugin { +export class FeaturesPlugin + implements + Plugin, RecursiveReadonly> { private readonly logger: Logger; private readonly featureRegistry: FeatureRegistry = new FeatureRegistry(); private isTimelionEnabled: boolean = false; @@ -68,10 +71,10 @@ export class Plugin { this.logger = this.initializerContext.logger.get(); } - public async setup( + public setup( core: CoreSetup, { visTypeTimelion }: { visTypeTimelion?: TimelionSetupContract } - ): Promise> { + ): RecursiveReadonly { this.isTimelionEnabled = visTypeTimelion !== undefined && visTypeTimelion.uiEnabled; defineRoutes({ diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index fce8e89a6573df..50e647e271ecc7 100644 --- a/x-pack/plugins/fleet/public/plugin.ts +++ b/x-pack/plugins/fleet/public/plugin.ts @@ -155,7 +155,7 @@ export class FleetPlugin implements Plugin { + public start(core: CoreStart): FleetStart { let successPromise: ReturnType; return { diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 1aa6b42611a342..7378d45e1bb3aa 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -12,7 +12,7 @@ import { CoreStart, ElasticsearchServiceStart, Logger, - Plugin, + AsyncPlugin, PluginInitializerContext, SavedObjectsServiceStart, HttpServiceSetup, @@ -169,7 +169,7 @@ export interface FleetStartContract { } export class FleetPlugin - implements Plugin { + implements AsyncPlugin { private licensing$!: Observable; private config$: Observable; private cloud: CloudSetup | undefined; diff --git a/x-pack/plugins/global_search/server/plugin.ts b/x-pack/plugins/global_search/server/plugin.ts index 8d560d9a0f5534..d7c06a92f70e07 100644 --- a/x-pack/plugins/global_search/server/plugin.ts +++ b/x-pack/plugins/global_search/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; import { LicensingPluginStart } from '../../licensing/server'; import { LicenseChecker, ILicenseChecker } from '../common/license_checker'; @@ -33,20 +31,19 @@ export class GlobalSearchPlugin GlobalSearchPluginSetupDeps, GlobalSearchPluginStartDeps > { - private readonly config$: Observable; + private readonly config: GlobalSearchConfigType; private readonly searchService = new SearchService(); private searchServiceStart?: SearchServiceStart; private licenseChecker?: ILicenseChecker; constructor(context: PluginInitializerContext) { - this.config$ = context.config.create(); + this.config = context.config.get(); } - public async setup(core: CoreSetup<{}, GlobalSearchPluginStart>) { - const config = await this.config$.pipe(take(1)).toPromise(); + public setup(core: CoreSetup<{}, GlobalSearchPluginStart>) { const { registerResultProvider } = this.searchService.setup({ basePath: core.http.basePath, - config, + config: this.config, }); registerRoutes(core.http.createRouter()); diff --git a/x-pack/plugins/graph/server/plugin.ts b/x-pack/plugins/graph/server/plugin.ts index 5c13756842039a..32dac5fba86f9a 100644 --- a/x-pack/plugins/graph/server/plugin.ts +++ b/x-pack/plugins/graph/server/plugin.ts @@ -20,7 +20,7 @@ import { graphWorkspace } from './saved_objects'; export class GraphPlugin implements Plugin { private licenseState: LicenseState | null = null; - public async setup( + public setup( core: CoreSetup, { licensing, diff --git a/x-pack/plugins/index_lifecycle_management/server/plugin.ts b/x-pack/plugins/index_lifecycle_management/server/plugin.ts index 532cf253c7f89b..95793c0cad4655 100644 --- a/x-pack/plugins/index_lifecycle_management/server/plugin.ts +++ b/x-pack/plugins/index_lifecycle_management/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { CoreSetup, @@ -52,22 +50,19 @@ const indexLifecycleDataEnricher = async ( }; export class IndexLifecycleManagementServerPlugin implements Plugin { - private readonly config$: Observable; + private readonly config: IndexLifecycleManagementConfig; private readonly license: License; private readonly logger: Logger; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); - this.config$ = initializerContext.config.create(); + this.config = initializerContext.config.get(); this.license = new License(); } - async setup( - { http }: CoreSetup, - { licensing, indexManagement, features }: Dependencies - ): Promise { + setup({ http }: CoreSetup, { licensing, indexManagement, features }: Dependencies): void { const router = http.createRouter(); - const config = await this.config$.pipe(first()).toPromise(); + const config = this.config; this.license.setup( { diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index e91e085207cb75..99555fa56acd59 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -8,8 +8,7 @@ import { Server } from '@hapi/hapi'; import { schema, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; -import { Observable } from 'rxjs'; -import { CoreSetup, PluginInitializerContext } from 'src/core/server'; +import { CoreSetup, PluginInitializerContext, Plugin } from 'src/core/server'; import { InfraStaticSourceConfiguration } from '../common/http_api/source_api'; import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view'; @@ -79,22 +78,15 @@ export interface InfraPluginSetup { ) => void; } -export class InfraServerPlugin { - private config$: Observable; - public config = {} as InfraConfig; +export class InfraServerPlugin implements Plugin { + public config: InfraConfig; public libs: InfraBackendLibs | undefined; constructor(context: PluginInitializerContext) { - this.config$ = context.config.create(); + this.config = context.config.get(); } - async setup(core: CoreSetup, plugins: InfraServerPluginSetupDeps) { - await new Promise((resolve) => { - this.config$.subscribe((configValue) => { - this.config = configValue; - resolve(); - }); - }); + setup(core: CoreSetup, plugins: InfraServerPluginSetupDeps) { const framework = new KibanaFramework(core, this.config, plugins); const sources = new InfraSources({ config: this.config, diff --git a/x-pack/plugins/licensing/public/plugin.ts b/x-pack/plugins/licensing/public/plugin.ts index 0207f793102738..1db463a47dbf0e 100644 --- a/x-pack/plugins/licensing/public/plugin.ts +++ b/x-pack/plugins/licensing/public/plugin.ts @@ -123,7 +123,7 @@ export class LicensingPlugin implements Plugin { private stop$ = new Subject(); private readonly logger: Logger; - private readonly config$: Observable; + private readonly config: LicenseConfigType; private loggingSubscription?: Subscription; private featureUsage = new FeatureUsageService(); @@ -92,13 +91,12 @@ export class LicensingPlugin implements Plugin(); + this.config = this.context.config.get(); } - public async setup(core: CoreSetup<{}, LicensingPluginStart>) { + public setup(core: CoreSetup<{}, LicensingPluginStart>) { this.logger.debug('Setting up Licensing plugin'); - const config = await this.config$.pipe(take(1)).toPromise(); - const pollingFrequency = config.api_polling_frequency; + const pollingFrequency = this.config.api_polling_frequency; async function callAsInternalUser( ...args: Parameters @@ -225,7 +223,7 @@ export class LicensingPlugin implements Plugin> => { - return context.config.create().pipe(map((config) => config)); -}; diff --git a/x-pack/plugins/lists/server/plugin.ts b/x-pack/plugins/lists/server/plugin.ts index bc064e236b658e..b79d6a0b89a575 100644 --- a/x-pack/plugins/lists/server/plugin.ts +++ b/x-pack/plugins/lists/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { Logger, Plugin, PluginInitializerContext } from 'kibana/server'; import type { CoreSetup, CoreStart } from 'src/core/server'; @@ -23,7 +22,6 @@ import type { ListsRequestHandlerContext, PluginsStart, } from './types'; -import { createConfig$ } from './create_config'; import { getSpaceId } from './get_space_id'; import { getUser } from './get_user'; import { initSavedObjects } from './saved_objects'; @@ -32,17 +30,17 @@ import { ExceptionListClient } from './services/exception_lists/exception_list_c export class ListPlugin implements Plugin, ListsPluginStart, {}, PluginsStart> { private readonly logger: Logger; + private readonly config: ConfigType; private spaces: SpacesServiceStart | undefined | null; - private config: ConfigType | undefined | null; private security: SecurityPluginStart | undefined | null; constructor(private readonly initializerContext: PluginInitializerContext) { this.logger = this.initializerContext.logger.get(); + this.config = this.initializerContext.config.get(); } public async setup(core: CoreSetup): Promise { - const config = await createConfig$(this.initializerContext).pipe(first()).toPromise(); - this.config = config; + const { config } = this; initSavedObjects(core.savedObjects); diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index 786e35212ec7b0..7440b6ee1e1dfa 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; -import { take } from 'rxjs/operators'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; // @ts-ignore @@ -134,12 +133,11 @@ export class MapsPlugin implements Plugin { } // @ts-ignore - async setup(core: CoreSetup, plugins: SetupDeps) { + setup(core: CoreSetup, plugins: SetupDeps) { const { usageCollection, home, licensing, features, mapsLegacy } = plugins; - // @ts-ignore + const mapsLegacyConfig = mapsLegacy.config; const config$ = this._initializerContext.config.create(); - const mapsLegacyConfig = await mapsLegacy.config$.pipe(take(1)).toPromise(); - const currentConfig = await config$.pipe(take(1)).toPromise(); + const currentConfig = this._initializerContext.config.get(); // @ts-ignore const mapsEnabled = currentConfig.enabled; diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index 3d3671ac0a6a44..b950b064774b19 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -49,7 +49,7 @@ export class MonitoringPlugin Plugin { constructor(private initializerContext: PluginInitializerContext) {} - public async setup( + public setup( core: CoreSetup, plugins: MonitoringSetupPluginDependencies ) { diff --git a/x-pack/plugins/monitoring/server/index.ts b/x-pack/plugins/monitoring/server/index.ts index 012c050cd3fa8a..97e572d15327c4 100644 --- a/x-pack/plugins/monitoring/server/index.ts +++ b/x-pack/plugins/monitoring/server/index.ts @@ -7,13 +7,15 @@ import { TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext, PluginConfigDescriptor } from '../../../../src/core/server'; -import { Plugin } from './plugin'; +import { MonitoringPlugin } from './plugin'; import { configSchema } from './config'; import { deprecations } from './deprecations'; export { KibanaSettingsCollector } from './kibana_monitoring/collectors'; export { MonitoringConfig } from './config'; -export const plugin = (initContext: PluginInitializerContext) => new Plugin(initContext); +export { MonitoringPluginSetup, IBulkUploader } from './types'; + +export const plugin = (initContext: PluginInitializerContext) => new MonitoringPlugin(initContext); export const config: PluginConfigDescriptor> = { schema: configSchema, deprecations, diff --git a/x-pack/plugins/monitoring/server/plugin.test.ts b/x-pack/plugins/monitoring/server/plugin.test.ts index 2a5138d0d88801..08224980a558fa 100644 --- a/x-pack/plugins/monitoring/server/plugin.test.ts +++ b/x-pack/plugins/monitoring/server/plugin.test.ts @@ -6,16 +6,9 @@ */ import { coreMock } from 'src/core/server/mocks'; -import { Plugin } from './plugin'; -import { combineLatest } from 'rxjs'; +import { MonitoringPlugin } from './plugin'; import { AlertsFactory } from './alerts'; -jest.mock('rxjs', () => ({ - // @ts-ignore - ...jest.requireActual('rxjs'), - combineLatest: jest.fn(), -})); - jest.mock('./es_client/instantiate_client', () => ({ instantiateClient: jest.fn().mockImplementation(() => ({ cluster: {}, @@ -32,30 +25,11 @@ jest.mock('./kibana_monitoring/collectors', () => ({ registerCollectors: jest.fn(), })); -describe('Monitoring plugin', () => { - const initializerContext = { - logger: { - get: jest.fn().mockImplementation(() => ({ - info: jest.fn(), - })), - }, - config: { - create: jest.fn().mockImplementation(() => ({ - pipe: jest.fn().mockImplementation(() => ({ - toPromise: jest.fn(), - })), - })), - legacy: { - globalConfig$: {}, - }, - }, - env: { - packageInfo: { - version: '1.0.0', - }, - }, - }; +jest.mock('./config', () => ({ + createConfig: (config: any) => config, +})); +describe('Monitoring plugin', () => { const coreSetup = coreMock.createSetup(); coreSetup.http.getServerInfo.mockReturnValue({ port: 5601 } as any); coreSetup.status.overall$.subscribe = jest.fn(); @@ -71,7 +45,6 @@ describe('Monitoring plugin', () => { }, }; - let config = {}; const defaultConfig = { ui: { elasticsearch: {}, @@ -83,20 +56,7 @@ describe('Monitoring plugin', () => { }, }; - beforeEach(() => { - config = defaultConfig; - (combineLatest as jest.Mock).mockImplementation(() => { - return { - pipe: jest.fn().mockImplementation(() => { - return { - toPromise: jest.fn().mockImplementation(() => { - return [config, 2]; - }), - }; - }), - }; - }); - }); + const initializerContext = coreMock.createPluginInitializerContext(defaultConfig); afterEach(() => { (setupPlugins.alerts.registerType as jest.Mock).mockReset(); @@ -104,14 +64,14 @@ describe('Monitoring plugin', () => { }); it('always create the bulk uploader', async () => { - const plugin = new Plugin(initializerContext as any); + const plugin = new MonitoringPlugin(initializerContext as any); await plugin.setup(coreSetup, setupPlugins as any); expect(coreSetup.status.overall$.subscribe).toHaveBeenCalled(); }); it('should register all alerts', async () => { const alerts = AlertsFactory.getAll(); - const plugin = new Plugin(initializerContext as any); + const plugin = new MonitoringPlugin(initializerContext as any); await plugin.setup(coreSetup as any, setupPlugins as any); expect(setupPlugins.alerts.registerType).toHaveBeenCalledTimes(alerts.length); }); diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 6fd9e7534ac650..654c3de7d81a96 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -6,8 +6,6 @@ */ import Boom from '@hapi/boom'; -import { combineLatest } from 'rxjs'; -import { first, map } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { has, get } from 'lodash'; import { TypeOf } from '@kbn/config-schema'; @@ -21,6 +19,7 @@ import { CoreStart, CustomHttpResponseOptions, ResponseError, + Plugin, } from 'kibana/server'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { @@ -43,6 +42,7 @@ import { AlertsFactory } from './alerts'; import { MonitoringCore, MonitoringLicenseService, + MonitoringPluginSetup, LegacyShimDependencies, IBulkUploader, PluginsSetup, @@ -66,7 +66,8 @@ const wrapError = (error: any): CustomHttpResponseOptions => { }; }; -export class Plugin { +export class MonitoringPlugin + implements Plugin { private readonly initializerContext: PluginInitializerContext; private readonly log: Logger; private readonly getLogger: (...scopes: string[]) => Logger; @@ -82,15 +83,9 @@ export class Plugin { this.getLogger = (...scopes: string[]) => initializerContext.logger.get(LOGGING_TAG, ...scopes); } - async setup(core: CoreSetup, plugins: PluginsSetup) { - const [config, legacyConfig] = await combineLatest([ - this.initializerContext.config - .create>() - .pipe(map((rawConfig) => createConfig(rawConfig))), - this.initializerContext.config.legacy.globalConfig$, - ]) - .pipe(first()) - .toPromise(); + setup(core: CoreSetup, plugins: PluginsSetup) { + const config = createConfig(this.initializerContext.config.get>()); + const legacyConfig = this.initializerContext.config.legacy.get(); const router = core.http.createRouter(); this.legacyShimDependencies = { diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index 0fd30189c54159..bb0b616d37eac3 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -94,6 +94,10 @@ export interface IBulkUploader { stop: () => void; } +export interface MonitoringPluginSetup { + getKibanaStats: IBulkUploader['getKibanaStats']; +} + export interface LegacyRequest { logger: Logger; getLogger: (...scopes: string[]) => Logger; diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index 62a330442fc29c..a5843d1c4ade14 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -6,7 +6,6 @@ */ import { PluginInitializerContext, Plugin, CoreSetup } from 'src/core/server'; -import { take } from 'rxjs/operators'; import { ObservabilityConfig } from '.'; import { bootstrapAnnotations, @@ -28,10 +27,8 @@ export class ObservabilityPlugin implements Plugin { this.initContext = initContext; } - public async setup(core: CoreSetup, plugins: {}): Promise { - const config$ = this.initContext.config.create(); - - const config = await config$.pipe(take(1)).toPromise(); + public setup(core: CoreSetup, plugins: {}): ObservabilityPluginSetup { + const config = this.initContext.config.get(); let annotationsApiPromise: Promise | undefined; diff --git a/x-pack/plugins/osquery/server/create_config.ts b/x-pack/plugins/osquery/server/create_config.ts index 19859ab05e6a97..d52f299a692cf5 100644 --- a/x-pack/plugins/osquery/server/create_config.ts +++ b/x-pack/plugins/osquery/server/create_config.ts @@ -5,14 +5,10 @@ * 2.0. */ -import { map } from 'rxjs/operators'; import { PluginInitializerContext } from 'kibana/server'; -import { Observable } from 'rxjs'; import { ConfigType } from './config'; -export const createConfig$ = ( - context: PluginInitializerContext -): Observable> => { - return context.config.create().pipe(map((config) => config)); +export const createConfig = (context: PluginInitializerContext): Readonly => { + return context.config.get(); }; diff --git a/x-pack/plugins/osquery/server/plugin.ts b/x-pack/plugins/osquery/server/plugin.ts index 77509275431e9b..c30f4ac057ec0c 100644 --- a/x-pack/plugins/osquery/server/plugin.ts +++ b/x-pack/plugins/osquery/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { PluginInitializerContext, CoreSetup, @@ -14,7 +13,7 @@ import { Logger, } from '../../../../src/core/server'; -import { createConfig$ } from './create_config'; +import { createConfig } from './create_config'; import { OsqueryPluginSetup, OsqueryPluginStart, SetupPlugins, StartPlugins } from './types'; import { defineRoutes } from './routes'; import { osquerySearchStrategyProvider } from './search_strategy/osquery'; @@ -26,9 +25,9 @@ export class OsqueryPlugin implements Plugin, plugins: SetupPlugins) { + public setup(core: CoreSetup, plugins: SetupPlugins) { this.logger.debug('osquery: Setup'); - const config = await createConfig$(this.initializerContext).pipe(first()).toPromise(); + const config = createConfig(this.initializerContext); if (!config.enabled) { return {}; diff --git a/x-pack/plugins/painless_lab/server/plugin.ts b/x-pack/plugins/painless_lab/server/plugin.ts index aefb5429a1b131..996adfdc13f647 100644 --- a/x-pack/plugins/painless_lab/server/plugin.ts +++ b/x-pack/plugins/painless_lab/server/plugin.ts @@ -22,7 +22,7 @@ export class PainlessLabServerPlugin implements Plugin { this.license = new License(); } - async setup({ http }: CoreSetup, { licensing }: Dependencies) { + setup({ http }: CoreSetup, { licensing }: Dependencies) { const router = http.createRouter(); this.license.setup( diff --git a/x-pack/plugins/remote_clusters/server/plugin.ts b/x-pack/plugins/remote_clusters/server/plugin.ts index ba78cf411e3691..2b8d9afe979e89 100644 --- a/x-pack/plugins/remote_clusters/server/plugin.ts +++ b/x-pack/plugins/remote_clusters/server/plugin.ts @@ -8,8 +8,6 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { PLUGIN } from '../common/constants'; import { Dependencies, LicenseStatus, RouteDependencies } from './types'; @@ -29,17 +27,16 @@ export class RemoteClustersServerPlugin implements Plugin { licenseStatus: LicenseStatus; log: Logger; - config$: Observable; + config: ConfigType; constructor({ logger, config }: PluginInitializerContext) { this.log = logger.get(); - this.config$ = config.create(); + this.config = config.get(); this.licenseStatus = { valid: false }; } - async setup({ http }: CoreSetup, { features, licensing, cloud }: Dependencies) { + setup({ http }: CoreSetup, { features, licensing, cloud }: Dependencies) { const router = http.createRouter(); - const config = await this.config$.pipe(first()).toPromise(); const routeDependencies: RouteDependencies = { router, @@ -89,7 +86,7 @@ export class RemoteClustersServerPlugin }); return { - isUiEnabled: config.ui.enabled, + isUiEnabled: this.config.ui.enabled, }; } diff --git a/x-pack/plugins/searchprofiler/server/plugin.ts b/x-pack/plugins/searchprofiler/server/plugin.ts index cebcbb1a0dc92c..ed85febac9a45b 100644 --- a/x-pack/plugins/searchprofiler/server/plugin.ts +++ b/x-pack/plugins/searchprofiler/server/plugin.ts @@ -21,7 +21,7 @@ export class SearchProfilerServerPlugin implements Plugin { this.licenseStatus = { valid: false }; } - async setup({ http }: CoreSetup, { licensing }: AppServerPluginDependencies) { + setup({ http }: CoreSetup, { licensing }: AppServerPluginDependencies) { const router = http.createRouter(); profileRoute.register({ router, diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index 6026e42676c57e..66b916ac7f70fe 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -15,7 +15,7 @@ import type { import { ConfigSchema } from './config'; import { securityConfigDeprecationProvider } from './config_deprecations'; import { - Plugin, + SecurityPlugin, SecurityPluginSetup, SecurityPluginStart, PluginSetupDependencies, @@ -51,4 +51,4 @@ export const plugin: PluginInitializer< RecursiveReadonly, RecursiveReadonly, PluginSetupDependencies -> = (initializerContext: PluginInitializerContext) => new Plugin(initializerContext); +> = (initializerContext: PluginInitializerContext) => new SecurityPlugin(initializerContext); diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 84fc410c72cd01..d57951ecb5b1d4 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -8,7 +8,7 @@ import { of } from 'rxjs'; import { ByteSizeValue } from '@kbn/config-schema'; import { ConfigSchema } from './config'; -import { Plugin, PluginSetupDependencies, PluginStartDependencies } from './plugin'; +import { SecurityPlugin, PluginSetupDependencies, PluginStartDependencies } from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; @@ -16,13 +16,13 @@ import { taskManagerMock } from '../../task_manager/server/mocks'; import { licensingMock } from '../../licensing/server/mocks'; describe('Security Plugin', () => { - let plugin: Plugin; + let plugin: SecurityPlugin; let mockCoreSetup: ReturnType; let mockCoreStart: ReturnType; let mockSetupDependencies: PluginSetupDependencies; let mockStartDependencies: PluginStartDependencies; beforeEach(() => { - plugin = new Plugin( + plugin = new SecurityPlugin( coreMock.createPluginInitializerContext( ConfigSchema.validate({ session: { idleTimeout: 1500 }, diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 3156af7e930bda..cccfa7de6d177d 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -8,6 +8,7 @@ import { combineLatest, Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; import { TypeOf } from '@kbn/config-schema'; +import { RecursiveReadonly } from '@kbn/utility-types'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { SecurityOssPluginSetup } from 'src/plugins/security_oss/server'; import { @@ -16,6 +17,7 @@ import { KibanaRequest, Logger, PluginInitializerContext, + Plugin, } from '../../../../src/core/server'; import { SpacesPluginSetup, SpacesPluginStart } from '../../spaces/server'; import { PluginSetupContract as FeaturesSetupContract } from '../../features/server'; @@ -101,7 +103,13 @@ export interface PluginStartDependencies { /** * Represents Security Plugin instance that will be managed by the Kibana plugin system. */ -export class Plugin { +export class SecurityPlugin + implements + Plugin< + RecursiveReadonly, + RecursiveReadonly, + PluginSetupDependencies + > { private readonly logger: Logger; private authorizationSetup?: AuthorizationServiceSetup; private auditSetup?: AuditServiceSetup; diff --git a/x-pack/plugins/security_solution/server/config.ts b/x-pack/plugins/security_solution/server/config.ts index 3791c63f662ae7..4658e6774b7269 100644 --- a/x-pack/plugins/security_solution/server/config.ts +++ b/x-pack/plugins/security_solution/server/config.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { Observable } from 'rxjs'; import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from '../../../../src/core/server'; import { SIGNALS_INDEX_KEY, DEFAULT_SIGNALS_INDEX } from '../common/constants'; @@ -38,9 +37,7 @@ export const configSchema = schema.object({ validateArtifactDownloads: schema.boolean({ defaultValue: true }), }); -export const createConfig$ = (context: PluginInitializerContext) => - context.config.create>(); +export const createConfig = (context: PluginInitializerContext) => + context.config.get>(); -export type ConfigType = ReturnType extends Observable - ? T - : ReturnType; +export type ConfigType = TypeOf; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 0d5d83582b42ba..8c35fd2ce8f8ba 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -6,7 +6,6 @@ */ import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import LRU from 'lru-cache'; @@ -47,7 +46,7 @@ import { isNotificationAlertExecutor } from './lib/detection_engine/notification import { ManifestTask } from './endpoint/lib/artifacts'; import { initSavedObjects, savedObjectTypes } from './saved_objects'; import { AppClientFactory } from './client'; -import { createConfig$, ConfigType } from './config'; +import { createConfig, ConfigType } from './config'; import { initUiSettings } from './ui_settings'; import { APP_ID, @@ -119,8 +118,7 @@ const securitySubPlugins = [ export class Plugin implements IPlugin { private readonly logger: Logger; - private readonly config$: Observable; - private config?: ConfigType; + private readonly config: ConfigType; private context: PluginInitializerContext; private appClientFactory: AppClientFactory; private setupPlugins?: SetupPlugins; @@ -137,7 +135,7 @@ export class Plugin implements IPlugin({ max: 3, maxAge: 1000 * 60 * 5 }); @@ -146,13 +144,12 @@ export class Plugin implements IPlugin, plugins: SetupPlugins) { + public setup(core: CoreSetup, plugins: SetupPlugins) { this.logger.debug('plugin setup'); this.setupPlugins = plugins; - const config = await this.config$.pipe(first()).toPromise(); - this.config = config; - const globalConfig = await this.context.config.legacy.globalConfig$.pipe(first()).toPromise(); + const config = this.config; + const globalConfig = this.context.config.legacy.get(); initSavedObjects(core.savedObjects); initUiSettings(core.uiSettings); diff --git a/x-pack/plugins/snapshot_restore/server/plugin.ts b/x-pack/plugins/snapshot_restore/server/plugin.ts index 9d4614cf602944..c93b5dbc4c36de 100644 --- a/x-pack/plugins/snapshot_restore/server/plugin.ts +++ b/x-pack/plugins/snapshot_restore/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { CoreSetup, @@ -43,14 +42,11 @@ export class SnapshotRestoreServerPlugin implements Plugin this.license = new License(); } - public async setup( + public setup( { http, getStartServices }: CoreSetup, { licensing, features, security, cloud }: Dependencies - ): Promise { - const pluginConfig = await this.context.config - .create() - .pipe(first()) - .toPromise(); + ): void { + const pluginConfig = this.context.config.get(); if (!pluginConfig.enabled) { return; diff --git a/x-pack/plugins/spaces/server/index.ts b/x-pack/plugins/spaces/server/index.ts index 9d2a075dc35f9e..fb6c00c2f6f480 100644 --- a/x-pack/plugins/spaces/server/index.ts +++ b/x-pack/plugins/spaces/server/index.ts @@ -7,7 +7,7 @@ import type { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server'; import { ConfigSchema, spacesConfigDeprecationProvider } from './config'; -import { Plugin } from './plugin'; +import { SpacesPlugin } from './plugin'; // These exports are part of public Spaces plugin contract, any change in signature of exported // functions or removal of exports should be considered as a breaking change. Ideally we should @@ -32,4 +32,4 @@ export const config: PluginConfigDescriptor = { deprecations: spacesConfigDeprecationProvider, }; export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); + new SpacesPlugin(initializerContext); diff --git a/x-pack/plugins/spaces/server/plugin.test.ts b/x-pack/plugins/spaces/server/plugin.test.ts index d576858c98e36b..d1bf4d51700ba8 100644 --- a/x-pack/plugins/spaces/server/plugin.test.ts +++ b/x-pack/plugins/spaces/server/plugin.test.ts @@ -9,7 +9,7 @@ import { CoreSetup } from 'src/core/server'; import { coreMock } from 'src/core/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; import { licensingMock } from '../../licensing/server/mocks'; -import { Plugin, PluginsStart } from './plugin'; +import { SpacesPlugin, PluginsStart } from './plugin'; import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/server/mocks'; describe('Spaces Plugin', () => { @@ -20,7 +20,7 @@ describe('Spaces Plugin', () => { const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); const spacesSetup = plugin.setup(core, { features, licensing }); expect(spacesSetup).toMatchInlineSnapshot(` Object { @@ -43,7 +43,7 @@ describe('Spaces Plugin', () => { const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); plugin.setup(core, { features, licensing }); @@ -59,7 +59,7 @@ describe('Spaces Plugin', () => { const usageCollection = usageCollectionPluginMock.createSetupContract(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); plugin.setup(core, { features, licensing, usageCollection }); @@ -72,7 +72,7 @@ describe('Spaces Plugin', () => { const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); plugin.setup(core, { features, licensing }); @@ -99,7 +99,7 @@ describe('Spaces Plugin', () => { const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); - const plugin = new Plugin(initializerContext); + const plugin = new SpacesPlugin(initializerContext); plugin.setup(coreSetup, { features, licensing }); const coreStart = coreMock.createStart(); diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index d9d32dd68c95d7..4b26b1016d5301 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -13,6 +13,7 @@ import { CoreStart, Logger, PluginInitializerContext, + Plugin, } from '../../../../src/core/server'; import { PluginSetupContract as FeaturesPluginSetup, @@ -62,7 +63,8 @@ export interface SpacesPluginStart { spacesService: SpacesServiceStart; } -export class Plugin { +export class SpacesPlugin + implements Plugin { private readonly config$: Observable; private readonly kibanaIndexConfig$: Observable<{ kibana: { index: string } }>; diff --git a/x-pack/plugins/stack_alerts/server/plugin.ts b/x-pack/plugins/stack_alerts/server/plugin.ts index 261d3d51aeb802..1343c46ecdd729 100644 --- a/x-pack/plugins/stack_alerts/server/plugin.ts +++ b/x-pack/plugins/stack_alerts/server/plugin.ts @@ -19,10 +19,7 @@ export class AlertingBuiltinsPlugin this.logger = ctx.logger.get(); } - public async setup( - core: CoreSetup, - { alerts, features }: StackAlertsDeps - ): Promise { + public setup(core: CoreSetup, { alerts, features }: StackAlertsDeps) { features.registerKibanaFeature(BUILT_IN_ALERTS_FEATURE); registerBuiltInAlertTypes({ @@ -34,6 +31,6 @@ export class AlertingBuiltinsPlugin }); } - public async start(): Promise {} - public async stop(): Promise {} + public start() {} + public stop() {} } diff --git a/x-pack/plugins/task_manager/server/plugin.test.ts b/x-pack/plugins/task_manager/server/plugin.test.ts index 77031d47649688..0a879ce92cba6e 100644 --- a/x-pack/plugins/task_manager/server/plugin.test.ts +++ b/x-pack/plugins/task_manager/server/plugin.test.ts @@ -39,7 +39,7 @@ describe('TaskManagerPlugin', () => { pluginInitializerContext.env.instanceUuid = ''; const taskManagerPlugin = new TaskManagerPlugin(pluginInitializerContext); - expect(taskManagerPlugin.setup(coreMock.createSetup())).rejects.toEqual( + expect(() => taskManagerPlugin.setup(coreMock.createSetup())).toThrow( new Error(`TaskManager is unable to start as Kibana has no valid UUID assigned to it.`) ); }); diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 62b8b75a38d737..149d111b08f02a 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -6,7 +6,7 @@ */ import { combineLatest, Observable, Subject } from 'rxjs'; -import { first, map, distinctUntilChanged } from 'rxjs/operators'; +import { map, distinctUntilChanged } from 'rxjs/operators'; import { PluginInitializerContext, Plugin, @@ -46,7 +46,7 @@ export class TaskManagerPlugin implements Plugin { private taskPollingLifecycle?: TaskPollingLifecycle; private taskManagerId?: string; - private config?: TaskManagerConfig; + private config: TaskManagerConfig; private logger: Logger; private definitions: TaskTypeDictionary; private middleware: Middleware = createInitialMiddleware(); @@ -56,15 +56,11 @@ export class TaskManagerPlugin constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; this.logger = initContext.logger.get(); + this.config = initContext.config.get(); this.definitions = new TaskTypeDictionary(this.logger); } - public async setup(core: CoreSetup): Promise { - this.config = await this.initContext.config - .create() - .pipe(first()) - .toPromise(); - + public setup(core: CoreSetup): TaskManagerSetupContract { this.elasticsearchAndSOAvailability$ = getElasticsearchAndSOAvailability(core.status.core$); setupSavedObjects(core.savedObjects, this.config); diff --git a/x-pack/plugins/triggers_actions_ui/server/plugin.ts b/x-pack/plugins/triggers_actions_ui/server/plugin.ts index f7c7e48d93d08f..3933751105cb44 100644 --- a/x-pack/plugins/triggers_actions_ui/server/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/server/plugin.ts @@ -30,7 +30,7 @@ export class TriggersActionsPlugin implements Plugin this.data = getService(); } - public async setup(core: CoreSetup, plugins: PluginsSetup): Promise { + public setup(core: CoreSetup, plugins: PluginsSetup): void { const router = core.http.createRouter(); registerDataService({ logger: this.logger, @@ -42,7 +42,7 @@ export class TriggersActionsPlugin implements Plugin createHealthRoute(this.logger, router, BASE_ROUTE, plugins.alerts !== undefined); } - public async start(): Promise { + public start(): PluginStartContract { return { data: this.data, }; diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index d7c0a465dd3e0e..8bbbecf8108feb 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -50,10 +50,7 @@ export class UptimePlugin implements Plugin { constructor(_context: PluginInitializerContext) {} - public async setup( - core: CoreSetup, - plugins: ClientPluginsSetup - ): Promise { + public setup(core: CoreSetup, plugins: ClientPluginsSetup): void { if (plugins.home) { plugins.home.featureCatalogue.register({ id: PLUGIN.ID, diff --git a/x-pack/plugins/watcher/server/plugin.ts b/x-pack/plugins/watcher/server/plugin.ts index 220a814835e759..ceade131fc5afc 100644 --- a/x-pack/plugins/watcher/server/plugin.ts +++ b/x-pack/plugins/watcher/server/plugin.ts @@ -47,7 +47,7 @@ export class WatcherServerPlugin implements Plugin { this.log = ctx.logger.get(); } - async setup({ http, getStartServices }: CoreSetup, { licensing, features }: Dependencies) { + setup({ http, getStartServices }: CoreSetup, { licensing, features }: Dependencies) { const router = http.createRouter(); const routeDependencies: RouteDependencies = { router, diff --git a/x-pack/plugins/xpack_legacy/server/plugin.ts b/x-pack/plugins/xpack_legacy/server/plugin.ts index 9bd42171c75d5d..ffef7117bbbd8a 100644 --- a/x-pack/plugins/xpack_legacy/server/plugin.ts +++ b/x-pack/plugins/xpack_legacy/server/plugin.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { first } from 'rxjs/operators'; - import { CoreStart, CoreSetup, @@ -23,11 +21,9 @@ interface SetupPluginDeps { export class XpackLegacyPlugin implements Plugin { constructor(private readonly initContext: PluginInitializerContext) {} - public async setup(core: CoreSetup, { usageCollection }: SetupPluginDeps) { + public setup(core: CoreSetup, { usageCollection }: SetupPluginDeps) { const router = core.http.createRouter(); - const globalConfig = await this.initContext.config.legacy.globalConfig$ - .pipe(first()) - .toPromise(); + const globalConfig = this.initContext.config.legacy.get(); const serverInfo = core.http.getServerInfo(); registerSettingsRoute({ From d152723bb1a1ea788e489dc29970fca23ae46702 Mon Sep 17 00:00:00 2001 From: Daniil Date: Mon, 8 Feb 2021 14:13:20 +0200 Subject: [PATCH 19/51] [Data Table] Add unit tests (#90173) * Move formatting columns into response handler * Use shared csv export * Cleanup files * Fix type * Fix translation * Filter out non-dimension values * Add unit tests for tableVisResponseHandler * Add unit tests for createFormattedTable * Add unit tests for addPercentageColumn * Add unit tests for usePagination * Add unit tests for useUiState * Add unit tests for table visualization * Add unit tests for TableVisBasic * Add unit tests for cell * Update license Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../table_vis_basic.test.tsx.snap | 115 +++++++++ .../table_vis_cell.test.tsx.snap | 13 ++ .../components/table_vis_basic.test.tsx | 130 +++++++++++ .../public/components/table_vis_cell.test.tsx | 36 +++ .../components/table_visualization.test.tsx | 69 ++++++ .../utils/add_percentage_column.test.ts | 79 +++++++ .../utils/create_formatted_table.test.ts | 218 ++++++++++++++++++ .../utils/table_vis_response_handler.test.ts | 171 ++++++++++++++ .../utils/table_vis_response_handler.ts | 5 +- .../public/utils/use/use_pagination.test.ts | 119 ++++++++++ .../public/utils/use/use_ui_state.test.ts | 163 +++++++++++++ 11 files changed, 1115 insertions(+), 3 deletions(-) create mode 100644 src/plugins/vis_type_table/public/components/__snapshots__/table_vis_basic.test.tsx.snap create mode 100644 src/plugins/vis_type_table/public/components/__snapshots__/table_vis_cell.test.tsx.snap create mode 100644 src/plugins/vis_type_table/public/components/table_vis_basic.test.tsx create mode 100644 src/plugins/vis_type_table/public/components/table_vis_cell.test.tsx create mode 100644 src/plugins/vis_type_table/public/components/table_visualization.test.tsx create mode 100644 src/plugins/vis_type_table/public/utils/add_percentage_column.test.ts create mode 100644 src/plugins/vis_type_table/public/utils/create_formatted_table.test.ts create mode 100644 src/plugins/vis_type_table/public/utils/table_vis_response_handler.test.ts create mode 100644 src/plugins/vis_type_table/public/utils/use/use_pagination.test.ts create mode 100644 src/plugins/vis_type_table/public/utils/use/use_ui_state.test.ts diff --git a/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_basic.test.tsx.snap b/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_basic.test.tsx.snap new file mode 100644 index 00000000000000..85cf9422630d68 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_basic.test.tsx.snap @@ -0,0 +1,115 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TableVisBasic should init data grid 1`] = ` + + + +`; + +exports[`TableVisBasic should init data grid with title provided - for split mode 1`] = ` + + +

+ My data table +

+
+ +
+`; + +exports[`TableVisBasic should render the toolbar 1`] = ` + + , + "showColumnSelector": false, + "showFullScreenSelector": false, + "showSortSelector": false, + "showStyleSelector": false, + } + } + /> + +`; diff --git a/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_cell.test.tsx.snap b/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_cell.test.tsx.snap new file mode 100644 index 00000000000000..b380b85f7f3564 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/__snapshots__/table_vis_cell.test.tsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`table vis cell should return a cell component with data in scope 1`] = ` +
+`; diff --git a/src/plugins/vis_type_table/public/components/table_vis_basic.test.tsx b/src/plugins/vis_type_table/public/components/table_vis_basic.test.tsx new file mode 100644 index 00000000000000..0fb74a41b5df0b --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_basic.test.tsx @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { TableVisBasic } from './table_vis_basic'; +import { FormattedColumn, TableVisConfig, TableVisUiState } from '../types'; +import { DatatableColumn } from 'src/plugins/expressions'; +import { createTableVisCell } from './table_vis_cell'; +import { createGridColumns } from './table_vis_columns'; + +jest.mock('./table_vis_columns', () => ({ + createGridColumns: jest.fn(() => []), +})); +jest.mock('./table_vis_cell', () => ({ + createTableVisCell: jest.fn(() => () => {}), +})); + +describe('TableVisBasic', () => { + const props = { + fireEvent: jest.fn(), + table: { + columns: [], + rows: [], + formattedColumns: { + test: { + formattedTotal: 100, + } as FormattedColumn, + }, + }, + visConfig: {} as TableVisConfig, + uiStateProps: { + sort: { + columnIndex: null, + direction: null, + }, + columnsWidth: [], + setColumnsWidth: jest.fn(), + setSort: jest.fn(), + }, + }; + + it('should init data grid', () => { + const comp = shallow(); + expect(comp).toMatchSnapshot(); + }); + + it('should init data grid with title provided - for split mode', () => { + const title = 'My data table'; + const comp = shallow(); + expect(comp).toMatchSnapshot(); + }); + + it('should render the toolbar', () => { + const comp = shallow( + + ); + expect(comp).toMatchSnapshot(); + }); + + it('should sort rows by column and pass the sorted rows for consumers', () => { + const uiStateProps = { + ...props.uiStateProps, + sort: { + columnIndex: 1, + direction: 'desc', + } as TableVisUiState['sort'], + }; + const table = { + columns: [{ id: 'first' }, { id: 'second' }] as DatatableColumn[], + rows: [ + { first: 1, second: 2 }, + { first: 3, second: 4 }, + { first: 5, second: 6 }, + ], + formattedColumns: {}, + }; + const sortedRows = [ + { first: 5, second: 6 }, + { first: 3, second: 4 }, + { first: 1, second: 2 }, + ]; + const comp = shallow( + + ); + expect(createTableVisCell).toHaveBeenCalledWith(sortedRows, table.formattedColumns); + expect(createGridColumns).toHaveBeenCalledWith( + table.columns, + sortedRows, + table.formattedColumns, + uiStateProps.columnsWidth, + props.fireEvent + ); + + const { onSort } = comp.find('EuiDataGrid').prop('sorting'); + // sort the first column + onSort([{ id: 'first', direction: 'asc' }]); + expect(uiStateProps.setSort).toHaveBeenCalledWith({ columnIndex: 0, direction: 'asc' }); + // sort the second column - should erase the first column sorting since there is only one level sorting available + onSort([ + { id: 'first', direction: 'asc' }, + { id: 'second', direction: 'desc' }, + ]); + expect(uiStateProps.setSort).toHaveBeenCalledWith({ columnIndex: 1, direction: 'desc' }); + }); + + it('should pass renderFooterCellValue for the total row', () => { + const comp = shallow( + + ); + const renderFooterCellValue: (props: any) => void = comp + .find('EuiDataGrid') + .prop('renderFooterCellValue'); + expect(renderFooterCellValue).toEqual(expect.any(Function)); + expect(renderFooterCellValue({ columnId: 'test' })).toEqual(100); + }); +}); diff --git a/src/plugins/vis_type_table/public/components/table_vis_cell.test.tsx b/src/plugins/vis_type_table/public/components/table_vis_cell.test.tsx new file mode 100644 index 00000000000000..322ceacbe002ea --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_cell.test.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiDataGridCellValueElementProps } from '@elastic/eui'; +import { createTableVisCell } from './table_vis_cell'; +import { FormattedColumns } from '../types'; + +describe('table vis cell', () => { + it('should return a cell component with data in scope', () => { + const rows = [{ first: 1, second: 2 }]; + const formattedColumns = ({ + second: { + formatter: { + convert: jest.fn(), + }, + }, + } as unknown) as FormattedColumns; + const Cell = createTableVisCell(rows, formattedColumns); + const cellProps = { + rowIndex: 0, + columnId: 'second', + } as EuiDataGridCellValueElementProps; + + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + expect(formattedColumns.second.formatter.convert).toHaveBeenLastCalledWith(2, 'html'); + }); +}); diff --git a/src/plugins/vis_type_table/public/components/table_visualization.test.tsx b/src/plugins/vis_type_table/public/components/table_visualization.test.tsx new file mode 100644 index 00000000000000..3d169531f57575 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_visualization.test.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +jest.mock('../utils', () => ({ + useUiState: jest.fn(() => 'uiState'), +})); + +import React from 'react'; +import { shallow } from 'enzyme'; +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { coreMock } from '../../../../core/public/mocks'; +import { TableVisConfig, TableVisData } from '../types'; +import TableVisualizationComponent from './table_visualization'; +import { useUiState } from '../utils'; + +describe('TableVisualizationComponent', () => { + const coreStartMock = coreMock.createStart(); + const handlers = ({ + done: jest.fn(), + uiState: 'uiState', + event: 'event', + } as unknown) as IInterpreterRenderHandlers; + const visData: TableVisData = { + table: { + columns: [], + rows: [], + formattedColumns: {}, + }, + tables: [], + }; + const visConfig = ({} as unknown) as TableVisConfig; + + it('should render the basic table', () => { + const comp = shallow( + + ); + expect(useUiState).toHaveBeenLastCalledWith(handlers.uiState); + expect(comp.find('.tbvChart__splitColumns').exists()).toBeFalsy(); + expect(comp.find('.tbvChart__split').exists()).toBeTruthy(); + }); + + it('should render split table', () => { + const comp = shallow( + + ); + expect(useUiState).toHaveBeenLastCalledWith(handlers.uiState); + expect(comp.find('.tbvChart__splitColumns').exists()).toBeTruthy(); + expect(comp.find('.tbvChart__split').exists()).toBeFalsy(); + expect(comp.find('[data-test-subj="tbvChart"]').children().prop('tables')).toEqual([]); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/add_percentage_column.test.ts b/src/plugins/vis_type_table/public/utils/add_percentage_column.test.ts new file mode 100644 index 00000000000000..0280637acc0999 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/add_percentage_column.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +jest.mock('../services', () => ({ + getFormatService: jest.fn(() => ({ + deserialize: jest.fn(() => 'formatter'), + })), +})); + +import { FieldFormat } from 'src/plugins/data/public'; +import { TableContext } from '../types'; +import { addPercentageColumn } from './add_percentage_column'; + +describe('', () => { + const table: TableContext = { + columns: [ + { id: 'col-0-1', name: 'Count', meta: { type: 'number' } }, + { id: 'col-1-5', name: 'category.keyword: Descending', meta: { type: 'string' } }, + { id: 'col-1-2', name: 'Gender', meta: { type: 'string' } }, + ], + rows: [ + { 'col-0-1': 1, 'col-1-5': "Women's Clothing", 'col-1-2': 'Men' }, + { 'col-0-1': 6, 'col-1-5': "Women's Clothing", 'col-1-2': 'Men' }, + ], + formattedColumns: { + 'col-0-1': { + sumTotal: 7, + title: 'Count', + filterable: false, + formatter: {} as FieldFormat, + }, + }, + }; + + it('should dnot add percentage column if it was not found', () => { + const output = addPercentageColumn(table, 'Extra'); + expect(output).toBe(table); + }); + + it('should add a brand new percentage column into table based on data', () => { + const output = addPercentageColumn(table, 'Count'); + const expectedColumns = [ + table.columns[0], + { + id: 'col-0-1-percents', + meta: { + params: { + id: 'percent', + }, + type: 'number', + }, + name: 'Count percentages', + }, + table.columns[1], + table.columns[2], + ]; + const expectedRows = [ + { ...table.rows[0], 'col-0-1-percents': 0.14285714285714285 }, + { ...table.rows[1], 'col-0-1-percents': 0.8571428571428571 }, + ]; + expect(output).toEqual({ + columns: expectedColumns, + rows: expectedRows, + formattedColumns: { + ...table.formattedColumns, + 'col-0-1-percents': { + filterable: false, + formatter: 'formatter', + title: 'Count percentages', + }, + }, + }); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/create_formatted_table.test.ts b/src/plugins/vis_type_table/public/utils/create_formatted_table.test.ts new file mode 100644 index 00000000000000..0a9c7320d43593 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/create_formatted_table.test.ts @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const mockDeserialize = jest.fn(() => ({})); + +jest.mock('../services', () => ({ + getFormatService: jest.fn(() => ({ + deserialize: mockDeserialize, + })), +})); + +import { Datatable } from 'src/plugins/expressions'; +import { AggTypes } from '../../common'; +import { TableVisConfig } from '../types'; +import { createFormattedTable } from './create_formatted_table'; + +const visConfig: TableVisConfig = { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + showToolbar: false, + showTotal: false, + totalFunc: AggTypes.SUM, + percentageCol: '', + title: 'My data table', + dimensions: { + buckets: [ + { + accessor: 1, + aggType: 'terms', + format: { id: 'string' }, + label: 'category_keyword: Descending', + params: {}, + }, + ], + metrics: [ + { accessor: 0, aggType: 'count', format: { id: 'number' }, label: 'Count', params: {} }, + ], + }, +}; + +describe('createFormattedTable', () => { + const table: Datatable = { + columns: [ + { id: 'col-0-1', name: 'Count', meta: { type: 'number' } }, + { id: 'col-1-5', name: 'category.keyword: Descending', meta: { type: 'string' } }, + { id: 'col-1-2', name: 'Gender', meta: { type: 'string' } }, + ], + rows: [ + { 'col-0-1': 1, 'col-1-5': "Women's Clothing", 'col-1-2': 'Men' }, + { 'col-0-1': 6, 'col-1-5': "Women's Clothing", 'col-1-2': 'Men' }, + ], + type: 'datatable', + }; + + it('should create formatted columns from data response and flter out non-dimension columns', () => { + const output = createFormattedTable(table, visConfig); + + // column to split is filtered out of real data representing + expect(output.columns).toEqual([table.columns[0], table.columns[1]]); + expect(output.rows).toEqual(table.rows); + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: {}, + title: 'Count', + }, + 'col-1-5': { + filterable: true, + formatter: {}, + title: 'category.keyword: Descending', + }, + }); + }); + + it('should add total sum to numeric columns', () => { + mockDeserialize.mockImplementationOnce(() => ({ + allowsNumericalAggregations: true, + convert: jest.fn((number) => number), + })); + const output = createFormattedTable(table, visConfig); + + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: { + allowsNumericalAggregations: true, + convert: expect.any(Function), + }, + title: 'Count', + sumTotal: 7, + total: 7, + formattedTotal: 7, + }, + 'col-1-5': { + filterable: true, + formatter: {}, + title: 'category.keyword: Descending', + }, + }); + }); + + it('should add total average to numeric columns', () => { + mockDeserialize.mockImplementationOnce(() => ({ + allowsNumericalAggregations: true, + convert: jest.fn((number) => number), + })); + const output = createFormattedTable(table, { ...visConfig, totalFunc: AggTypes.AVG }); + + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: { + allowsNumericalAggregations: true, + convert: expect.any(Function), + }, + title: 'Count', + sumTotal: 7, + total: 3.5, + formattedTotal: 3.5, + }, + 'col-1-5': { + filterable: true, + formatter: {}, + title: 'category.keyword: Descending', + }, + }); + }); + + it('should find min value as total', () => { + mockDeserialize.mockImplementationOnce(() => ({ + allowsNumericalAggregations: true, + convert: jest.fn((number) => number), + })); + const output = createFormattedTable(table, { ...visConfig, totalFunc: AggTypes.MIN }); + + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: { + allowsNumericalAggregations: true, + convert: expect.any(Function), + }, + title: 'Count', + sumTotal: 7, + total: 1, + formattedTotal: 1, + }, + 'col-1-5': { + filterable: true, + formatter: {}, + title: 'category.keyword: Descending', + }, + }); + }); + + it('should find max value as total', () => { + mockDeserialize.mockImplementationOnce(() => ({ + allowsNumericalAggregations: true, + convert: jest.fn((number) => number), + })); + const output = createFormattedTable(table, { ...visConfig, totalFunc: AggTypes.MAX }); + + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: { + allowsNumericalAggregations: true, + convert: expect.any(Function), + }, + title: 'Count', + sumTotal: 7, + total: 6, + formattedTotal: 6, + }, + 'col-1-5': { + filterable: true, + formatter: {}, + title: 'category.keyword: Descending', + }, + }); + }); + + it('should add rows count as total', () => { + mockDeserialize.mockImplementationOnce(() => ({ + allowsNumericalAggregations: true, + convert: jest.fn((number) => number), + })); + const output = createFormattedTable(table, { ...visConfig, totalFunc: AggTypes.COUNT }); + + expect(output.formattedColumns).toEqual({ + 'col-0-1': { + filterable: false, + formatter: { + allowsNumericalAggregations: true, + convert: expect.any(Function), + }, + title: 'Count', + sumTotal: 7, + total: 2, + formattedTotal: 2, + }, + 'col-1-5': { + filterable: true, + formattedTotal: 2, + formatter: {}, + sumTotal: "0Women's ClothingWomen's Clothing", + title: 'category.keyword: Descending', + total: 2, + }, + }); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/table_vis_response_handler.test.ts b/src/plugins/vis_type_table/public/utils/table_vis_response_handler.test.ts new file mode 100644 index 00000000000000..8adc535e802f0e --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/table_vis_response_handler.test.ts @@ -0,0 +1,171 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const mockConverter = jest.fn((name) => `By ${name}`); + +jest.mock('../services', () => ({ + getFormatService: jest.fn(() => ({ + deserialize: jest.fn(() => ({ + convert: mockConverter, + })), + })), +})); + +jest.mock('./create_formatted_table', () => ({ + createFormattedTable: jest.fn((data) => ({ + ...data, + formattedColumns: {}, + })), +})); + +jest.mock('./add_percentage_column', () => ({ + addPercentageColumn: jest.fn((data, column) => ({ + ...data, + percentage: `${column} with percentage`, + })), +})); + +import { Datatable } from 'src/plugins/expressions'; +import { SchemaConfig } from 'src/plugins/visualizations/public'; +import { AggTypes } from '../../common'; +import { TableGroup, TableVisConfig } from '../types'; +import { addPercentageColumn } from './add_percentage_column'; +import { createFormattedTable } from './create_formatted_table'; +import { tableVisResponseHandler } from './table_vis_response_handler'; + +const visConfig: TableVisConfig = { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + showToolbar: false, + showTotal: false, + totalFunc: AggTypes.AVG, + percentageCol: '', + title: 'My data table', + dimensions: { + buckets: [], + metrics: [], + }, +}; + +describe('tableVisResponseHandler', () => { + describe('basic table', () => { + const input: Datatable = { + columns: [], + rows: [], + type: 'datatable', + }; + + it('should create formatted table for basic usage', () => { + const output = tableVisResponseHandler(input, visConfig); + + expect(output.direction).toBeUndefined(); + expect(output.tables.length).toEqual(0); + expect(addPercentageColumn).not.toHaveBeenCalled(); + expect(createFormattedTable).toHaveBeenCalledWith(input, visConfig); + expect(output.table).toEqual({ + ...input, + formattedColumns: {}, + }); + }); + + it('should add a percentage column if it is set', () => { + const output = tableVisResponseHandler(input, { ...visConfig, percentageCol: 'Count' }); + expect(output.table).toEqual({ + ...input, + formattedColumns: {}, + percentage: 'Count with percentage', + }); + }); + }); + + describe('split table', () => { + const input: Datatable = { + columns: [ + { id: 'col-0-1', name: 'Count', meta: { type: 'number' } }, + { id: 'col-1-2', name: 'Gender', meta: { type: 'string' } }, + ], + rows: [ + { 'col-0-1': 1, 'col-1-2': 'Men' }, + { 'col-0-1': 3, 'col-1-2': 'Women' }, + { 'col-0-1': 6, 'col-1-2': 'Men' }, + ], + type: 'datatable', + }; + const split: SchemaConfig[] = [ + { + accessor: 1, + label: 'Split', + format: {}, + params: {}, + aggType: 'terms', + }, + ]; + const expectedOutput: TableGroup[] = [ + { + title: 'By Men: Gender', + table: { + columns: input.columns, + rows: [input.rows[0], input.rows[2]], + formattedColumns: {}, + }, + }, + { + title: 'By Women: Gender', + table: { + columns: input.columns, + rows: [input.rows[1]], + formattedColumns: {}, + }, + }, + ]; + + it('should split data by row', () => { + const output = tableVisResponseHandler(input, { + ...visConfig, + dimensions: { ...visConfig.dimensions, splitRow: split }, + }); + + expect(output.direction).toEqual('row'); + expect(output.table).toBeUndefined(); + expect(output.tables).toEqual(expectedOutput); + }); + + it('should split data by column', () => { + const output = tableVisResponseHandler(input, { + ...visConfig, + dimensions: { ...visConfig.dimensions, splitColumn: split }, + }); + + expect(output.direction).toEqual('column'); + expect(output.table).toBeUndefined(); + expect(output.tables).toEqual(expectedOutput); + }); + + it('should add percentage columns to each table', () => { + const output = tableVisResponseHandler(input, { + ...visConfig, + percentageCol: 'Count', + dimensions: { ...visConfig.dimensions, splitColumn: split }, + }); + + expect(output.direction).toEqual('column'); + expect(output.table).toBeUndefined(); + expect(output.tables).toEqual([ + { + ...expectedOutput[0], + table: { ...expectedOutput[0].table, percentage: 'Count with percentage' }, + }, + { + ...expectedOutput[1], + table: { ...expectedOutput[1].table, percentage: 'Count with percentage' }, + }, + ]); + }); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/table_vis_response_handler.ts b/src/plugins/vis_type_table/public/utils/table_vis_response_handler.ts index e0919671135ea5..69521c20cddfed 100644 --- a/src/plugins/vis_type_table/public/utils/table_vis_response_handler.ts +++ b/src/plugins/vis_type_table/public/utils/table_vis_response_handler.ts @@ -27,7 +27,6 @@ export function tableVisResponseHandler(input: Datatable, visConfig: TableVisCon const splitColumnIndex = split[0].accessor; const splitColumnFormatter = getFormatService().deserialize(split[0].format); const splitColumn = input.columns[splitColumnIndex]; - const columns = input.columns.filter((c, idx) => idx !== splitColumnIndex); const splitMap: { [key: string]: number } = {}; let splitIndex = 0; @@ -39,7 +38,7 @@ export function tableVisResponseHandler(input: Datatable, visConfig: TableVisCon const tableGroup: TableGroup = { title: `${splitColumnFormatter.convert(splitValue)}: ${splitColumn.name}`, table: { - columns, + columns: input.columns, rows: [], formattedColumns: {}, }, @@ -53,7 +52,7 @@ export function tableVisResponseHandler(input: Datatable, visConfig: TableVisCon }); tables.forEach((tg) => { - tg.table = createFormattedTable({ ...tg.table, columns: input.columns }, visConfig); + tg.table = createFormattedTable(tg.table, visConfig); if (visConfig.percentageCol) { tg.table = addPercentageColumn(tg.table, visConfig.percentageCol); diff --git a/src/plugins/vis_type_table/public/utils/use/use_pagination.test.ts b/src/plugins/vis_type_table/public/utils/use/use_pagination.test.ts new file mode 100644 index 00000000000000..3d0b58aa6c8a34 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/use/use_pagination.test.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { AggTypes } from '../../../common'; +import { usePagination } from './use_pagination'; + +describe('usePagination', () => { + const visParams = { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + showToolbar: false, + showTotal: false, + totalFunc: AggTypes.SUM, + percentageCol: '', + title: 'My data table', + }; + + it('should set up pagination on init', () => { + const { result } = renderHook(() => usePagination(visParams, 15)); + + expect(result.current).toEqual({ + pageIndex: 0, + pageSize: 10, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); + + it('should skip setting the pagination if perPage is not set', () => { + const { result } = renderHook(() => usePagination({ ...visParams, perPage: '' }, 15)); + + expect(result.current).toBeUndefined(); + }); + + it('should change the page via callback', () => { + const { result } = renderHook(() => usePagination(visParams, 15)); + + act(() => { + // change the page to the next one + result.current?.onChangePage(1); + }); + + expect(result.current).toEqual({ + pageIndex: 1, + pageSize: 10, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); + + it('should change items per page via callback', () => { + const { result } = renderHook(() => usePagination(visParams, 15)); + + act(() => { + // change the page to the next one + result.current?.onChangeItemsPerPage(20); + }); + + expect(result.current).toEqual({ + pageIndex: 0, + pageSize: 20, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); + + it('should change the page when props were changed', () => { + const { result, rerender } = renderHook( + (props) => usePagination(props.visParams, props.rowCount), + { + initialProps: { + visParams, + rowCount: 15, + }, + } + ); + const updatedParams = { ...visParams, perPage: 5 }; + + // change items per page count + rerender({ visParams: updatedParams, rowCount: 15 }); + + expect(result.current).toEqual({ + pageIndex: 0, + pageSize: 5, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + + act(() => { + // change the page to the last one - 3 + result.current?.onChangePage(3); + }); + + expect(result.current).toEqual({ + pageIndex: 3, + pageSize: 5, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + + // decrease the rows count + rerender({ visParams: updatedParams, rowCount: 10 }); + + // should switch to the last available page + expect(result.current).toEqual({ + pageIndex: 1, + pageSize: 5, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/use/use_ui_state.test.ts b/src/plugins/vis_type_table/public/utils/use/use_ui_state.test.ts new file mode 100644 index 00000000000000..be1f9d3a10cf76 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/use/use_ui_state.test.ts @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import type { PersistedState } from 'src/plugins/visualizations/public'; +import { TableVisUiState } from '../../types'; +import { useUiState } from './use_ui_state'; + +describe('useUiState', () => { + let uiState: PersistedState; + + beforeEach(() => { + uiState = { + get: jest.fn(), + on: jest.fn(), + off: jest.fn(), + set: jest.fn(), + } as any; + }); + + it("should init default columnsWidth & sort if uiState doesn't have it set", () => { + const { result } = renderHook(() => useUiState(uiState)); + + expect(result.current).toEqual({ + columnsWidth: [], + sort: { + columnIndex: null, + direction: null, + }, + setColumnsWidth: expect.any(Function), + setSort: expect.any(Function), + }); + }); + + it('should subscribe on uiState changes and update local state', async () => { + const { result, unmount, waitForNextUpdate } = renderHook(() => useUiState(uiState)); + + expect(uiState.on).toHaveBeenCalledWith('change', expect.any(Function)); + // @ts-expect-error + const updateOnChange = uiState.on.mock.calls[0][1]; + + uiState.getChanges = jest.fn(() => ({ + vis: { + params: { + sort: { + columnIndex: 1, + direction: 'asc', + }, + colWidth: [], + }, + }, + })); + + act(() => { + updateOnChange(); + }); + + await waitForNextUpdate(); + + // should update local state with new values + expect(result.current).toEqual({ + columnsWidth: [], + sort: { + columnIndex: 1, + direction: 'asc', + }, + setColumnsWidth: expect.any(Function), + setSort: expect.any(Function), + }); + + act(() => { + updateOnChange(); + }); + + // should skip setting the state again if it is equal + expect(result.current).toEqual({ + columnsWidth: [], + sort: { + columnIndex: 1, + direction: 'asc', + }, + setColumnsWidth: expect.any(Function), + setSort: expect.any(Function), + }); + + unmount(); + + expect(uiState.off).toHaveBeenCalledWith('change', updateOnChange); + }); + + describe('updating uiState through callbacks', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + it('should update the uiState with new sort', async () => { + const { result } = renderHook(() => useUiState(uiState)); + const newSort: TableVisUiState['sort'] = { + columnIndex: 5, + direction: 'desc', + }; + + act(() => { + result.current.setSort(newSort); + }); + + expect(result.current.sort).toEqual(newSort); + + jest.runAllTimers(); + + expect(uiState.set).toHaveBeenCalledTimes(1); + expect(uiState.set).toHaveBeenCalledWith('vis.params.sort', newSort); + }); + + it('should update the uiState with new columns width', async () => { + const { result } = renderHook(() => useUiState(uiState)); + const col1 = { colIndex: 0, width: 300 }; + const col2 = { colIndex: 1, width: 100 }; + + // set width of a column + act(() => { + result.current.setColumnsWidth(col1); + }); + + expect(result.current.columnsWidth).toEqual([col1]); + + jest.runAllTimers(); + + expect(uiState.set).toHaveBeenCalledTimes(1); + expect(uiState.set).toHaveBeenLastCalledWith('vis.params.colWidth', [col1]); + + // set width of another column + act(() => { + result.current.setColumnsWidth(col2); + }); + + jest.runAllTimers(); + + expect(uiState.set).toHaveBeenCalledTimes(2); + expect(uiState.set).toHaveBeenLastCalledWith('vis.params.colWidth', [col1, col2]); + + const updatedCol1 = { colIndex: 0, width: 200 }; + // update width of existing column + act(() => { + result.current.setColumnsWidth(updatedCol1); + }); + + jest.runAllTimers(); + + expect(uiState.set).toHaveBeenCalledTimes(3); + expect(uiState.set).toHaveBeenCalledWith('vis.params.colWidth', [updatedCol1, col2]); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + }); +}); From 4a1946b7ae980181b3becb021baec2f18f9373ed Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 8 Feb 2021 13:48:18 +0100 Subject: [PATCH 20/51] [Lens] Retain column config (#90048) --- .../visualization.test.tsx | 34 +++++++++++++++++++ .../datatable_visualization/visualization.tsx | 12 ++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index 25275ba8e2249b..2a6228f16867dc 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -108,6 +108,40 @@ describe('Datatable Visualization', () => { expect(suggestions.length).toBeGreaterThan(0); }); + it('should retain width and hidden config from existing state', () => { + const suggestions = datatableVisualization.getSuggestions({ + state: { + layerId: 'first', + columns: [ + { columnId: 'col1', width: 123 }, + { columnId: 'col2', hidden: true }, + ], + sorting: { + columnId: 'col1', + direction: 'asc', + }, + }, + table: { + isMultiRow: true, + layerId: 'first', + changeType: 'initial', + columns: [numCol('col1'), strCol('col2'), strCol('col3')], + }, + keptLayerIds: [], + }); + + expect(suggestions.length).toBeGreaterThan(0); + expect(suggestions[0].state.columns).toEqual([ + { columnId: 'col1', width: 123 }, + { columnId: 'col2', hidden: true }, + { columnId: 'col3' }, + ]); + expect(suggestions[0].state.sorting).toEqual({ + columnId: 'col1', + direction: 'asc', + }); + }); + it('should not make suggestions when the table is unchanged', () => { const suggestions = datatableVisualization.getSuggestions({ state: { diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 77fda43c37fef9..9625a814c79589 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -98,6 +98,12 @@ export const datatableVisualization: Visualization ) { return []; } + const oldColumnSettings: Record = {}; + if (state) { + state.columns.forEach((column) => { + oldColumnSettings[column.columnId] = column; + }); + } const title = table.changeType === 'unchanged' ? i18n.translate('xpack.lens.datatable.suggestionLabel', { @@ -126,8 +132,12 @@ export const datatableVisualization: Visualization // table with >= 10 columns will have a score of 0.4, fewer columns reduce score score: (Math.min(table.columns.length, 10) / 10) * 0.4, state: { + ...(state || {}), layerId: table.layerId, - columns: table.columns.map((col) => ({ columnId: col.columnId })), + columns: table.columns.map((col) => ({ + ...(oldColumnSettings[col.columnId] || {}), + columnId: col.columnId, + })), }, previewIcon: LensIconChartDatatable, // tables are hidden from suggestion bar, but used for drag & drop and chart switching From be9f7c3dc99658d315dd21963aaf243a34a85e2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Mon, 8 Feb 2021 14:37:32 +0100 Subject: [PATCH 21/51] [APM] Export ProcessorEvent type (#90540) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/apm/server/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index f0524e67d324cc..da3afd03513f05 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -97,3 +97,4 @@ export const plugin = (initContext: PluginInitializerContext) => new APMPlugin(initContext); export { APMPlugin, APMPluginSetup } from './plugin'; +export type { ProcessorEvent } from '../common/processor_event'; From d201ed756f79bae54e1d4b03c44ecdf3ea3bdda6 Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Mon, 8 Feb 2021 13:40:41 +0000 Subject: [PATCH 22/51] [APM-UI][E2E] use githubNotify step (#90514) --- .ci/end2end.groovy | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.ci/end2end.groovy b/.ci/end2end.groovy index 025836a90204c5..a89ff166bf32e9 100644 --- a/.ci/end2end.groovy +++ b/.ci/end2end.groovy @@ -121,9 +121,15 @@ pipeline { } def notifyStatus(String description, String status) { - withGithubNotify.notify('end2end-for-apm-ui', description, status, getBlueoceanTabURL('pipeline')) + notify(context: 'end2end-for-apm-ui', description: description, status: status, targetUrl: getBlueoceanTabURL('pipeline')) } def notifyTestStatus(String description, String status) { - withGithubNotify.notify('end2end-for-apm-ui', description, status, getBlueoceanTabURL('tests')) + notify(context: 'end2end-for-apm-ui', description: description, status: status, targetUrl: getBlueoceanTabURL('tests')) +} + +def notify(Map args = [:]) { + retryWithSleep(retries: 2, seconds: 5, backoff: true) { + githubNotify(context: args.context, description: args.description, status: args.status, targetUrl: args.targetUrl) + } } From 3d2373325c76dc595d53f78209ff98b86e57ff81 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Mon, 8 Feb 2021 15:34:19 +0100 Subject: [PATCH 23/51] [Discover] Inline state modifying function to react (#89543) --- .../public/application/angular/discover.js | 111 +--------- .../application/angular/discover_legacy.html | 11 - .../angular/doc_table/actions/columns.ts | 61 ++++++ .../components/create_discover_directive.ts | 10 - .../application/components/discover.test.tsx | 25 +-- .../application/components/discover.tsx | 205 ++++++++++-------- .../components/discover_topnav.test.tsx | 68 ++++++ .../components/discover_topnav.tsx | 71 ++++++ .../sidebar/discover_index_pattern.test.tsx | 40 ++-- .../sidebar/discover_index_pattern.tsx | 60 ++++- .../sidebar/discover_sidebar.test.tsx | 8 +- .../components/sidebar/discover_sidebar.tsx | 107 ++------- .../discover_sidebar_responsive.test.tsx | 22 +- .../sidebar/discover_sidebar_responsive.tsx | 26 ++- .../public/application/components/types.ts | 55 +---- .../application/helpers/popularize_field.ts | 4 +- 16 files changed, 480 insertions(+), 404 deletions(-) create mode 100644 src/plugins/discover/public/application/components/discover_topnav.test.tsx create mode 100644 src/plugins/discover/public/application/components/discover_topnav.tsx diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index b22bb6dc713422..af63485507d05c 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -22,7 +22,6 @@ import { syncQueryStateWithUrl, } from '../../../../data/public'; import { getSortArray } from './doc_table'; -import * as columnActions from './doc_table/actions/columns'; import indexTemplateLegacy from './discover_legacy.html'; import { addHelpMenuToAppChrome } from '../components/help_menu/help_menu_util'; import { discoverResponseHandler } from './response_handler'; @@ -43,13 +42,9 @@ import { setBreadcrumbsTitle, } from '../helpers/breadcrumbs'; import { validateTimeRange } from '../helpers/validate_time_range'; -import { popularizeField } from '../helpers/popularize_field'; -import { getSwitchIndexPatternAppState } from '../helpers/get_switch_index_pattern_app_state'; import { addFatalError } from '../../../../kibana_legacy/public'; -import { METRIC_TYPE } from '@kbn/analytics'; import { DEFAULT_COLUMNS_SETTING, - MODIFY_COLUMNS_ON_SWITCH, SAMPLE_SIZE_SETTING, SEARCH_FIELDS_FROM_SOURCE, SEARCH_ON_PAGE_LOAD_SETTING, @@ -69,12 +64,10 @@ const { chrome, data, history: getHistory, - indexPatterns, filterManager, timefilter, toastNotifications, uiSettings: config, - trackUiMetric, } = getServices(); const fetchStatuses = { @@ -292,21 +285,6 @@ function discoverController($route, $scope, Promise) { } ); - $scope.setIndexPattern = async (id) => { - const nextIndexPattern = await indexPatterns.get(id); - if (nextIndexPattern) { - const nextAppState = getSwitchIndexPatternAppState( - $scope.indexPattern, - nextIndexPattern, - $scope.state.columns, - $scope.state.sort, - config.get(MODIFY_COLUMNS_ON_SWITCH), - $scope.useNewFieldsApi - ); - await setAppState(nextAppState); - } - }; - // update data source when filters update subscriptions.add( subscribeWithScope( @@ -327,6 +305,7 @@ function discoverController($route, $scope, Promise) { sampleSize: config.get(SAMPLE_SIZE_SETTING), timefield: getTimeField(), savedSearch: savedSearch, + services, indexPatternList: $route.current.locals.savedObjects.ip.list, config: config, setHeaderActionMenu: getHeaderActionMenuMounter(), @@ -340,18 +319,8 @@ function discoverController($route, $scope, Promise) { requests: new RequestAdapter(), }); - $scope.timefilterUpdateHandler = (ranges) => { - timefilter.setTime({ - from: moment(ranges.from).toISOString(), - to: moment(ranges.to).toISOString(), - mode: 'absolute', - }); - }; $scope.minimumVisibleRows = 50; $scope.fetchStatus = fetchStatuses.UNINITIALIZED; - $scope.showSaveQuery = capabilities.discover.saveQuery; - $scope.showTimeCol = - !config.get('doc_table:hideTimeColumn', false) && $scope.indexPattern.timeFieldName; let abortController; $scope.$on('$destroy', () => { @@ -495,12 +464,6 @@ function discoverController($route, $scope, Promise) { ) ); - $scope.changeInterval = (interval) => { - if (interval) { - setAppState({ interval }); - } - }; - $scope.$watchMulti( ['rows', 'fetchStatus'], (function updateResultState() { @@ -606,19 +569,6 @@ function discoverController($route, $scope, Promise) { } }; - $scope.updateSavedQueryId = (newSavedQueryId) => { - if (newSavedQueryId) { - setAppState({ savedQuery: newSavedQueryId }); - } else { - // remove savedQueryId from state - const state = { - ...appStateContainer.getState(), - }; - delete state.savedQuery; - appStateContainer.set(state); - } - }; - function getDimensions(aggs, timeRange) { const [metric, agg] = aggs; agg.params.timeRange = timeRange; @@ -752,65 +702,6 @@ function discoverController($route, $scope, Promise) { return Promise.resolve(); }; - $scope.setSortOrder = function setSortOrder(sort) { - setAppState({ sort }); - }; - - // TODO: On array fields, negating does not negate the combination, rather all terms - $scope.filterQuery = function (field, values, operation) { - const { indexPattern } = $scope; - - popularizeField(indexPattern, field.name, indexPatterns); - const newFilters = esFilters.generateFilters( - filterManager, - field, - values, - operation, - $scope.indexPattern.id - ); - if (trackUiMetric) { - trackUiMetric(METRIC_TYPE.CLICK, 'filter_added'); - } - return filterManager.addFilters(newFilters); - }; - - $scope.addColumn = function addColumn(columnName) { - const { indexPattern, useNewFieldsApi } = $scope; - if (capabilities.discover.save) { - popularizeField(indexPattern, columnName, indexPatterns); - } - const columns = columnActions.addColumn($scope.state.columns, columnName, useNewFieldsApi); - setAppState({ columns }); - }; - - $scope.removeColumn = function removeColumn(columnName) { - const { indexPattern, useNewFieldsApi } = $scope; - if (capabilities.discover.save) { - popularizeField(indexPattern, columnName, indexPatterns); - } - const columns = columnActions.removeColumn($scope.state.columns, columnName, useNewFieldsApi); - // The state's sort property is an array of [sortByColumn,sortDirection] - const sort = $scope.state.sort.length - ? $scope.state.sort.filter((subArr) => subArr[0] !== columnName) - : []; - setAppState({ columns, sort }); - }; - - $scope.moveColumn = function moveColumn(columnName, newIndex) { - const columns = columnActions.moveColumn($scope.state.columns, columnName, newIndex); - setAppState({ columns }); - }; - - $scope.setColumns = function setColumns(columns) { - // remove first element of columns if it's the configured timeFieldName, which is prepended automatically - const actualColumns = - $scope.indexPattern.timeFieldName && $scope.indexPattern.timeFieldName === columns[0] - ? columns.slice(1) - : columns; - $scope.state = { ...$scope.state, columns: actualColumns }; - setAppState({ columns: actualColumns }); - }; - async function setupVisualization() { // If no timefield has been specified we don't create a histogram of messages if (!getTimeField()) return; diff --git a/src/plugins/discover/public/application/angular/discover_legacy.html b/src/plugins/discover/public/application/angular/discover_legacy.html index 83a9cf23c85f3d..dc18b7929318bd 100644 --- a/src/plugins/discover/public/application/angular/discover_legacy.html +++ b/src/plugins/discover/public/application/angular/discover_legacy.html @@ -8,27 +8,16 @@ hits="hits" index-pattern="indexPattern" minimum-visible-rows="minimumVisibleRows" - on-add-column="addColumn" - on-add-filter="filterQuery" - on-move-column="moveColumn" - on-change-interval="changeInterval" - on-remove-column="removeColumn" - on-set-columns="setColumns" on-skip-bottom-button-click="onSkipBottomButtonClick" - on-sort="setSortOrder" opts="opts" reset-query="resetQuery" result-state="resultState" rows="rows" search-source="searchSource" - set-index-pattern="setIndexPattern" - show-save-query="showSaveQuery" state="state" - time-filter-update-handler="timefilterUpdateHandler" time-range="timeRange" top-nav-menu="topNavMenu" update-query="handleRefresh" - update-saved-query-id="updateSavedQueryId" use-new-fields-api="useNewFieldsApi" unmapped-fields-config="unmappedFieldsConfig" > diff --git a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts b/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts index 946f11024360f6..53ced59b17c5d0 100644 --- a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts +++ b/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts @@ -5,6 +5,10 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import { Capabilities } from 'kibana/public'; +import { popularizeField } from '../../../helpers/popularize_field'; +import { IndexPattern, IndexPatternsContract } from '../../../../kibana_services'; +import { AppState } from '../../discover_state'; /** * Helper function to provide a fallback to a single _source column if the given array of columns @@ -47,3 +51,60 @@ export function moveColumn(columns: string[], columnName: string, newIndex: numb modifiedColumns.splice(newIndex, 0, columnName); // insert before new index return modifiedColumns; } + +export function getStateColumnActions({ + capabilities, + indexPattern, + indexPatterns, + useNewFieldsApi, + setAppState, + state, +}: { + capabilities: Capabilities; + indexPattern: IndexPattern; + indexPatterns: IndexPatternsContract; + useNewFieldsApi: boolean; + setAppState: (state: Partial) => void; + state: AppState; +}) { + function onAddColumn(columnName: string) { + if (capabilities.discover.save) { + popularizeField(indexPattern, columnName, indexPatterns); + } + const columns = addColumn(state.columns || [], columnName, useNewFieldsApi); + setAppState({ columns }); + } + + function onRemoveColumn(columnName: string) { + if (capabilities.discover.save) { + popularizeField(indexPattern, columnName, indexPatterns); + } + const columns = removeColumn(state.columns || [], columnName, useNewFieldsApi); + // The state's sort property is an array of [sortByColumn,sortDirection] + const sort = + state.sort && state.sort.length + ? state.sort.filter((subArr) => subArr[0] !== columnName) + : []; + setAppState({ columns, sort }); + } + + function onMoveColumn(columnName: string, newIndex: number) { + const columns = moveColumn(state.columns || [], columnName, newIndex); + setAppState({ columns }); + } + + function onSetColumns(columns: string[]) { + // remove first element of columns if it's the configured timeFieldName, which is prepended automatically + const actualColumns = + indexPattern.timeFieldName && indexPattern.timeFieldName === columns[0] + ? columns.slice(1) + : columns; + setAppState({ columns: actualColumns }); + } + return { + onAddColumn, + onRemoveColumn, + onMoveColumn, + onSetColumns, + }; +} diff --git a/src/plugins/discover/public/application/components/create_discover_directive.ts b/src/plugins/discover/public/application/components/create_discover_directive.ts index 2a88c1b7131329..8d1360aeaddada 100644 --- a/src/plugins/discover/public/application/components/create_discover_directive.ts +++ b/src/plugins/discover/public/application/components/create_discover_directive.ts @@ -5,7 +5,6 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import { Discover } from './discover'; export function createDiscoverDirective(reactDirective: any) { @@ -18,24 +17,15 @@ export function createDiscoverDirective(reactDirective: any) { ['hits', { watchDepth: 'reference' }], ['indexPattern', { watchDepth: 'reference' }], ['minimumVisibleRows', { watchDepth: 'reference' }], - ['onAddColumn', { watchDepth: 'reference' }], - ['onAddFilter', { watchDepth: 'reference' }], - ['onChangeInterval', { watchDepth: 'reference' }], - ['onMoveColumn', { watchDepth: 'reference' }], - ['onRemoveColumn', { watchDepth: 'reference' }], - ['onSetColumns', { watchDepth: 'reference' }], ['onSkipBottomButtonClick', { watchDepth: 'reference' }], - ['onSort', { watchDepth: 'reference' }], ['opts', { watchDepth: 'reference' }], ['resetQuery', { watchDepth: 'reference' }], ['resultState', { watchDepth: 'reference' }], ['rows', { watchDepth: 'reference' }], ['savedSearch', { watchDepth: 'reference' }], ['searchSource', { watchDepth: 'reference' }], - ['setIndexPattern', { watchDepth: 'reference' }], ['showSaveQuery', { watchDepth: 'reference' }], ['state', { watchDepth: 'reference' }], - ['timefilterUpdateHandler', { watchDepth: 'reference' }], ['timeRange', { watchDepth: 'reference' }], ['topNavMenu', { watchDepth: 'reference' }], ['updateQuery', { watchDepth: 'reference' }], diff --git a/src/plugins/discover/public/application/components/discover.test.tsx b/src/plugins/discover/public/application/components/discover.test.tsx index bb0014f4278a11..f0f11558abd65c 100644 --- a/src/plugins/discover/public/application/components/discover.test.tsx +++ b/src/plugins/discover/public/application/components/discover.test.tsx @@ -11,6 +11,7 @@ import { shallowWithIntl } from '@kbn/test/jest'; import { Discover } from './discover'; import { esHits } from '../../__mocks__/es_hits'; import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { DiscoverServices } from '../../build_services'; import { GetStateReturn } from '../angular/discover_state'; import { savedSearchMock } from '../../__mocks__/saved_search'; import { createSearchSourceMock } from '../../../../data/common/search/search_source/mocks'; @@ -46,7 +47,14 @@ jest.mock('../../kibana_services', () => { function getProps(indexPattern: IndexPattern): DiscoverProps { const searchSourceMock = createSearchSourceMock({}); - const state = ({} as unknown) as GetStateReturn; + const services = ({ + capabilities: { + discover: { + save: true, + }, + }, + uiSettings: mockUiSettings, + } as unknown) as DiscoverServices; return { fetch: jest.fn(), @@ -56,14 +64,7 @@ function getProps(indexPattern: IndexPattern): DiscoverProps { hits: esHits.length, indexPattern, minimumVisibleRows: 10, - onAddColumn: jest.fn(), - onAddFilter: jest.fn(), - onChangeInterval: jest.fn(), - onMoveColumn: jest.fn(), - onRemoveColumn: jest.fn(), - onSetColumns: jest.fn(), onSkipBottomButtonClick: jest.fn(), - onSort: jest.fn(), opts: { config: mockUiSettings, data: dataPluginMock.createStartContract(), @@ -74,20 +75,18 @@ function getProps(indexPattern: IndexPattern): DiscoverProps { navigateTo: jest.fn(), sampleSize: 10, savedSearch: savedSearchMock, - setAppState: jest.fn(), setHeaderActionMenu: jest.fn(), - stateContainer: state, timefield: indexPattern.timeFieldName || '', + setAppState: jest.fn(), + services, + stateContainer: {} as GetStateReturn, }, resetQuery: jest.fn(), resultState: 'ready', rows: esHits, searchSource: searchSourceMock, - setIndexPattern: jest.fn(), state: { columns: [] }, - timefilterUpdateHandler: jest.fn(), updateQuery: jest.fn(), - updateSavedQueryId: jest.fn(), }; } diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx index baee0623f0b5ae..99baa30e18c7a7 100644 --- a/src/plugins/discover/public/application/components/discover.tsx +++ b/src/plugins/discover/public/application/components/discover.tsx @@ -5,9 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import './discover.scss'; -import React, { useState, useRef, useMemo } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { EuiButtonEmpty, EuiButtonIcon, @@ -21,37 +20,34 @@ import { EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { METRIC_TYPE } from '@kbn/analytics'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import classNames from 'classnames'; import { HitsCounter } from './hits_counter'; import { TimechartHeader } from './timechart_header'; -import { getServices } from '../../kibana_services'; import { DiscoverHistogram, DiscoverUninitialized } from '../angular/directives'; import { DiscoverNoResults } from './no_results'; import { LoadingSpinner } from './loading_spinner/loading_spinner'; -import { DocTableLegacy, DocTableLegacyProps } from '../angular/doc_table/create_doc_table_react'; +import { DocTableLegacy } from '../angular/doc_table/create_doc_table_react'; import { SkipBottomButton } from './skip_bottom_button'; -import { search } from '../../../../data/public'; -import { - DiscoverSidebarResponsive, - DiscoverSidebarResponsiveProps, -} from './sidebar/discover_sidebar_responsive'; +import { esFilters, IndexPatternField, search } from '../../../../data/public'; +import { DiscoverSidebarResponsive } from './sidebar'; import { DiscoverProps } from './types'; import { getDisplayedColumns } from '../helpers/columns'; import { SortPairArr } from '../angular/doc_table/lib/get_sort'; -import { DiscoverGrid, DiscoverGridProps } from './discover_grid/discover_grid'; import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; +import { popularizeField } from '../helpers/popularize_field'; +import { getStateColumnActions } from '../angular/doc_table/actions/columns'; +import { DocViewFilterFn } from '../doc_views/doc_views_types'; +import { DiscoverGrid } from './discover_grid/discover_grid'; +import { DiscoverTopNav } from './discover_topnav'; import { ElasticSearchHit } from '../doc_views/doc_views_types'; -import { getTopNavLinks } from './top_nav/get_top_nav_links'; -const DocTableLegacyMemoized = React.memo((props: DocTableLegacyProps) => ( - -)); -const SidebarMemoized = React.memo((props: DiscoverSidebarResponsiveProps) => ( - -)); - -const DataGridMemoized = React.memo((props: DiscoverGridProps) => ); +const DocTableLegacyMemoized = React.memo(DocTableLegacy); +const SidebarMemoized = React.memo(DiscoverSidebarResponsive); +const DataGridMemoized = React.memo(DiscoverGrid); +const TopNavMemoized = React.memo(DiscoverTopNav); export function Discover({ fetch, @@ -62,25 +58,15 @@ export function Discover({ hits, indexPattern, minimumVisibleRows, - onAddColumn, - onAddFilter, - onChangeInterval, - onMoveColumn, - onRemoveColumn, - onSetColumns, onSkipBottomButtonClick, - onSort, opts, resetQuery, resultState, rows, searchSource, - setIndexPattern, state, - timefilterUpdateHandler, timeRange, updateQuery, - updateSavedQueryId, unmappedFieldsConfig, }: DiscoverProps) { const [expandedDoc, setExpandedDoc] = useState(undefined); @@ -92,28 +78,9 @@ export function Discover({ }; const [toggleOn, toggleChart] = useState(true); + const { savedSearch, indexPatternList, config, services, data, setAppState } = opts; + const { trackUiMetric, capabilities, indexPatterns } = services; const [isSidebarClosed, setIsSidebarClosed] = useState(false); - const services = useMemo(() => getServices(), []); - const topNavMenu = useMemo( - () => - getTopNavLinks({ - getFieldCounts: opts.getFieldCounts, - indexPattern, - inspectorAdapters: opts.inspectorAdapters, - navigateTo: opts.navigateTo, - savedSearch: opts.savedSearch, - services, - state: opts.stateContainer, - onOpenInspector: () => { - // prevent overlapping - setExpandedDoc(undefined); - }, - }), - [indexPattern, opts, services] - ); - const { TopNavMenu } = services.navigation.ui; - const { trackUiMetric } = services; - const { savedSearch, indexPatternList, config } = opts; const bucketAggConfig = opts.chartAggConfigs?.aggs[1]; const bucketInterval = bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig) @@ -123,6 +90,95 @@ export function Discover({ const isLegacy = services.uiSettings.get('doc_table:legacy'); const useNewFieldsApi = !services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); + const { onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useMemo( + () => + getStateColumnActions({ + capabilities, + indexPattern, + indexPatterns, + setAppState, + state, + useNewFieldsApi, + }), + [capabilities, indexPattern, indexPatterns, setAppState, state, useNewFieldsApi] + ); + + const onOpenInspector = useCallback(() => { + // prevent overlapping + setExpandedDoc(undefined); + }, [setExpandedDoc]); + + const onSort = useCallback( + (sort: string[][]) => { + setAppState({ sort }); + }, + [setAppState] + ); + + const onAddFilter = useCallback( + (field: IndexPatternField | string, values: string, operation: '+' | '-') => { + const fieldName = typeof field === 'string' ? field : field.name; + popularizeField(indexPattern, fieldName, indexPatterns); + const newFilters = esFilters.generateFilters( + opts.filterManager, + field, + values, + operation, + String(indexPattern.id) + ); + if (trackUiMetric) { + trackUiMetric(METRIC_TYPE.CLICK, 'filter_added'); + } + return opts.filterManager.addFilters(newFilters); + }, + [opts, indexPattern, indexPatterns, trackUiMetric] + ); + + const onChangeInterval = useCallback( + (interval: string) => { + if (interval) { + setAppState({ interval }); + } + }, + [setAppState] + ); + + const timefilterUpdateHandler = useCallback( + (ranges: { from: number; to: number }) => { + data.query.timefilter.timefilter.setTime({ + from: moment(ranges.from).toISOString(), + to: moment(ranges.to).toISOString(), + mode: 'absolute', + }); + }, + [data] + ); + + const onBackToTop = useCallback(() => { + if (scrollableDesktop && scrollableDesktop.current) { + scrollableDesktop.current.focus(); + } + // Only the desktop one needs to target a specific container + if (!isMobile() && scrollableDesktop.current) { + scrollableDesktop.current.scrollTo(0, 0); + } else if (window) { + window.scrollTo(0, 0); + } + }, [scrollableDesktop]); + + const onResize = useCallback( + (colSettings: { columnId: string; width: number }) => { + const grid = { ...state.grid } || {}; + const newColumns = { ...grid.columns } || {}; + newColumns[colSettings.columnId] = { + width: colSettings.width, + }; + const newGrid = { ...grid, columns: newColumns }; + opts.setAppState({ grid: newGrid }); + }, + [opts, state] + ); + const columns = useMemo(() => { if (!state.columns) { return []; @@ -132,20 +188,12 @@ export function Discover({ return ( -

@@ -154,16 +202,19 @@ export function Discover({ { - if (scrollableDesktop && scrollableDesktop.current) { - scrollableDesktop.current.focus(); - } - // Only the desktop one needs to target a specific container - if (!isMobile() && scrollableDesktop.current) { - scrollableDesktop.current.scrollTo(0, 0); - } else if (window) { - window.scrollTo(0, 0); - } - }} + onBackToTop={onBackToTop} onFilter={onAddFilter} onMoveColumn={onMoveColumn} onRemoveColumn={onRemoveColumn} @@ -352,19 +393,11 @@ export function Discover({ services={services} settings={state.grid} onAddColumn={onAddColumn} - onFilter={onAddFilter} + onFilter={onAddFilter as DocViewFilterFn} onRemoveColumn={onRemoveColumn} onSetColumns={onSetColumns} onSort={onSort} - onResize={(colSettings: { columnId: string; width: number }) => { - const grid = { ...state.grid } || {}; - const newColumns = { ...grid.columns } || {}; - newColumns[colSettings.columnId] = { - width: colSettings.width, - }; - const newGrid = { ...grid, columns: newColumns }; - opts.setAppState({ grid: newGrid }); - }} + onResize={onResize} />

)} diff --git a/src/plugins/discover/public/application/components/discover_topnav.test.tsx b/src/plugins/discover/public/application/components/discover_topnav.test.tsx new file mode 100644 index 00000000000000..3f12386281059d --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_topnav.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { DiscoverServices } from '../../build_services'; +import { AppState, GetStateReturn } from '../angular/discover_state'; +import { savedSearchMock } from '../../__mocks__/saved_search'; +import { dataPluginMock } from '../../../../data/public/mocks'; +import { createFilterManagerMock } from '../../../../data/public/query/filter_manager/filter_manager.mock'; +import { uiSettingsMock as mockUiSettings } from '../../__mocks__/ui_settings'; +import { IndexPatternAttributes } from '../../../../data/common/index_patterns'; +import { SavedObject } from '../../../../../core/types'; +import { DiscoverTopNav, DiscoverTopNavProps } from './discover_topnav'; +import { RequestAdapter } from '../../../../inspector/common/adapters/request'; +import { TopNavMenu } from '../../../../navigation/public'; + +function getProps(): DiscoverTopNavProps { + const state = ({} as unknown) as AppState; + const services = ({ + navigation: { + ui: { TopNavMenu }, + }, + capabilities: { + discover: { + save: true, + }, + }, + uiSettings: mockUiSettings, + } as unknown) as DiscoverServices; + const indexPattern = indexPatternMock; + return { + indexPattern: indexPatternMock, + opts: { + config: mockUiSettings, + data: dataPluginMock.createStartContract(), + filterManager: createFilterManagerMock(), + getFieldCounts: jest.fn(), + indexPatternList: (indexPattern as unknown) as Array>, + inspectorAdapters: { requests: {} as RequestAdapter }, + navigateTo: jest.fn(), + sampleSize: 10, + savedSearch: savedSearchMock, + services, + setAppState: jest.fn(), + setHeaderActionMenu: jest.fn(), + stateContainer: {} as GetStateReturn, + timefield: indexPattern.timeFieldName || '', + }, + state, + updateQuery: jest.fn(), + onOpenInspector: jest.fn(), + }; +} + +describe('Discover topnav component', () => { + test('setHeaderActionMenu was called', () => { + const props = getProps(); + mountWithIntl(); + expect(props.opts.setHeaderActionMenu).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/discover/public/application/components/discover_topnav.tsx b/src/plugins/discover/public/application/components/discover_topnav.tsx new file mode 100644 index 00000000000000..69a1433b6505c0 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_topnav.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React, { useMemo } from 'react'; +import { DiscoverProps } from './types'; +import { getTopNavLinks } from './top_nav/get_top_nav_links'; + +export type DiscoverTopNavProps = Pick< + DiscoverProps, + 'indexPattern' | 'updateQuery' | 'state' | 'opts' +> & { onOpenInspector: () => void }; + +export const DiscoverTopNav = ({ + indexPattern, + opts, + onOpenInspector, + state, + updateQuery, +}: DiscoverTopNavProps) => { + const showDatePicker = useMemo(() => indexPattern.isTimeBased(), [indexPattern]); + const { TopNavMenu } = opts.services.navigation.ui; + const topNavMenu = useMemo( + () => + getTopNavLinks({ + getFieldCounts: opts.getFieldCounts, + indexPattern, + inspectorAdapters: opts.inspectorAdapters, + navigateTo: opts.navigateTo, + savedSearch: opts.savedSearch, + services: opts.services, + state: opts.stateContainer, + onOpenInspector, + }), + [indexPattern, opts, onOpenInspector] + ); + + const updateSavedQueryId = (newSavedQueryId: string | undefined) => { + const { appStateContainer, setAppState } = opts.stateContainer; + if (newSavedQueryId) { + setAppState({ savedQuery: newSavedQueryId }); + } else { + // remove savedQueryId from state + const newState = { + ...appStateContainer.getState(), + }; + delete newState.savedQuery; + appStateContainer.set(newState); + } + }; + return ( + + ); +}; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx index 7178eccfec4b67..73de3b14f88f6a 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx @@ -7,39 +7,44 @@ */ import React from 'react'; +import { act } from 'react-dom/test-utils'; import { shallowWithIntl as shallow } from '@kbn/test/jest'; - -// @ts-ignore import { ShallowWrapper } from 'enzyme'; import { ChangeIndexPattern } from './change_indexpattern'; import { SavedObject } from 'kibana/server'; -import { DiscoverIndexPattern } from './discover_index_pattern'; +import { DiscoverIndexPattern, DiscoverIndexPatternProps } from './discover_index_pattern'; import { EuiSelectable } from '@elastic/eui'; -import { IIndexPattern } from 'src/plugins/data/public'; +import { IndexPattern } from 'src/plugins/data/public'; +import { configMock } from '../../../__mocks__/config'; +import { indexPatternsMock } from '../../../__mocks__/index_patterns'; const indexPattern = { - id: 'test1', + id: 'the-index-pattern-id-first', title: 'test1 title', -} as IIndexPattern; +} as IndexPattern; const indexPattern1 = { - id: 'test1', + id: 'the-index-pattern-id-first', attributes: { title: 'test1 title', }, } as SavedObject; const indexPattern2 = { - id: 'test2', + id: 'the-index-pattern-id', attributes: { title: 'test2 title', }, } as SavedObject; const defaultProps = { + config: configMock, indexPatternList: [indexPattern1, indexPattern2], selectedIndexPattern: indexPattern, - setIndexPattern: jest.fn(async () => {}), + state: {}, + setAppState: jest.fn(), + useNewFieldsApi: true, + indexPatterns: indexPatternsMock, }; function getIndexPatternPickerList(instance: ShallowWrapper) { @@ -63,11 +68,11 @@ function selectIndexPatternPickerOption(instance: ShallowWrapper, selectedLabel: describe('DiscoverIndexPattern', () => { test('Invalid props dont cause an exception', () => { - const props = { + const props = ({ indexPatternList: null, selectedIndexPattern: null, setIndexPattern: jest.fn(), - } as any; + } as unknown) as DiscoverIndexPatternProps; expect(shallow()).toMatchSnapshot(`""`); }); @@ -80,10 +85,15 @@ describe('DiscoverIndexPattern', () => { ]); }); - test('should switch data panel to target index pattern', () => { + test('should switch data panel to target index pattern', async () => { const instance = shallow(); - - selectIndexPatternPickerOption(instance, 'test2 title'); - expect(defaultProps.setIndexPattern).toHaveBeenCalledWith('test2'); + await act(async () => { + selectIndexPatternPickerOption(instance, 'test2 title'); + }); + expect(defaultProps.setAppState).toHaveBeenCalledWith({ + index: 'the-index-pattern-id', + columns: [], + sort: [], + }); }); }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx index 29c62d5c60775e..ea3e35f607be41 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx @@ -6,35 +6,63 @@ * Side Public License, v 1. */ -import React, { useState, useEffect } from 'react'; -import { SavedObject } from 'kibana/public'; -import { IIndexPattern, IndexPatternAttributes } from 'src/plugins/data/public'; +import React, { useState, useEffect, useCallback } from 'react'; +import { IUiSettingsClient, SavedObject } from 'kibana/public'; +import { + IndexPattern, + IndexPatternAttributes, + IndexPatternsContract, +} from 'src/plugins/data/public'; import { I18nProvider } from '@kbn/i18n/react'; import { IndexPatternRef } from './types'; import { ChangeIndexPattern } from './change_indexpattern'; +import { getSwitchIndexPatternAppState } from '../../helpers/get_switch_index_pattern_app_state'; +import { SortPairArr } from '../../angular/doc_table/lib/get_sort'; +import { MODIFY_COLUMNS_ON_SWITCH } from '../../../../common'; +import { AppState } from '../../angular/discover_state'; export interface DiscoverIndexPatternProps { + /** + * Client of uiSettings + */ + config: IUiSettingsClient; /** * list of available index patterns, if length > 1, component offers a "change" link */ indexPatternList: Array>; + /** + * Index patterns service + */ + indexPatterns: IndexPatternsContract; /** * currently selected index pattern, due to angular issues it's undefined at first rendering */ - selectedIndexPattern: IIndexPattern; + selectedIndexPattern: IndexPattern; + /** + * Function to set the current state + */ + setAppState: (state: Partial) => void; + /** + * Discover App state + */ + state: AppState; /** - * triggered when user selects a new index pattern + * Read from the Fields API */ - setIndexPattern: (id: string) => void; + useNewFieldsApi?: boolean; } /** * Component allows you to select an index pattern in discovers side bar */ export function DiscoverIndexPattern({ + config, indexPatternList, selectedIndexPattern, - setIndexPattern, + indexPatterns, + state, + setAppState, + useNewFieldsApi, }: DiscoverIndexPatternProps) { const options: IndexPatternRef[] = (indexPatternList || []).map((entity) => ({ id: entity.id, @@ -42,6 +70,24 @@ export function DiscoverIndexPattern({ })); const { id: selectedId, title: selectedTitle } = selectedIndexPattern || {}; + const setIndexPattern = useCallback( + async (id: string) => { + const nextIndexPattern = await indexPatterns.get(id); + if (nextIndexPattern && selectedIndexPattern) { + const nextAppState = getSwitchIndexPatternAppState( + selectedIndexPattern, + nextIndexPattern, + state.columns || [], + (state.sort || []) as SortPairArr[], + config.get(MODIFY_COLUMNS_ON_SWITCH), + useNewFieldsApi + ); + setAppState(nextAppState); + } + }, + [selectedIndexPattern, state, config, indexPatterns, setAppState, useNewFieldsApi] + ); + const [selected, setSelected] = useState({ id: selectedId, title: selectedTitle || '', diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx index 9c33bbcbc200a6..0ff70585af1448 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx @@ -24,6 +24,8 @@ import { getDefaultFieldFilter } from './lib/field_filter'; import { DiscoverSidebar } from './discover_sidebar'; import { DiscoverServices } from '../../../build_services'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { configMock } from '../../../__mocks__/config'; +import { indexPatternsMock } from '../../../__mocks__/index_patterns'; const mockServices = ({ history: () => ({ @@ -56,7 +58,7 @@ jest.mock('./lib/get_index_pattern_field_list', () => ({ getIndexPatternFieldList: jest.fn((indexPattern) => indexPattern.fields), })); -function getCompProps() { +function getCompProps(): DiscoverSidebarProps { const indexPattern = getStubIndexPattern( 'logstash-*', (cfg: any) => cfg, @@ -84,20 +86,22 @@ function getCompProps() { } } return { + config: configMock, columns: ['extension'], fieldCounts, hits, indexPatternList, + indexPatterns: indexPatternsMock, onAddFilter: jest.fn(), onAddField: jest.fn(), onRemoveField: jest.fn(), selectedIndexPattern: indexPattern, services: mockServices, - setIndexPattern: jest.fn(), state: {}, trackUiMetric: jest.fn(), fieldFilter: getDefaultFieldFilter(), setFieldFilter: jest.fn(), + setAppState: jest.fn(), }; } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index db5f40d8e13cba..f0303553dfac0a 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -9,7 +9,6 @@ import './discover_sidebar.scss'; import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { UiCounterMetricType } from '@kbn/analytics'; import { EuiAccordion, EuiFlexItem, @@ -25,122 +24,42 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { DiscoverField } from './discover_field'; import { DiscoverIndexPattern } from './discover_index_pattern'; import { DiscoverFieldSearch } from './discover_field_search'; -import { IndexPatternAttributes } from '../../../../../data/common'; -import { SavedObject } from '../../../../../../core/types'; import { FIELDS_LIMIT_SETTING } from '../../../../common'; import { groupFields } from './lib/group_fields'; -import { IndexPatternField, IndexPattern } from '../../../../../data/public'; +import { IndexPatternField } from '../../../../../data/public'; import { getDetails } from './lib/get_details'; import { FieldFilterState, getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter'; import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list'; -import { DiscoverServices } from '../../../build_services'; -import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { DiscoverSidebarResponsiveProps } from './discover_sidebar_responsive'; -export interface DiscoverSidebarProps { - /** - * Determines whether add/remove buttons are displayed not only when focused - */ - alwaysShowActionButtons?: boolean; - /** - * the selected columns displayed in the doc table in discover - */ - columns: string[]; - /** - * a statistics of the distribution of fields in the given hits - */ - fieldCounts: Record; +export interface DiscoverSidebarProps extends DiscoverSidebarResponsiveProps { /** * Current state of the field filter, filtering fields by name, type, ... */ fieldFilter: FieldFilterState; - /** - * hits fetched from ES, displayed in the doc table - */ - hits: ElasticSearchHit[]; - /** - * List of available index patterns - */ - indexPatternList: Array>; - /** - * Callback function when selecting a field - */ - onAddField: (fieldName: string) => void; - /** - * Callback function when adding a filter from sidebar - */ - onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; - /** - * Callback function when removing a field - * @param fieldName - */ - onRemoveField: (fieldName: string) => void; - /** - * Currently selected index pattern - */ - selectedIndexPattern?: IndexPattern; - /** - * Discover plugin services; - */ - services: DiscoverServices; /** * Change current state of fieldFilter */ setFieldFilter: (next: FieldFilterState) => void; - /** - * Callback function to select another index pattern - */ - setIndexPattern: (id: string) => void; - /** - * If on, fields are read from the fields API, not from source - */ - useNewFieldsApi?: boolean; - /** - * Metric tracking function - * @param metricType - * @param eventName - */ - trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; - /** - * Shows index pattern and a button that displays the sidebar in a flyout - */ - useFlyout?: boolean; - - /** - * an object containing properties for proper handling of unmapped fields in the UI - */ - unmappedFieldsConfig?: { - /** - * callback function to change the value of `showUnmappedFields` flag - * @param value new value to set - */ - onChangeUnmappedFields: (value: boolean) => void; - /** - * determines whether to display unmapped fields - * configurable through the switch in the UI - */ - showUnmappedFields: boolean; - /** - * determines if we should display an option to toggle showUnmappedFields value in the first place - * this value is not configurable through the UI - */ - showUnmappedFieldsDefaultValue: boolean; - }; } export function DiscoverSidebar({ alwaysShowActionButtons = false, columns, + config, fieldCounts, fieldFilter, hits, indexPatternList, + indexPatterns, onAddField, onAddFilter, onRemoveField, selectedIndexPattern, services, + setAppState, setFieldFilter, - setIndexPattern, + state, trackUiMetric, useNewFieldsApi = false, useFlyout = false, @@ -240,9 +159,13 @@ export function DiscoverSidebar({ })} > o.attributes.title)} + indexPatterns={indexPatterns} + state={state} + setAppState={setAppState} + useNewFieldsApi={useNewFieldsApi} /> ); @@ -266,9 +189,13 @@ export function DiscoverSidebar({ > o.attributes.title)} + indexPatterns={indexPatterns} + state={state} + setAppState={setAppState} + useNewFieldsApi={useNewFieldsApi} /> diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx index 7ee6cb56d99f27..02ab5abade7fba 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx @@ -15,15 +15,19 @@ import realHits from 'fixtures/real_hits.js'; import stubbedLogstashFields from 'fixtures/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; -import { DiscoverSidebar, DiscoverSidebarProps } from './discover_sidebar'; import { coreMock } from '../../../../../../core/public/mocks'; import { IndexPatternAttributes } from '../../../../../data/common'; import { getStubIndexPattern } from '../../../../../data/public/test_utils'; import { SavedObject } from '../../../../../../core/types'; -import { FieldFilterState } from './lib/field_filter'; -import { DiscoverSidebarResponsive } from './discover_sidebar_responsive'; +import { + DiscoverSidebarResponsive, + DiscoverSidebarResponsiveProps, +} from './discover_sidebar_responsive'; import { DiscoverServices } from '../../../build_services'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { configMock } from '../../../__mocks__/config'; +import { indexPatternsMock } from '../../../__mocks__/index_patterns'; +import { DiscoverSidebar } from './discover_sidebar'; const mockServices = ({ history: () => ({ @@ -56,7 +60,7 @@ jest.mock('./lib/get_index_pattern_field_list', () => ({ getIndexPatternFieldList: jest.fn((indexPattern) => indexPattern.fields), })); -function getCompProps() { +function getCompProps(): DiscoverSidebarResponsiveProps { const indexPattern = getStubIndexPattern( 'logstash-*', (cfg: any) => cfg, @@ -85,25 +89,25 @@ function getCompProps() { } return { columns: ['extension'], + config: configMock, fieldCounts, hits, indexPatternList, + indexPatterns: indexPatternsMock, onAddFilter: jest.fn(), onAddField: jest.fn(), onRemoveField: jest.fn(), selectedIndexPattern: indexPattern, services: mockServices, - setIndexPattern: jest.fn(), + setAppState: jest.fn(), state: {}, trackUiMetric: jest.fn(), - fieldFilter: {} as FieldFilterState, - setFieldFilter: jest.fn(), }; } describe('discover responsive sidebar', function () { - let props: DiscoverSidebarProps; - let comp: ReactWrapper; + let props: DiscoverSidebarResponsiveProps; + let comp: ReactWrapper; beforeAll(() => { props = getCompProps(); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx index b8e8fd0679baa6..b689db12969222 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx @@ -11,6 +11,7 @@ import { sortBy } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { UiCounterMetricType } from '@kbn/analytics'; +import { IUiSettingsClient } from 'kibana/public'; import { EuiTitle, EuiHideFor, @@ -25,13 +26,14 @@ import { EuiPortal, } from '@elastic/eui'; import { DiscoverIndexPattern } from './discover_index_pattern'; -import { IndexPatternAttributes } from '../../../../../data/common'; +import { IndexPatternAttributes, IndexPatternsContract } from '../../../../../data/common'; import { SavedObject } from '../../../../../../core/types'; import { IndexPatternField, IndexPattern } from '../../../../../data/public'; import { getDefaultFieldFilter } from './lib/field_filter'; import { DiscoverSidebar } from './discover_sidebar'; import { DiscoverServices } from '../../../build_services'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { AppState } from '../../angular/discover_state'; export interface DiscoverSidebarResponsiveProps { /** @@ -42,6 +44,10 @@ export interface DiscoverSidebarResponsiveProps { * the selected columns displayed in the doc table in discover */ columns: string[]; + /** + * Client of uiSettings + */ + config: IUiSettingsClient; /** * a statistics of the distribution of fields in the given hits */ @@ -54,6 +60,10 @@ export interface DiscoverSidebarResponsiveProps { * List of available index patterns */ indexPatternList: Array>; + /** + * Index patterns service + */ + indexPatterns: IndexPatternsContract; /** * Has been toggled closed */ @@ -80,9 +90,13 @@ export interface DiscoverSidebarResponsiveProps { */ services: DiscoverServices; /** - * Callback function to select another index pattern + * Function to set the current state + */ + setAppState: (state: Partial) => void; + /** + * Discover App state */ - setIndexPattern: (id: string) => void; + state: AppState; /** * Metric tracking function * @param metricType @@ -151,9 +165,13 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) )} > o.attributes.title)} + indexPatterns={props.indexPatterns} + state={props.state} + setAppState={props.setAppState} + useNewFieldsApi={props.useNewFieldsApi} /> diff --git a/src/plugins/discover/public/application/components/types.ts b/src/plugins/discover/public/application/components/types.ts index b73f7391bf22a4..ee06bcab6528b2 100644 --- a/src/plugins/discover/public/application/components/types.ts +++ b/src/plugins/discover/public/application/components/types.ts @@ -9,7 +9,7 @@ import { IUiSettingsClient, MountPoint, SavedObject } from 'kibana/public'; import { Chart } from '../angular/helpers/point_series'; import { IndexPattern } from '../../../../data/common/index_patterns/index_patterns'; -import { DocViewFilterFn, ElasticSearchHit } from '../doc_views/doc_views_types'; +import { ElasticSearchHit } from '../doc_views/doc_views_types'; import { AggConfigs } from '../../../../data/common/search/aggs'; import { @@ -23,6 +23,7 @@ import { import { SavedSearch } from '../../saved_searches'; import { AppState, GetStateReturn } from '../angular/discover_state'; import { RequestAdapter } from '../../../../inspector/common'; +import { DiscoverServices } from '../../build_services'; export interface DiscoverProps { /** @@ -59,38 +60,10 @@ export interface DiscoverProps { * Increased when scrolling down */ minimumVisibleRows: number; - /** - * Function to add a column to state - */ - onAddColumn: (column: string) => void; - /** - * Function to add a filter to state - */ - onAddFilter: DocViewFilterFn; - /** - * Function to change the used time interval of the date histogram - */ - onChangeInterval: (interval: string) => void; - /** - * Function to move a given column to a given index, used in legacy table - */ - onMoveColumn: (columns: string, newIdx: number) => void; - /** - * Function to remove a given column from state - */ - onRemoveColumn: (column: string) => void; - /** - * Function to replace columns in state - */ - onSetColumns: (columns: string[]) => void; /** * Function to scroll down the legacy table to the bottom */ onSkipBottomButtonClick: () => void; - /** - * Function to change sorting of the table, triggers a fetch - */ - onSort: (sort: string[][]) => void; opts: { /** * Date histogram aggregation config @@ -108,10 +81,6 @@ export interface DiscoverProps { * Use angular router for navigation */ navigateTo: () => void; - /** - * Functions to get/mutate state - */ - stateContainer: GetStateReturn; /** * Inspect, for analyzing requests and responses */ @@ -128,6 +97,10 @@ export interface DiscoverProps { * List of available index patterns */ indexPatternList: Array>; + /** + * Kibana core services used by discover + */ + services: DiscoverServices; /** * The number of documents that can be displayed in the table/grid */ @@ -140,6 +113,10 @@ export interface DiscoverProps { * Function to set the header menu */ setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; + /** + * Functions for retrieving/mutating state + */ + stateContainer: GetStateReturn; /** * Timefield of the currently used index pattern */ @@ -165,18 +142,10 @@ export interface DiscoverProps { * Instance of SearchSource, the high level search API */ searchSource: ISearchSource; - /** - * Function to change the current index pattern - */ - setIndexPattern: (id: string) => void; /** * Current app state of URL */ state: AppState; - /** - * Function to update the time filter - */ - timefilterUpdateHandler: (ranges: { from: number; to: number }) => void; /** * Currently selected time range */ @@ -185,10 +154,6 @@ export interface DiscoverProps { * Function to update the actual query */ updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; - /** - * Function to update the actual savedQuery id - */ - updateSavedQueryId: (savedQueryId?: string) => void; /** * An object containing properties for proper handling of unmapped fields in the UI */ diff --git a/src/plugins/discover/public/application/helpers/popularize_field.ts b/src/plugins/discover/public/application/helpers/popularize_field.ts index b97b6f46600ae2..4ade7d17684198 100644 --- a/src/plugins/discover/public/application/helpers/popularize_field.ts +++ b/src/plugins/discover/public/application/helpers/popularize_field.ts @@ -6,12 +6,12 @@ * Side Public License, v 1. */ -import { IndexPattern, IndexPatternsService } from '../../../../data/public'; +import { IndexPattern, IndexPatternsContract } from '../../../../data/public'; async function popularizeField( indexPattern: IndexPattern, fieldName: string, - indexPatternsService: IndexPatternsService + indexPatternsService: IndexPatternsContract ) { if (!indexPattern.id) return; const field = indexPattern.fields.getByName(fieldName); From ea96eeccb45d4fd944a62ab8a6a09e0b9f6e5b52 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Mon, 8 Feb 2021 17:44:35 +0300 Subject: [PATCH 24/51] [VEGA] src/plugins/vis_type_vega/public/lib/vega.js should be removed (#89861) * [VEGA] src/plugins/vis_type_vega/public/lib/vega.js should be removed * remove leaflet dependency * fix CI * cleanup vega-scenagraph Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 2 +- src/plugins/maps_legacy/public/leaflet.js | 1 - .../vis_type_vega/public/data_model/types.ts | 35 +++++++------ .../public/data_model/vega_parser.test.js | 5 -- .../public/data_model/vega_parser.ts | 13 ++--- src/plugins/vis_type_vega/public/lib/vega.js | 12 ----- src/plugins/vis_type_vega/public/vega_fn.ts | 2 +- src/plugins/vis_type_vega/public/vega_type.ts | 3 +- .../public/vega_view/vega_base_view.js | 29 +++++------ .../vega_view/vega_map_view/constants.ts | 3 +- .../vega_map_view/layers/vega_layer.test.ts | 5 +- .../vega_map_view/layers/vega_layer.ts | 8 ++- .../vega_map_view/utils/vsi_helper.ts | 5 +- .../vega_view/vega_map_view/view.test.ts | 12 ++--- .../public/vega_view/vega_map_view/view.ts | 14 +++--- .../public/vega_view/vega_view.js | 4 +- .../public/vega_visualization.test.js | 5 -- src/plugins/vis_type_vega/server/types.ts | 3 +- .../register_vega_collector.test.ts | 3 +- src/plugins/vis_type_vega/tsconfig.json | 3 +- yarn.lock | 50 ++----------------- 21 files changed, 75 insertions(+), 142 deletions(-) delete mode 100644 src/plugins/vis_type_vega/public/lib/vega.js diff --git a/package.json b/package.json index a5c6fa6f7b3c2e..b224f0c1ae0d5b 100644 --- a/package.json +++ b/package.json @@ -714,7 +714,6 @@ "leaflet": "1.5.1", "leaflet-draw": "0.4.14", "leaflet-responsive-popup": "0.6.4", - "leaflet-vega": "^0.8.6", "leaflet.heat": "0.2.0", "less": "npm:@elastic/less@2.7.3-kibana", "license-checker": "^16.0.0", @@ -833,6 +832,7 @@ "val-loader": "^1.1.1", "vega": "^5.19.1", "vega-lite": "^4.17.0", + "vega-spec-injector": "^0.0.2", "vega-schema-url-parser": "^2.1.0", "vega-tooltip": "^0.25.0", "venn.js": "0.2.20", diff --git a/src/plugins/maps_legacy/public/leaflet.js b/src/plugins/maps_legacy/public/leaflet.js index 69531013abae4d..fd02f83d72823c 100644 --- a/src/plugins/maps_legacy/public/leaflet.js +++ b/src/plugins/maps_legacy/public/leaflet.js @@ -12,7 +12,6 @@ if (!window.hasOwnProperty('L')) { window.L.Browser.touch = false; window.L.Browser.pointer = false; - require('leaflet-vega'); require('leaflet.heat/dist/leaflet-heat.js'); require('leaflet-draw/dist/leaflet.draw.css'); require('leaflet-draw/dist/leaflet.draw.js'); diff --git a/src/plugins/vis_type_vega/public/data_model/types.ts b/src/plugins/vis_type_vega/public/data_model/types.ts index 8d6a8227203d26..042ffac583e987 100644 --- a/src/plugins/vis_type_vega/public/data_model/types.ts +++ b/src/plugins/vis_type_vega/public/data_model/types.ts @@ -10,6 +10,8 @@ import { SearchResponse, SearchParams } from 'elasticsearch'; import { Filter } from 'src/plugins/data/public'; import { DslQuery } from 'src/plugins/data/common'; +import { Assign } from '@kbn/utility-types'; +import { Spec } from 'vega'; import { EsQueryParser } from './es_query_parser'; import { EmsFileParser } from './ems_file_parser'; import { UrlParser } from './url_parser'; @@ -93,21 +95,24 @@ export interface KibanaConfig { renderer: Renderer; } -export interface VegaSpec { - [index: string]: any; - $schema: string; - data?: Data; - encoding?: Encoding; - mark?: string; - title?: string; - autosize?: AutoSize; - projections?: Projection[]; - width?: number | 'container'; - height?: number | 'container'; - padding?: number | Padding; - _hostConfig?: KibanaConfig; - config: VegaSpecConfig; -} +export type VegaSpec = Assign< + Spec, + { + [index: string]: any; + $schema: string; + data?: Data; + encoding?: Encoding; + mark?: string; + title?: string; + autosize?: AutoSize; + projections?: Projection[]; + width?: number | 'container'; + height?: number | 'container'; + padding?: number | Padding; + _hostConfig?: KibanaConfig; + config: VegaSpecConfig; + } +>; export enum CONSTANTS { TIMEFILTER = '%timefilter%', diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js index eeacec0834ea62..1948792d55a83e 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js @@ -13,11 +13,6 @@ import { bypassExternalUrlCheck } from '../vega_view/vega_base_view'; jest.mock('../services'); -jest.mock('../lib/vega', () => ({ - vega: jest.requireActual('vega'), - vegaLite: jest.requireActual('vega-lite'), -})); - describe(`VegaParser.parseAsync`, () => { test(`should throw an error in case of $spec is not defined`, async () => { const vp = new VegaParser('{}'); diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts index 667350b693a54c..e97418581a42f1 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts @@ -13,8 +13,9 @@ import hjson from 'hjson'; import { euiPaletteColorBlind } from '@elastic/eui'; import { euiThemeVars } from '@kbn/ui-shared-deps/theme'; import { i18n } from '@kbn/i18n'; -// @ts-ignore -import { vega, vegaLite } from '../lib/vega'; + +import { logger, Warn, version as vegaVersion } from 'vega'; +import { compile, TopLevelSpec, version as vegaLiteVersion } from 'vega-lite'; import { EsQueryParser } from './es_query_parser'; import { Utils } from './utils'; import { EmsFileParser } from './ems_file_parser'; @@ -235,9 +236,9 @@ The URL is an identifier only. Kibana and your browser will never access this UR */ private _compileVegaLite() { this.vlspec = this.spec; - const logger = vega.logger(vega.Warn); // note: eslint has a false positive here - logger.warn = this._onWarning.bind(this); - this.spec = vegaLite.compile(this.vlspec, logger).spec; + const vegaLogger = logger(Warn); // note: eslint has a false positive here + vegaLogger.warn = this._onWarning.bind(this); + this.spec = compile(this.vlspec as TopLevelSpec, { logger: vegaLogger }).spec; // When using VL with the type=map and user did not provid their own projection settings, // remove the default projection that was generated by VegaLite compiler. @@ -534,7 +535,7 @@ The URL is an identifier only. Kibana and your browser will never access this UR private parseSchema(spec: VegaSpec) { const schema = schemaParser(spec.$schema); const isVegaLite = schema.library === 'vega-lite'; - const libVersion = isVegaLite ? vegaLite.version : vega.version; + const libVersion = isVegaLite ? vegaLiteVersion : vegaVersion; if (versionCompare(schema.version, libVersion) > 0) { this._onWarning( diff --git a/src/plugins/vis_type_vega/public/lib/vega.js b/src/plugins/vis_type_vega/public/lib/vega.js deleted file mode 100644 index b7c59fce6dec21..00000000000000 --- a/src/plugins/vis_type_vega/public/lib/vega.js +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as vegaLite from 'vega-lite/build-es5/vega-lite'; -import * as vega from 'vega/build-es5/vega'; - -export { vega, vegaLite }; diff --git a/src/plugins/vis_type_vega/public/vega_fn.ts b/src/plugins/vis_type_vega/public/vega_fn.ts index fb36a0097c9700..76479cbcdf1ec9 100644 --- a/src/plugins/vis_type_vega/public/vega_fn.ts +++ b/src/plugins/vis_type_vega/public/vega_fn.ts @@ -16,7 +16,7 @@ import { VegaInspectorAdapters } from './vega_inspector/index'; import { KibanaContext, TimeRange, Query } from '../../data/public'; import { VegaParser } from './data_model/vega_parser'; -type Input = KibanaContext | null; +type Input = KibanaContext | { type: 'null' }; type Output = Promise>; interface Arguments { diff --git a/src/plugins/vis_type_vega/public/vega_type.ts b/src/plugins/vis_type_vega/public/vega_type.ts index 54d4cf16f0cde0..902f79d03e680e 100644 --- a/src/plugins/vis_type_vega/public/vega_type.ts +++ b/src/plugins/vis_type_vega/public/vega_type.ts @@ -19,7 +19,6 @@ import { toExpressionAst } from './to_ast'; import { getInfoMessage } from './components/experimental_map_vis_info'; import { VegaVisEditorComponent } from './components/vega_vis_editor_lazy'; -import type { VegaSpec } from './data_model/types'; import type { VisParams } from './vega_fn'; export const createVegaTypeDefinition = (): VisTypeDefinition => { @@ -58,7 +57,7 @@ export const createVegaTypeDefinition = (): VisTypeDefinition => { try { const spec = parse(visParams.spec, { legacyRoot: false, keepWsc: true }); - return extractIndexPatternsFromSpec(spec as VegaSpec); + return extractIndexPatternsFromSpec(spec); } catch (e) { // spec is invalid } diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index 2ef687594ce065..d9b1b536a6d171 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -9,7 +9,8 @@ import $ from 'jquery'; import moment from 'moment'; import dateMath from '@elastic/datemath'; -import { vega, vegaLite } from '../lib/vega'; +import { scheme, loader, logger, Warn, version as vegaVersion, expressionFunction } from 'vega'; +import { version as vegaLiteVersion } from 'vega-lite'; import { Utils } from '../data_model/utils'; import { euiPaletteColorBlind } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -19,7 +20,7 @@ import { esFilters } from '../../../data/public'; import { getEnableExternalUrls, getData } from '../services'; import { extractIndexPatternsFromSpec } from '../lib/extract_index_pattern'; -vega.scheme('elastic', euiPaletteColorBlind()); +scheme('elastic', euiPaletteColorBlind()); // Vega's extension functions are global. When called, // we forward execution to the instance-specific handler @@ -32,8 +33,8 @@ const vegaFunctions = { }; for (const funcName of Object.keys(vegaFunctions)) { - if (!vega.expressionFunction(funcName)) { - vega.expressionFunction(funcName, function handlerFwd(...args) { + if (!expressionFunction(funcName)) { + expressionFunction(funcName, function handlerFwd(...args) { const view = this.context.dataflow; view.runAfter(() => view._kibanaView.vegaFunctionsHandler(funcName, ...args)); }); @@ -164,9 +165,9 @@ export class VegaBaseView { }; // Override URL sanitizer to prevent external data loading (if disabled) - const loader = vega.loader(); - const originalSanitize = loader.sanitize.bind(loader); - loader.sanitize = (uri, options) => { + const vegaLoader = loader(); + const originalSanitize = vegaLoader.sanitize.bind(vegaLoader); + vegaLoader.sanitize = (uri, options) => { if (uri.bypassToken === bypassToken) { // If uri has a bypass token, the uri was encoded by bypassExternalUrlCheck() above. // because user can only supply pure JSON data structure. @@ -185,14 +186,14 @@ export class VegaBaseView { } return originalSanitize(uri, options); }; - config.loader = loader; + config.loader = vegaLoader; - const logger = vega.logger(vega.Warn); + const vegaLogger = logger(Warn); - logger.warn = this.onWarn.bind(this); - logger.error = this.onError.bind(this); + vegaLogger.warn = this.onWarn.bind(this); + vegaLogger.error = this.onError.bind(this); - config.logger = logger; + config.logger = vegaLogger; return config; } @@ -430,8 +431,8 @@ export class VegaBaseView { } const debugObj = {}; window.VEGA_DEBUG = debugObj; - window.VEGA_DEBUG.VEGA_VERSION = vega.version; - window.VEGA_DEBUG.VEGA_LITE_VERSION = vegaLite.version; + window.VEGA_DEBUG.VEGA_VERSION = vegaVersion; + window.VEGA_DEBUG.VEGA_LITE_VERSION = vegaLiteVersion; window.VEGA_DEBUG.view = view; window.VEGA_DEBUG.vega_spec = spec; window.VEGA_DEBUG.vegalite_spec = vlspec; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts index f200d27e1b9674..3dc245f196774b 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { Style } from 'mapbox-gl'; import { TMS_IN_YML_ID } from '../../../../maps_legacy/public'; export const vegaLayerId = 'vega'; @@ -16,7 +17,7 @@ export const defaultMapConfig = { tileSize: 256, }; -export const defaultMabBoxStyle = { +export const defaultMabBoxStyle: Style = { /** * according to the MapBox documentation that value should be '8' * @see (https://docs.mapbox.com/mapbox-gl-js/style-spec/root/#version) diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts index 963c2bd03f415c..da4c14c77bc98a 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.test.ts @@ -7,6 +7,7 @@ */ import { initVegaLayer } from './vega_layer'; +import type { View } from 'vega'; type InitVegaLayerParams = Parameters[0]; @@ -32,9 +33,9 @@ describe('vega_map_view/tms_raster_layer', () => { addLayer: jest.fn(), } as unknown) as MapType; context = { - vegaView: { + vegaView: ({ initialize: jest.fn(), - }, + } as unknown) as View, updateVegaView: jest.fn(), }; }); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts index 884e948e2aea33..a3efba804b4548 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts @@ -7,14 +7,12 @@ */ import type { Map, CustomLayerInterface } from 'mapbox-gl'; +import type { View } from 'vega'; import type { LayerParameters } from './types'; -// @ts-ignore -import { vega } from '../../lib/vega'; - export interface VegaLayerContext { - vegaView: vega.View; - updateVegaView: (map: Map, view: vega.View) => void; + vegaView: View; + updateVegaView: (map: Map, view: View) => void; } export function initVegaLayer({ diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.ts index 29c8d33cf39673..2085e250045f62 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/utils/vsi_helper.ts @@ -7,13 +7,12 @@ */ // @ts-expect-error -// eslint-disable-next-line import/no-extraneous-dependencies import Vsi from 'vega-spec-injector'; -import { VegaSpec } from '../../../data_model/types'; +import { Spec } from 'vega'; import { defaultProjection } from '../constants'; -export const injectMapPropsIntoSpec = (spec: VegaSpec) => { +export const injectMapPropsIntoSpec = (spec: Spec) => { const vsi = new Vsi(); vsi.overrideField(spec, 'autosize', 'none'); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts index b59e1c65ab3f80..21c18e15c242ca 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts @@ -28,11 +28,8 @@ import { setMapServiceSettings, setUISettings, } from '../../services'; - -jest.mock('../../lib/vega', () => ({ - vega: jest.requireActual('vega'), - vegaLite: jest.requireActual('vega-lite'), -})); +import { initVegaLayer, initTmsRasterLayer } from './layers'; +import { Map, NavigationControl, Style } from 'mapbox-gl'; jest.mock('mapbox-gl', () => ({ Map: jest.fn().mockImplementation(() => ({ @@ -55,9 +52,6 @@ jest.mock('./layers', () => ({ initTmsRasterLayer: jest.fn(), })); -import { initVegaLayer, initTmsRasterLayer } from './layers'; -import { Map, NavigationControl } from 'mapbox-gl'; - describe('vega_map_view/view', () => { describe('VegaMapView', () => { const coreStart = coreMock.createStart(); @@ -76,7 +70,7 @@ describe('vega_map_view/view', () => { setUISettings(coreStart.uiSettings); const getTmsService = jest.fn().mockReturnValue(({ - getVectorStyleSheet: () => ({ + getVectorStyleSheet: (): Style => ({ version: 8, sources: {}, layers: [], diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts index 1cdc3af7335895..4c155d6b5ea884 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import { Map, Style, NavigationControl, MapboxOptions } from 'mapbox-gl'; +import { View, parse } from 'vega'; import { initTmsRasterLayer, initVegaLayer } from './layers'; import { VegaBaseView } from '../vega_base_view'; import { getMapServiceSettings } from '../../services'; @@ -24,12 +25,9 @@ import { import { validateZoomSettings, injectMapPropsIntoSpec } from './utils'; -// @ts-expect-error -import { vega } from '../../lib/vega'; - import './vega_map_view.scss'; -async function updateVegaView(mapBoxInstance: Map, vegaView: vega.View) { +async function updateVegaView(mapBoxInstance: Map, vegaView: View) { const mapCanvas = mapBoxInstance.getCanvas(); const { lat, lng } = mapBoxInstance.getCenter(); let shouldRender = false; @@ -77,7 +75,7 @@ export class VegaMapView extends VegaBaseView { }; } - private async initMapContainer(vegaView: vega.View) { + private async initMapContainer(vegaView: View) { let style: Style = defaultMabBoxStyle; let customAttribution: MapboxOptions['customAttribution'] = []; const zoomSettings = { @@ -139,7 +137,7 @@ export class VegaMapView extends VegaBaseView { } } - private initLayers(mapBoxInstance: Map, vegaView: vega.View) { + private initLayers(mapBoxInstance: Map, vegaView: View) { const shouldShowUserConfiguredLayer = this.mapStyle === userConfiguredLayerId; if (shouldShowUserConfiguredLayer) { @@ -168,8 +166,8 @@ export class VegaMapView extends VegaBaseView { } protected async _initViewCustomizations() { - const vegaView = new vega.View( - vega.parse(injectMapPropsIntoSpec(this._parser.spec)), + const vegaView = new View( + parse(injectMapPropsIntoSpec(this._parser.spec)), this._vegaViewConfig ); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_view.js index 5d5f3ed3d37337..5b1e49a73343bf 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_view.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { vega } from '../lib/vega'; +import { View, parse } from 'vega'; import { VegaBaseView } from './vega_base_view'; export class VegaView extends VegaBaseView { @@ -14,7 +14,7 @@ export class VegaView extends VegaBaseView { // In some cases, Vega may be initialized twice... TBD if (!this._$container) return; - const view = new vega.View(vega.parse(this._parser.spec), this._vegaViewConfig); + const view = new View(parse(this._parser.spec), this._vegaViewConfig); if (this._parser.useResize) this.updateVegaSize(view); view.initialize(this._$container.get(0), this._$controls.get(0)); diff --git a/src/plugins/vis_type_vega/public/vega_visualization.test.js b/src/plugins/vis_type_vega/public/vega_visualization.test.js index a55d5c4423f0e0..776f8898b3e3a3 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.test.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.test.js @@ -26,11 +26,6 @@ jest.mock('./default_spec', () => ({ getDefaultSpec: () => jest.requireActual('./test_utils/default.spec.json'), })); -jest.mock('./lib/vega', () => ({ - vega: jest.requireActual('vega'), - vegaLite: jest.requireActual('vega-lite'), -})); - // FLAKY: https://github.com/elastic/kibana/issues/71713 describe('VegaVisualizations', () => { let domNode; diff --git a/src/plugins/vis_type_vega/server/types.ts b/src/plugins/vis_type_vega/server/types.ts index f1e97416d76657..affd93dedb8ca9 100644 --- a/src/plugins/vis_type_vega/server/types.ts +++ b/src/plugins/vis_type_vega/server/types.ts @@ -7,10 +7,11 @@ */ import { Observable } from 'rxjs'; +import { SharedGlobalConfig } from 'kibana/server'; import { HomeServerPluginSetup } from '../../home/server'; import { UsageCollectionSetup } from '../../usage_collection/server'; -export type ConfigObservable = Observable<{ kibana: { index: string } }>; +export type ConfigObservable = Observable; export interface VegaSavedObjectAttributes { title: string; diff --git a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts index b3abc460701594..9db1b7657f4447 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts @@ -12,11 +12,12 @@ import { createUsageCollectionSetupMock } from 'src/plugins/usage_collection/ser import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; import { HomeServerPluginSetup } from '../../../home/server'; import { registerVegaUsageCollector } from './register_vega_collector'; +import { ConfigObservable } from '../types'; describe('registerVegaUsageCollector', () => { const mockIndex = 'mock_index'; const mockDeps = { home: ({} as unknown) as HomeServerPluginSetup }; - const mockConfig = of({ kibana: { index: mockIndex } }); + const mockConfig = of({ kibana: { index: mockIndex } }) as ConfigObservable; it('makes a usage collector and registers it`', () => { const mockCollectorSet = createUsageCollectionSetupMock(); diff --git a/src/plugins/vis_type_vega/tsconfig.json b/src/plugins/vis_type_vega/tsconfig.json index c013056ba4566f..d03ee6eae790e8 100644 --- a/src/plugins/vis_type_vega/tsconfig.json +++ b/src/plugins/vis_type_vega/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "strictNullChecks": false }, "include": [ "server/**/*", diff --git a/yarn.lock b/yarn.lock index fa7ebacb1cd708..6df258e9715b78 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19427,13 +19427,6 @@ leaflet-responsive-popup@0.6.4: resolved "https://registry.yarnpkg.com/leaflet-responsive-popup/-/leaflet-responsive-popup-0.6.4.tgz#b93d9368ef9f96d6dc911cf5b96d90e08601c6b3" integrity sha512-2D8G9aQA6NHkulDBPN9kqbUCkCpWQQ6dF0xFL11AuEIWIbsL4UC/ZPP5m8GYM0dpU6YTlmyyCh1Tz+cls5Q4dg== -leaflet-vega@^0.8.6: - version "0.8.6" - resolved "https://registry.yarnpkg.com/leaflet-vega/-/leaflet-vega-0.8.6.tgz#dd4090a6123cb983c2b732d53ec9e4daa53736b2" - integrity sha1-3UCQphI8uYPCtzLVPsnk2qU3NrI= - dependencies: - vega-spec-injector "^0.0.2" - leaflet.heat@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/leaflet.heat/-/leaflet.heat-0.2.0.tgz#109d8cf586f0adee41f05aff031e27a77fecc229" @@ -29574,7 +29567,7 @@ vega-event-selector@^2.0.6, vega-event-selector@~2.0.6: resolved "https://registry.yarnpkg.com/vega-event-selector/-/vega-event-selector-2.0.6.tgz#6beb00e066b78371dde1a0f40cb5e0bbaecfd8bc" integrity sha512-UwCu50Sqd8kNZ1X/XgiAY+QAyQUmGFAwyDu7y0T5fs6/TPQnDo/Bo346NgSgINBEhEKOAMY1Nd/rPOk4UEm/ew== -vega-expression@^4.0.0, vega-expression@^4.0.1, vega-expression@~4.0.1: +vega-expression@^4.0.1, vega-expression@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/vega-expression/-/vega-expression-4.0.1.tgz#c03e4fc68a00acac49557faa4e4ed6ac8a59c5fd" integrity sha512-ZrDj0hP8NmrCpdLFf7Rd/xMUHGoSYsAOTaYp7uXZ2dkEH5x0uPy5laECMc8TiQvL8W+8IrN2HAWCMRthTSRe2Q== @@ -29608,24 +29601,7 @@ vega-format@^1.0.4, vega-format@~1.0.4: vega-time "^2.0.3" vega-util "^1.15.2" -vega-functions@^5.10.0: - version "5.10.0" - resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.10.0.tgz#3d384111f13b3b0dd38a4fca656c5ae54b66e158" - integrity sha512-1l28OxUwOj8FEvRU62Oz2hiTuDECrvx1DPU1qLebBKhlgaKbcCk3XyHrn1kUzhMKpXq+SFv5VPxchZP47ASSvQ== - dependencies: - d3-array "^2.7.1" - d3-color "^2.0.0" - d3-geo "^2.0.1" - vega-dataflow "^5.7.3" - vega-expression "^4.0.1" - vega-scale "^7.1.1" - vega-scenegraph "^4.9.2" - vega-selections "^5.1.5" - vega-statistics "^1.7.9" - vega-time "^2.0.4" - vega-util "^1.16.0" - -vega-functions@^5.12.0, vega-functions@~5.12.0: +vega-functions@^5.10.0, vega-functions@^5.12.0, vega-functions@~5.12.0: version "5.12.0" resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.12.0.tgz#44bf08a7b20673dc8cf51d6781c8ea1399501668" integrity sha512-3hljmGs+gR7TbO/yYuvAP9P5laKISf1GKk4yRHLNdM61fWgKm8pI3f6LY2Hvq9cHQFTiJ3/5/Bx2p1SX5R4quQ== @@ -29752,19 +29728,7 @@ vega-scale@^7.0.3, vega-scale@^7.1.1, vega-scale@~7.1.1: vega-time "^2.0.4" vega-util "^1.15.2" -vega-scenegraph@^4.9.2: - version "4.9.2" - resolved "https://registry.yarnpkg.com/vega-scenegraph/-/vega-scenegraph-4.9.2.tgz#83b1dbc34a9ab5595c74d547d6d95849d74451ed" - integrity sha512-epm1CxcB8AucXQlSDeFnmzy0FCj+HV2k9R6ch2lfLRln5lPLEfgJWgFcFhVf5jyheY0FSeHH52Q5zQn1vYI1Ow== - dependencies: - d3-path "^2.0.0" - d3-shape "^2.0.0" - vega-canvas "^1.2.5" - vega-loader "^4.3.3" - vega-scale "^7.1.1" - vega-util "^1.15.2" - -vega-scenegraph@^4.9.3, vega-scenegraph@~4.9.3: +vega-scenegraph@^4.9.2, vega-scenegraph@^4.9.3, vega-scenegraph@~4.9.3: version "4.9.3" resolved "https://registry.yarnpkg.com/vega-scenegraph/-/vega-scenegraph-4.9.3.tgz#c4720550ea7ff5c8d9d0690f47fe2640547cfc6b" integrity sha512-lBvqLbXqrqRCTGJmSgzZC/tLR/o+TXfakbdhDzNdpgTavTaQ65S/67Gpj5hPpi77DvsfZUIY9lCEeO37aJhy0Q== @@ -29781,14 +29745,6 @@ vega-schema-url-parser@^2.1.0: resolved "https://registry.yarnpkg.com/vega-schema-url-parser/-/vega-schema-url-parser-2.1.0.tgz#847f9cf9f1624f36f8a51abc1adb41ebc6673cb4" integrity sha512-JHT1PfOyVzOohj89uNunLPirs05Nf59isPT5gnwIkJph96rRgTIBJE7l7yLqndd7fLjr3P8JXHGAryRp74sCaQ== -vega-selections@^5.1.5: - version "5.1.5" - resolved "https://registry.yarnpkg.com/vega-selections/-/vega-selections-5.1.5.tgz#c7662edf26c1cfb18623573b30590c9774348d1c" - integrity sha512-oRSsfkqYqA5xfEJqDpgnSDd+w0k6p6SGYisMD6rGXMxuPl0x0Uy6RvDr4nbEtB+dpWdoWEvgrsZVS6axyDNWvQ== - dependencies: - vega-expression "^4.0.0" - vega-util "^1.15.2" - vega-selections@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/vega-selections/-/vega-selections-5.3.0.tgz#810f2e7b7642fa836cf98b2e5dcc151093b1f6a7" From 5176aa6bc7b6b74dfc9fbe6b84d212e044fd43cd Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Mon, 8 Feb 2021 07:51:45 -0700 Subject: [PATCH 25/51] Update geo alerting docs to just cover geo containment (#90480) --- docs/user/alerting/geo-alert-types.asciidoc | 67 ++---------------- .../images/alert-types-tracking-select.png | Bin 37690 -> 30066 bytes 2 files changed, 7 insertions(+), 60 deletions(-) diff --git a/docs/user/alerting/geo-alert-types.asciidoc b/docs/user/alerting/geo-alert-types.asciidoc index f79885e3bc7163..d9073ecca1145f 100644 --- a/docs/user/alerting/geo-alert-types.asciidoc +++ b/docs/user/alerting/geo-alert-types.asciidoc @@ -1,19 +1,16 @@ [role="xpack"] -[[geo-alert-types]] -== Geo alert types +[[geo-alerting]] +== Geo alerting -Two additional stack alerts are available: -<> and <>. +Alerting now includes one additional stack alert: <>. As with other stack alerts, you need `all` access to the *Stack Alerts* feature -to be able to create and edit either of the geo alerts. +to be able to create and edit a geo alert. See <> for more information on configuring roles that provide access to this feature. [float] -=== Geo alert requirements - -To create either a *Tracking threshold* or a *Tracking containment* alert, the -following requirements must be present: +=== Geo alerting requirements +To create a *Tracking containment* alert, the following requirements must be present: - *Tracks index or index pattern*: An index containing a `geo_point` field, `date` field, and some form of entity identifier. An entity identifier is a `keyword` or `number` @@ -33,62 +30,12 @@ than the current time minus the amount of the interval. If data older than [float] === Creating a geo alert -Both *threshold* and *containment* alerts can be created by clicking the *Create* -button in the <>. +Click the *Create* button in the <>. Complete the <>. -Select <> to generate an alert when an entity crosses a boundary, and you desire the -ability to highlight lines of crossing on a custom map. -Select -<> if an entity should send out constant alerts -while contained within a boundary (this feature is optional) or if the alert is generally -just more focused around activity when an entity exists within a shape. [role="screenshot"] image::images/alert-types-tracking-select.png[Choosing a tracking alert type] -[NOTE] -================================================== -With recent advances in the alerting framework, most of the features -available in Tracking threshold alerts can be replicated with just -a little more work in Tracking containment alerts. The capabilities of Tracking -threshold alerts may be deprecated or folded into Tracking containment alerts -in the future. -================================================== - -[float] -[[alert-type-tracking-threshold]] -=== Tracking threshold -The Tracking threshold alert type runs an {es} query over indices, comparing the latest -entity locations with their previous locations. In the event that an entity has crossed a -boundary from the selected boundary index, an alert may be generated. - -[float] -==== Defining the conditions -Tracking threshold has a *Delayed evaluation offset* and 4 clauses that define the -condition to detect, as well as 2 Kuery bars used to provide additional filtering -context for each of the indices. - -[role="screenshot"] -image::images/alert-types-tracking-threshold-conditions.png[Five clauses define the condition to detect] - - -Delayed evaluation offset:: If a data source lags or is intermittent, you may supply -an optional value to evaluate alert conditions following a fixed delay. For instance, if data -is consistently indexed 5-10 minutes following its original timestamp, a *Delayed evaluation -offset* of `10 minutes` would ensure that alertable instances are still captured. -Index (entity):: This clause requires an *index or index pattern*, a *time field* that will be used for the *time window*, and a *`geo_point` field* for tracking. -By:: This clause specifies the field to use in the previously provided -*index or index pattern* for tracking Entities. An entity is a `keyword` -or `number` field that consistently identifies the entity to be tracked. -When entity:: This clause specifies which crossing option to track. The values -*Entered*, *Exited*, and *Crossed* can be selected to indicate which crossing conditions -should trigger an alert. *Entered* alerts on entry into a boundary, *Exited* alerts on exit -from a boundary, and *Crossed* alerts on all boundary crossings whether they be entrances -or exits. -Index (Boundary):: This clause requires an *index or index pattern*, a *`geo_shape` field* -identifying boundaries, and an optional *Human-readable boundary name* for better alerting -messages. - [float] [[alert-type-tracking-containment]] === Tracking containment diff --git a/docs/user/alerting/images/alert-types-tracking-select.png b/docs/user/alerting/images/alert-types-tracking-select.png index 445a5202ffd0c2c9b809d95e7d9c4d6d8a8723ca..44fcf1a2600b8e287594ef13b0bffd274994b88c 100644 GIT binary patch literal 30066 zcmdRW1yq#ryXIJcBCT{NAV@a|Lx>EZq=105NO$KDD$*t0Al=;!(jXw+3|#|5ckR#r z|L*SDy}S3^yLZo?vmQJ!@O|I>`hDK#dEW7VFDHrhi1ZNz0>OIs7N!V+-2DiF+&RFw z3w~2$MxPA6J+ywSW(R>_xBvZrCyEK13<7xuc?T2y;Hau#)-*916@&wqM^7eJmv|6^WaxwRWjzuhj(+auyb{yDhSF zpZI+!e2A(;G|m1tfA)#k2fXK*RDcHfwm@+IIRvst`VAKXamEtGfIu32p52E)w8d~i z1B;(y+<|uL5|KSvtDHtJpK?mZ9U)# zJnn0Pa)iPz7j757T`<=M!y?^L zGjy$IWIZ`hg?zF)N{KD>AQ>+@I@*;*Yp628Y@zh$$_kOVc*pL1W4+Dc_-{4TKvG?! z%b^eawK)7>WpmnL9vz!}3HvIHRlA|VG0ExUnF9*U)zv-p6N_ts2mU!=REjg7MTuZE z@?D&tThXO`y?3GYii-S}`DA{MtS^QoSG{s8J$ABHOcK1WZRi~^bjwjp7&=XEm5y_0 z)CZcctj^0`tQlrKdi02=q5=(jgUEinyJGk5Y0Ol??3p*tRv5To)C&FauIlPzDMIkIO8fK7z8D;eNhT&H zX_#=+OX|Bk zZvKLYr_f8!$U@6qr@8-eo4(OHx5d0var97TU#hv7goL3`AKi4_1re+EXQr;ul+p1q zC)8;0oIx)KOILqFbue<&Vz|OUQIS)l*74e>s>q{%x^s8lfGY*%hTb)>ozvCu0Nn0= ziSt>K#AmA9sTs6Ie9q^=1MkOeHSpg?x23+=R0d@2h6A<}<6}BYm5EB|kS`*#wXDHe znVAG($;sGG3(&}@sAy*Gm-O^qC`w`ckMG~Jn{OcqmjJl_f}l| ze4ucz8{KK!e5MR*EKk{DI0Fw)2u?Vt(xkFhLC&Ji9T*tgU@csuQW$UEQI{E2Ru=yL zrHq{1O>m*khx^*ud3nDG(Xlrs>KyW&rlvse@tvlps&>Hf(0~bnc*fXj zks3U_f7fyoYC2xP;9_v#0lSYU6cTtCTmM>n5;{|ii z)I1~3ef;q8@!Ox$(6kM z#tmy|NQ{e9%an=&=K}4_M{q*Xq|MGYXw{$Eb_vgMBat-^AYYo_)_yB7iJ((0_DK

T1+R=<4bOKr@3A)ZMu-l$DkJ-_}b6 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

Z=b$?CE%Gp~U{yd9HNKQ^@G22Il7oCaCoLa&z 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 Date: Mon, 8 Feb 2021 08:59:45 -0600 Subject: [PATCH 26/51] TypeScript project references for infra plugin (#90118) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Felix Stürmer --- .../http_api/log_alerts/chart_preview_data.ts | 16 ++++- .../infra/common/http_api/shared/errors.ts | 41 +++++++++---- .../infra/common/inventory_models/types.ts | 2 +- .../use_metric_threshold_alert_prefill.ts | 2 +- .../public/components/document_title.tsx | 8 +-- .../public/components/eui/toolbar/toolbar.tsx | 18 ++++-- .../public/components/fixed_datepicker.tsx | 14 +++-- .../infra/public/components/toolbar_panel.ts | 17 ++++-- .../components/waffle/custom_field_panel.tsx | 4 +- .../inventory_view/components/waffle/node.tsx | 4 +- .../waffle/waffle_group_by_controls.tsx | 4 +- .../infra/public/utils/use_tracked_promise.ts | 8 +-- x-pack/plugins/infra/tsconfig.json | 36 +++++++++++ x-pack/test/tsconfig.json | 60 +++++++++---------- x-pack/tsconfig.json | 2 + x-pack/tsconfig.refs.json | 1 + 16 files changed, 163 insertions(+), 74 deletions(-) create mode 100644 x-pack/plugins/infra/tsconfig.json diff --git a/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts b/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts index 76533a476561b9..e6baca305508ea 100644 --- a/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts +++ b/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts @@ -41,7 +41,21 @@ export type GetLogAlertsChartPreviewDataSuccessResponsePayload = rt.TypeOf< typeof getLogAlertsChartPreviewDataSuccessResponsePayloadRT >; -export const getLogAlertsChartPreviewDataAlertParamsSubsetRT = rt.intersection([ +// This should not have an explicit `any` return type, but it's here because its +// inferred type includes `Comparator` which is a string enum exported from +// common/alerting/logs/log_threshold/types.ts. +// +// There's a bug that's fixed in TypeScript 4.2.0 that will allow us to remove +// the `:any` from this, so remove it when that update happens. +// +// If it's removed before then you get: +// +// x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts:44:14 - error TS4023: +// Exported variable 'getLogAlertsChartPreviewDataAlertParamsSubsetRT' has or is using name 'Comparator' +// from external module "/Users/smith/Code/kibana/x-pack/plugins/infra/common/alerting/logs/log_threshold/types" +// but cannot be named. +// +export const getLogAlertsChartPreviewDataAlertParamsSubsetRT: any = rt.intersection([ rt.type({ criteria: countCriteriaRT, timeUnit: timeUnitRT, diff --git a/x-pack/plugins/infra/common/http_api/shared/errors.ts b/x-pack/plugins/infra/common/http_api/shared/errors.ts index 5e439c31bbdc9e..2b5461d71500e2 100644 --- a/x-pack/plugins/infra/common/http_api/shared/errors.ts +++ b/x-pack/plugins/infra/common/http_api/shared/errors.ts @@ -7,18 +7,35 @@ import * as rt from 'io-ts'; -const createErrorRuntimeType = ( - statusCode: number, - errorCode: string, - attributes?: Attributes -) => +export const badRequestErrorRT = rt.intersection([ rt.type({ - statusCode: rt.literal(statusCode), - error: rt.literal(errorCode), + statusCode: rt.literal(400), + error: rt.literal('Bad Request'), message: rt.string, - ...(!!attributes ? { attributes } : {}), - }); + }), + rt.partial({ + attributes: rt.unknown, + }), +]); -export const badRequestErrorRT = createErrorRuntimeType(400, 'Bad Request'); -export const forbiddenErrorRT = createErrorRuntimeType(403, 'Forbidden'); -export const conflictErrorRT = createErrorRuntimeType(409, 'Conflict'); +export const forbiddenErrorRT = rt.intersection([ + rt.type({ + statusCode: rt.literal(403), + error: rt.literal('Forbidden'), + message: rt.string, + }), + rt.partial({ + attributes: rt.unknown, + }), +]); + +export const conflictErrorRT = rt.intersection([ + rt.type({ + statusCode: rt.literal(409), + error: rt.literal('Conflict'), + message: rt.string, + }), + rt.partial({ + attributes: rt.unknown, + }), +]); diff --git a/x-pack/plugins/infra/common/inventory_models/types.ts b/x-pack/plugins/infra/common/inventory_models/types.ts index 2d3b6a7c45d075..764f41966261c6 100644 --- a/x-pack/plugins/infra/common/inventory_models/types.ts +++ b/x-pack/plugins/infra/common/inventory_models/types.ts @@ -286,7 +286,7 @@ export const ESTopHitsAggRT = rt.type({ top_hits: rt.object, }); -interface SnapshotTermsWithAggregation { +export interface SnapshotTermsWithAggregation { terms: { field: string }; aggregations: MetricsUIAggregation; } diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts index 3664be3b4903a9..068c33ea2c31f5 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts @@ -9,7 +9,7 @@ import { isEqual } from 'lodash'; import { useState } from 'react'; import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer'; -interface MetricThresholdPrefillOptions { +export interface MetricThresholdPrefillOptions { groupBy: string | string[] | undefined; filterQuery: string | undefined; metrics: MetricsExplorerMetric[]; diff --git a/x-pack/plugins/infra/public/components/document_title.tsx b/x-pack/plugins/infra/public/components/document_title.tsx index 9c3c89294f403b..20e482d9df5b5e 100644 --- a/x-pack/plugins/infra/public/components/document_title.tsx +++ b/x-pack/plugins/infra/public/components/document_title.tsx @@ -48,19 +48,19 @@ const wrapWithSharedState = () => { return null; } - private getTitle(title: TitleProp) { + public getTitle(title: TitleProp) { return typeof title === 'function' ? title(titles[this.state.index - 1]) : title; } - private pushTitle(title: string) { + public pushTitle(title: string) { titles[this.state.index] = title; } - private removeTitle() { + public removeTitle() { titles.pop(); } - private updateDocumentTitle() { + public updateDocumentTitle() { const title = (titles[titles.length - 1] || '') + TITLE_SUFFIX; if (title !== document.title) { document.title = title; diff --git a/x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx b/x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx index a2dd383695983e..f1a793d11166ca 100644 --- a/x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx +++ b/x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx @@ -6,13 +6,19 @@ */ import { EuiPanel } from '@elastic/eui'; +import { FunctionComponent } from 'react'; +import { StyledComponent } from 'styled-components'; +import { euiStyled, EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; -import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; - -export const Toolbar = euiStyled(EuiPanel).attrs(() => ({ - grow: false, - paddingSize: 'none', -}))` +// The return type of this component needs to be specified because the inferred +// return type depends on types that are not exported from EUI. You get a TS4023 +// error if the return type is not specified. +export const Toolbar: StyledComponent = euiStyled(EuiPanel).attrs( + () => ({ + grow: false, + paddingSize: 'none', + }) +)` border-top: none; border-right: none; border-left: none; diff --git a/x-pack/plugins/infra/public/components/fixed_datepicker.tsx b/x-pack/plugins/infra/public/components/fixed_datepicker.tsx index 62093dbfe53ece..dfaf0a490225a1 100644 --- a/x-pack/plugins/infra/public/components/fixed_datepicker.tsx +++ b/x-pack/plugins/infra/public/components/fixed_datepicker.tsx @@ -5,12 +5,18 @@ * 2.0. */ -import React from 'react'; - import { EuiDatePicker, EuiDatePickerProps } from '@elastic/eui'; -import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; +import React, { FunctionComponent } from 'react'; +import { StyledComponent } from 'styled-components'; +import { euiStyled, EuiTheme } from '../../../../../src/plugins/kibana_react/common'; -export const FixedDatePicker = euiStyled( +// The return type of this component needs to be specified because the inferred +// return type depends on types that are not exported from EUI. You get a TS4023 +// error if the return type is not specified. +export const FixedDatePicker: StyledComponent< + FunctionComponent, + EuiTheme +> = euiStyled( ({ className, inputClassName, diff --git a/x-pack/plugins/infra/public/components/toolbar_panel.ts b/x-pack/plugins/infra/public/components/toolbar_panel.ts index 22352b97da0ea3..d94e7faa0eabff 100644 --- a/x-pack/plugins/infra/public/components/toolbar_panel.ts +++ b/x-pack/plugins/infra/public/components/toolbar_panel.ts @@ -5,13 +5,20 @@ * 2.0. */ +import { FunctionComponent } from 'react'; import { EuiPanel } from '@elastic/eui'; -import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; +import { StyledComponent } from 'styled-components'; +import { EuiTheme, euiStyled } from '../../../../../src/plugins/kibana_react/common'; -export const ToolbarPanel = euiStyled(EuiPanel).attrs(() => ({ - grow: false, - paddingSize: 'none', -}))` +// The return type of this component needs to be specified because the inferred +// return type depends on types that are not exported from EUI. You get a TS4023 +// error if the return type is not specified. +export const ToolbarPanel: StyledComponent = euiStyled(EuiPanel).attrs( + () => ({ + grow: false, + paddingSize: 'none', + }) +)` border-top: none; border-right: none; border-left: none; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx index 8932388398b6a0..acc6ae7af2727a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx @@ -27,7 +27,7 @@ const initialState = { type State = Readonly; -export const CustomFieldPanel = class extends React.PureComponent { +export class CustomFieldPanel extends React.PureComponent { public static displayName = 'CustomFieldPanel'; public readonly state: State = initialState; public render() { @@ -86,4 +86,4 @@ export const CustomFieldPanel = class extends React.PureComponent private handleFieldSelection = (selectedOptions: SelectedOption[]) => { this.setState({ selectedOptions }); }; -}; +} diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx index c76ff798b1286c..d6934c6846b79c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx @@ -44,7 +44,7 @@ interface Props { currentTime: number; } -export const Node = class extends React.PureComponent { +export class Node extends React.PureComponent { public readonly state: State = initialState; public render() { const { nodeType, node, options, squareSize, bounds, formatter, currentTime } = this.props; @@ -164,7 +164,7 @@ export const Node = class extends React.PureComponent { this.setState({ isPopoverOpen: false }); } }; -}; +} const NodeContainer = euiStyled.div` position: relative; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx index 5c57ef11380e59..9f350610b1366d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx @@ -39,7 +39,7 @@ const initialState = { type State = Readonly; -export const WaffleGroupByControls = class extends React.PureComponent { +export class WaffleGroupByControls extends React.PureComponent { public static displayName = 'WaffleGroupByControls'; public readonly state: State = initialState; @@ -192,7 +192,7 @@ export const WaffleGroupByControls = class extends React.PureComponent( return [promiseState, execute] as [typeof promiseState, typeof execute]; }; -interface UninitializedPromiseState { +export interface UninitializedPromiseState { state: 'uninitialized'; } -interface PendingPromiseState { +export interface PendingPromiseState { state: 'pending'; promise: Promise; } -interface ResolvedPromiseState { +export interface ResolvedPromiseState { state: 'resolved'; promise: Promise; value: ResolvedValue; } -interface RejectedPromiseState { +export interface RejectedPromiseState { state: 'rejected'; promise: Promise; value: RejectedValue; diff --git a/x-pack/plugins/infra/tsconfig.json b/x-pack/plugins/infra/tsconfig.json new file mode 100644 index 00000000000000..a8a0e2c7119a91 --- /dev/null +++ b/x-pack/plugins/infra/tsconfig.json @@ -0,0 +1,36 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "../../typings/**/*", + "common/**/*", + "public/**/*", + "scripts/**/*", + "server/**/*", + "types/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../../../src/plugins/vis_type_timeseries/tsconfig.json" }, + { "path": "../data_enhanced/tsconfig.json" }, + { "path": "../alerts/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../license_management/tsconfig.json" }, + { "path": "../ml/tsconfig.json" }, + { "path": "../observability/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, + { "path": "../triggers_actions_ui/tsconfig.json" } + ] +} diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 10943b3a2929f2..0a7a30f373e07f 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -9,73 +9,73 @@ "exclude": ["../typings/jest.d.ts"], "references": [ { "path": "../../src/core/tsconfig.json" }, - { "path": "../../src/plugins/telemetry_management_section/tsconfig.json" }, - { "path": "../../src/plugins/management/tsconfig.json" }, { "path": "../../src/plugins/bfetch/tsconfig.json" }, { "path": "../../src/plugins/charts/tsconfig.json" }, { "path": "../../src/plugins/console/tsconfig.json" }, { "path": "../../src/plugins/dashboard/tsconfig.json" }, - { "path": "../../src/plugins/discover/tsconfig.json" }, { "path": "../../src/plugins/data/tsconfig.json" }, + { "path": "../../src/plugins/discover/tsconfig.json" }, { "path": "../../src/plugins/embeddable/tsconfig.json" }, { "path": "../../src/plugins/es_ui_shared/tsconfig.json" }, { "path": "../../src/plugins/expressions/tsconfig.json" }, { "path": "../../src/plugins/home/tsconfig.json" }, + { "path": "../../src/plugins/index_pattern_management/tsconfig.json" }, { "path": "../../src/plugins/kibana_overview/tsconfig.json" }, { "path": "../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../src/plugins/legacy_export/tsconfig.json" }, + { "path": "../../src/plugins/management/tsconfig.json" }, { "path": "../../src/plugins/navigation/tsconfig.json" }, { "path": "../../src/plugins/newsfeed/tsconfig.json" }, - { "path": "../../src/plugins/saved_objects/tsconfig.json" }, { "path": "../../src/plugins/saved_objects_management/tsconfig.json" }, { "path": "../../src/plugins/saved_objects_tagging_oss/tsconfig.json" }, + { "path": "../../src/plugins/saved_objects/tsconfig.json" }, { "path": "../../src/plugins/share/tsconfig.json" }, { "path": "../../src/plugins/telemetry_collection_manager/tsconfig.json" }, + { "path": "../../src/plugins/telemetry_management_section/tsconfig.json" }, { "path": "../../src/plugins/telemetry/tsconfig.json" }, - { "path": "../../src/plugins/usage_collection/tsconfig.json" }, { "path": "../../src/plugins/ui_actions/tsconfig.json" }, { "path": "../../src/plugins/url_forwarding/tsconfig.json" }, - { "path": "../../src/plugins/index_pattern_management/tsconfig.json" }, - + { "path": "../../src/plugins/usage_collection/tsconfig.json" }, { "path": "../plugins/actions/tsconfig.json" }, { "path": "../plugins/alerts/tsconfig.json" }, + { "path": "../plugins/beats_management/tsconfig.json" }, + { "path": "../plugins/cloud/tsconfig.json" }, { "path": "../plugins/code/tsconfig.json" }, { "path": "../plugins/console_extensions/tsconfig.json" }, - { "path": "../plugins/data_enhanced/tsconfig.json" }, { "path": "../plugins/dashboard_mode/tsconfig.json" }, - { "path": "../plugins/enterprise_search/tsconfig.json" }, - { "path": "../plugins/global_search/tsconfig.json" }, - { "path": "../plugins/global_search_providers/tsconfig.json" }, - { "path": "../plugins/features/tsconfig.json" }, + { "path": "../plugins/data_enhanced/tsconfig.json" }, { "path": "../plugins/embeddable_enhanced/tsconfig.json" }, + { "path": "../plugins/encrypted_saved_objects/tsconfig.json" }, + { "path": "../plugins/enterprise_search/tsconfig.json" }, { "path": "../plugins/event_log/tsconfig.json" }, - { "path": "../plugins/licensing/tsconfig.json" }, + { "path": "../plugins/features/tsconfig.json" }, + { "path": "../plugins/global_search_bar/tsconfig.json" }, + { "path": "../plugins/global_search_providers/tsconfig.json" }, + { "path": "../plugins/global_search/tsconfig.json" }, + { "path": "../plugins/grokdebugger/tsconfig.json" }, + { "path": "../plugins/index_management/tsconfig.json" }, + { "path": "../plugins/infra/tsconfig.json" }, + { "path": "../plugins/ingest_pipelines/tsconfig.json" }, { "path": "../plugins/lens/tsconfig.json" }, + { "path": "../plugins/license_management/tsconfig.json" }, + { "path": "../plugins/licensing/tsconfig.json" }, { "path": "../plugins/ml/tsconfig.json" }, + { "path": "../plugins/observability/tsconfig.json" }, + { "path": "../plugins/painless_lab/tsconfig.json" }, + { "path": "../plugins/runtime_fields/tsconfig.json" }, + { "path": "../plugins/saved_objects_tagging/tsconfig.json" }, + { "path": "../plugins/security/tsconfig.json" }, + { "path": "../plugins/snapshot_restore/tsconfig.json" }, + { "path": "../plugins/spaces/tsconfig.json" }, + { "path": "../plugins/stack_alerts/tsconfig.json" }, { "path": "../plugins/task_manager/tsconfig.json" }, { "path": "../plugins/telemetry_collection_xpack/tsconfig.json" }, { "path": "../plugins/transform/tsconfig.json" }, { "path": "../plugins/triggers_actions_ui/tsconfig.json" }, { "path": "../plugins/ui_actions_enhanced/tsconfig.json" }, - { "path": "../plugins/spaces/tsconfig.json" }, - { "path": "../plugins/security/tsconfig.json" }, - { "path": "../plugins/encrypted_saved_objects/tsconfig.json" }, - { "path": "../plugins/stack_alerts/tsconfig.json" }, - { "path": "../plugins/beats_management/tsconfig.json" }, - { "path": "../plugins/cloud/tsconfig.json" }, - { "path": "../plugins/saved_objects_tagging/tsconfig.json" }, - { "path": "../plugins/global_search_bar/tsconfig.json" }, - { "path": "../plugins/observability/tsconfig.json" }, - { "path": "../plugins/ingest_pipelines/tsconfig.json" }, - { "path": "../plugins/license_management/tsconfig.json" }, - { "path": "../plugins/snapshot_restore/tsconfig.json" }, - { "path": "../plugins/grokdebugger/tsconfig.json" }, - { "path": "../plugins/painless_lab/tsconfig.json" }, { "path": "../plugins/upgrade_assistant/tsconfig.json" }, - { "path": "../plugins/watcher/tsconfig.json" }, - { "path": "../plugins/runtime_fields/tsconfig.json" }, - { "path": "../plugins/index_management/tsconfig.json" } + { "path": "../plugins/watcher/tsconfig.json" } ] } diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 6fabd16752dfab..5d51c2923abd02 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -22,6 +22,7 @@ "plugins/embeddable_enhanced/**/*", "plugins/event_log/**/*", "plugins/enterprise_search/**/*", + "plugins/infra/**/*", "plugins/licensing/**/*", "plugins/lens/**/*", "plugins/maps/**/*", @@ -118,6 +119,7 @@ { "path": "./plugins/global_search/tsconfig.json" }, { "path": "./plugins/graph/tsconfig.json" }, { "path": "./plugins/grokdebugger/tsconfig.json" }, + { "path": "./plugins/infra/tsconfig.json" }, { "path": "./plugins/ingest_pipelines/tsconfig.json" }, { "path": "./plugins/lens/tsconfig.json" }, { "path": "./plugins/license_management/tsconfig.json" }, diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index e35cfe4e024a26..ae88ab6486e64c 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -23,6 +23,7 @@ { "path": "./plugins/global_search/tsconfig.json" }, { "path": "./plugins/graph/tsconfig.json" }, { "path": "./plugins/grokdebugger/tsconfig.json" }, + { "path": "./plugins/infra/tsconfig.json" }, { "path": "./plugins/ingest_pipelines/tsconfig.json" }, { "path": "./plugins/lens/tsconfig.json" }, { "path": "./plugins/license_management/tsconfig.json" }, From dccea865e47a8c70b8abbd7d7400172061d2c89e Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 8 Feb 2021 15:17:35 +0000 Subject: [PATCH 27/51] chore(NA): push important bazel config files under operations team code owners (#90610) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3884f975c813d5..2917cc52a6c6db 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -150,6 +150,12 @@ /src/legacy/server/warnings/ @elastic/kibana-operations /.ci/es-snapshots/ @elastic/kibana-operations /vars/ @elastic/kibana-operations +/.bazelignore @elastic/kibana-operations +/.bazeliskversion @elastic/kibana-operations +/.bazelrc @elastic/kibana-operations +/.bazelrc.common @elastic/kibana-operations +/.bazelversion @elastic/kibana-operations +/WORKSPACE.bazel @elastic/kibana-operations #CC# /packages/kbn-expect/ @elastic/kibana-operations # Quality Assurance From bbda20619ea31f430570fa2b9e1f78142d44cbc5 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 8 Feb 2021 16:20:56 +0100 Subject: [PATCH 28/51] [Search Sessions] Disable "save session" due to timeout (#90294) --- x-pack/plugins/data_enhanced/public/plugin.ts | 4 + ...onnected_search_session_indicator.test.tsx | 139 ++++++++++++++++-- .../connected_search_session_indicator.tsx | 118 ++++++++++----- .../search_session_tour.tsx | 21 +-- .../search_session_indicator.stories.tsx | 6 +- .../search_session_indicator.test.tsx | 16 +- .../search_session_indicator.tsx | 68 +++++---- .../services/search_sessions.ts | 4 +- 8 files changed, 276 insertions(+), 100 deletions(-) diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index b7d7b7c0e20d10..0a116545e6e366 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -6,6 +6,7 @@ */ import React from 'react'; +import moment from 'moment'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { BfetchPublicSetup } from '../../../../src/plugins/bfetch/public'; @@ -86,6 +87,9 @@ export class DataEnhancedPlugin application: core.application, timeFilter: plugins.data.query.timefilter.timefilter, storage: this.storage, + disableSaveAfterSessionCompletesTimeout: moment + .duration(this.config.search.sessions.notTouchedTimeout) + .asMilliseconds(), }) ) ), diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx index 79e49050941be4..3437920ed7c98a 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import { StubBrowserStorage } from '@kbn/test/jest'; import { render, waitFor, screen, act } from '@testing-library/react'; import { Storage } from '../../../../../../../src/plugins/kibana_utils/public/'; @@ -20,6 +20,8 @@ import { } from '../../../../../../../src/plugins/data/public'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { TOUR_RESTORE_STEP_KEY, TOUR_TAKING_TOO_LONG_STEP_KEY } from './search_session_tour'; +import userEvent from '@testing-library/user-event'; +import { IntlProvider } from 'react-intl'; const coreStart = coreMock.createStart(); const dataStart = dataPluginMock.createStartContract(); @@ -30,6 +32,12 @@ const timeFilter = dataStart.query.timefilter.timefilter as jest.Mocked refreshInterval$); timeFilter.getRefreshInterval.mockImplementation(() => refreshInterval$.getValue()); +const disableSaveAfterSessionCompletesTimeout = 5 * 60 * 1000; + +function Container({ children }: { children?: ReactNode }) { + return {children}; +} + beforeEach(() => { storage = new Storage(new StubBrowserStorage()); refreshInterval$.next({ value: 0, pause: true }); @@ -47,8 +55,13 @@ test("shouldn't show indicator in case no active search session", async () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - const { getByTestId, container } = render(); + const { getByTestId, container } = render( + + + + ); // make sure `searchSessionIndicator` isn't appearing after some time (lazy-loading) await expect( @@ -69,8 +82,13 @@ test("shouldn't show indicator in case app hasn't opt-in", async () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - const { getByTestId, container } = render(); + const { getByTestId, container } = render( + + + + ); sessionService.isSessionStorageReady.mockImplementation(() => false); // make sure `searchSessionIndicator` isn't appearing after some time (lazy-loading) @@ -93,8 +111,13 @@ test('should show indicator in case there is an active search session', async () application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - const { getByTestId } = render(); + const { getByTestId } = render( + + + + ); await waitFor(() => getByTestId('searchSessionIndicator')); }); @@ -118,13 +141,20 @@ test('should be disabled in case uiConfig says so ', async () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - render(); + render( + + + + ); await waitFor(() => screen.getByTestId('searchSessionIndicator')); - expect(screen.getByTestId('searchSessionIndicator').querySelector('button')).toBeDisabled(); + await userEvent.click(screen.getByLabelText('Search session loading')); + + expect(screen.getByRole('button', { name: 'Save session' })).toBeDisabled(); }); test('should be disabled during auto-refresh', async () => { @@ -135,19 +165,82 @@ test('should be disabled during auto-refresh', async () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - render(); + render( + + + + ); await waitFor(() => screen.getByTestId('searchSessionIndicator')); - expect(screen.getByTestId('searchSessionIndicator').querySelector('button')).not.toBeDisabled(); + await userEvent.click(screen.getByLabelText('Search session loading')); + + expect(screen.getByRole('button', { name: 'Save session' })).not.toBeDisabled(); act(() => { refreshInterval$.next({ value: 0, pause: false }); }); - expect(screen.getByTestId('searchSessionIndicator').querySelector('button')).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Save session' })).toBeDisabled(); +}); + +describe('Completed inactivity', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + afterEach(() => { + jest.useRealTimers(); + }); + test('save should be disabled after completed and timeout', async () => { + const state$ = new BehaviorSubject(SearchSessionState.Loading); + + const SearchSessionIndicator = createConnectedSearchSessionIndicator({ + sessionService: { ...sessionService, state$ }, + application: coreStart.application, + timeFilter, + storage, + disableSaveAfterSessionCompletesTimeout, + }); + + render( + + + + ); + + await waitFor(() => screen.getByTestId('searchSessionIndicator')); + + await userEvent.click(screen.getByLabelText('Search session loading')); + + expect(screen.getByRole('button', { name: 'Save session' })).not.toBeDisabled(); + + act(() => { + jest.advanceTimersByTime(5 * 60 * 1000); + }); + + expect(screen.getByRole('button', { name: 'Save session' })).not.toBeDisabled(); + + act(() => { + state$.next(SearchSessionState.Completed); + }); + + expect(screen.getByRole('button', { name: 'Save session' })).not.toBeDisabled(); + + act(() => { + jest.advanceTimersByTime(2.5 * 60 * 1000); + }); + + expect(screen.getByRole('button', { name: 'Save session' })).not.toBeDisabled(); + + act(() => { + jest.advanceTimersByTime(2.5 * 60 * 1000); + }); + + expect(screen.getByRole('button', { name: 'Save session' })).toBeDisabled(); + }); }); describe('tour steps', () => { @@ -167,8 +260,13 @@ describe('tour steps', () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - const rendered = render(); + const rendered = render( + + + + ); await waitFor(() => rendered.getByTestId('searchSessionIndicator')); @@ -199,8 +297,13 @@ describe('tour steps', () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - const rendered = render(); + const rendered = render( + + + + ); const searchSessionIndicator = await rendered.findByTestId('searchSessionIndicator'); expect(searchSessionIndicator).toBeTruthy(); @@ -225,8 +328,13 @@ describe('tour steps', () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - const rendered = render(); + const rendered = render( + + + + ); await waitFor(() => rendered.getByTestId('searchSessionIndicator')); expect(screen.getByTestId('searchSessionIndicatorPopoverContainer')).toBeInTheDocument(); @@ -242,8 +350,13 @@ describe('tour steps', () => { application: coreStart.application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }); - const rendered = render(); + const rendered = render( + + + + ); await waitFor(() => rendered.getByTestId('searchSessionIndicator')); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx index b572db7ebfd4ca..3935b5bb2814b7 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import React, { useRef } from 'react'; -import { debounce, distinctUntilChanged, map } from 'rxjs/operators'; -import { timer } from 'rxjs'; +import React, { useCallback, useState } from 'react'; +import { debounce, distinctUntilChanged, map, mapTo, switchMap } from 'rxjs/operators'; +import { merge, of, timer } from 'rxjs'; import useObservable from 'react-use/lib/useObservable'; import { i18n } from '@kbn/i18n'; import { SearchSessionIndicator, SearchSessionIndicatorRef } from '../search_session_indicator'; @@ -26,6 +26,11 @@ export interface SearchSessionIndicatorDeps { timeFilter: TimefilterContract; application: ApplicationStart; storage: IStorageWrapper; + /** + * Controls for how long we allow to save a session, + * after the last search in the session has completed + */ + disableSaveAfterSessionCompletesTimeout: number; } export const createConnectedSearchSessionIndicator = ({ @@ -33,6 +38,7 @@ export const createConnectedSearchSessionIndicator = ({ application, timeFilter, storage, + disableSaveAfterSessionCompletesTimeout, }: SearchSessionIndicatorDeps): React.FC => { const isAutoRefreshEnabled = () => !timeFilter.getRefreshInterval().pause; const isAutoRefreshEnabled$ = timeFilter @@ -43,60 +49,104 @@ export const createConnectedSearchSessionIndicator = ({ debounce((_state) => timer(_state === SearchSessionState.None ? 50 : 300)) // switch to None faster to quickly remove indicator when navigating away ); + const disableSaveAfterSessionCompleteTimedOut$ = sessionService.state$.pipe( + switchMap((_state) => + _state === SearchSessionState.Completed + ? merge(of(false), timer(disableSaveAfterSessionCompletesTimeout).pipe(mapTo(true))) + : of(false) + ), + distinctUntilChanged() + ); + return () => { - const ref = useRef(null); const state = useObservable(debouncedSessionServiceState$, SearchSessionState.None); const autoRefreshEnabled = useObservable(isAutoRefreshEnabled$, isAutoRefreshEnabled()); - const isDisabledByApp = sessionService.getSearchSessionIndicatorUiConfig().isDisabled(); + const isSaveDisabledByApp = sessionService.getSearchSessionIndicatorUiConfig().isDisabled(); + const disableSaveAfterSessionCompleteTimedOut = useObservable( + disableSaveAfterSessionCompleteTimedOut$, + false + ); + const [ + searchSessionIndicator, + setSearchSessionIndicator, + ] = useState(null); + const searchSessionIndicatorRef = useCallback((ref: SearchSessionIndicatorRef) => { + if (ref !== null) { + setSearchSessionIndicator(ref); + } + }, []); - let disabled = false; - let disabledReasonText: string = ''; + let saveDisabled = false; + let saveDisabledReasonText: string = ''; if (autoRefreshEnabled) { - disabled = true; - disabledReasonText = i18n.translate( + saveDisabled = true; + saveDisabledReasonText = i18n.translate( 'xpack.data.searchSessionIndicator.disabledDueToAutoRefreshMessage', { - defaultMessage: 'Search sessions are not available when auto refresh is enabled.', + defaultMessage: 'Saving search session is not available when auto refresh is enabled.', + } + ); + } + + if (disableSaveAfterSessionCompleteTimedOut) { + saveDisabled = true; + saveDisabledReasonText = i18n.translate( + 'xpack.data.searchSessionIndicator.disabledDueToTimeoutMessage', + { + defaultMessage: 'Search session results expired.', } ); } + if (isSaveDisabledByApp.disabled) { + saveDisabled = true; + saveDisabledReasonText = isSaveDisabledByApp.reasonText; + } + const { markOpenedDone, markRestoredDone } = useSearchSessionTour( storage, - ref, + searchSessionIndicator, state, - disabled + saveDisabled ); - if (isDisabledByApp.disabled) { - disabled = true; - disabledReasonText = isDisabledByApp.reasonText; - } + const onOpened = useCallback( + (openedState: SearchSessionState) => { + markOpenedDone(); + if (openedState === SearchSessionState.Restored) { + markRestoredDone(); + } + }, + [markOpenedDone, markRestoredDone] + ); + + const onContinueInBackground = useCallback(() => { + if (saveDisabled) return; + sessionService.save(); + }, [saveDisabled]); + + const onSaveResults = useCallback(() => { + if (saveDisabled) return; + sessionService.save(); + }, [saveDisabled]); + + const onCancel = useCallback(() => { + sessionService.cancel(); + }, []); if (!sessionService.isSessionStorageReady()) return null; return ( { - sessionService.save(); - }} - onSaveResults={() => { - sessionService.save(); - }} - onCancel={() => { - sessionService.cancel(); - }} - disabled={disabled} - disabledReasonText={disabledReasonText} - onOpened={(openedState) => { - markOpenedDone(); - if (openedState === SearchSessionState.Restored) { - markRestoredDone(); - } - }} + saveDisabled={saveDisabled} + saveDisabledReasonText={saveDisabledReasonText} + onContinueInBackground={onContinueInBackground} + onSaveResults={onSaveResults} + onCancel={onCancel} + onOpened={onOpened} /> ); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/search_session_tour.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/search_session_tour.tsx index 8c04410f9953bf..7987278f400ff9 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/search_session_tour.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/search_session_tour.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { MutableRefObject, useCallback, useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/public'; import { SearchSessionIndicatorRef } from '../search_session_indicator'; import { SearchSessionState } from '../../../../../../../src/plugins/data/public'; @@ -16,7 +16,7 @@ export const TOUR_RESTORE_STEP_KEY = `data.searchSession.tour.restore`; export function useSearchSessionTour( storage: IStorageWrapper, - searchSessionIndicatorRef: MutableRefObject, + searchSessionIndicatorRef: SearchSessionIndicatorRef | null, state: SearchSessionState, searchSessionsDisabled: boolean ) { @@ -30,19 +30,20 @@ export function useSearchSessionTour( useEffect(() => { if (searchSessionsDisabled) return; + if (!searchSessionIndicatorRef) return; let timeoutHandle: number; if (state === SearchSessionState.Loading) { if (!safeHas(storage, TOUR_TAKING_TOO_LONG_STEP_KEY)) { timeoutHandle = window.setTimeout(() => { - safeOpen(searchSessionIndicatorRef); + searchSessionIndicatorRef.openPopover(); }, TOUR_TAKING_TOO_LONG_TIMEOUT); } } if (state === SearchSessionState.Restored) { if (!safeHas(storage, TOUR_RESTORE_STEP_KEY)) { - safeOpen(searchSessionIndicatorRef); + searchSessionIndicatorRef.openPopover(); } } @@ -79,15 +80,3 @@ function safeSet(storage: IStorageWrapper, key: string) { return true; } } - -function safeOpen(searchSessionIndicatorRef: MutableRefObject) { - if (searchSessionIndicatorRef.current) { - searchSessionIndicatorRef.current.openPopover(); - } else { - // TODO: needed for initial open when component is not rendered yet - // fix after: https://github.com/elastic/eui/issues/4460 - setTimeout(() => { - searchSessionIndicatorRef.current?.openPopover(); - }, 50); - } -} diff --git a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.stories.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.stories.tsx index f2d5a3c52daea6..62d95c1043800d 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.stories.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.stories.tsx @@ -33,9 +33,9 @@ storiesOf('components/SearchSessionIndicator', module).add('default', () => (

diff --git a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx index 59c39aecddb329..ff9e27cad1869a 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx @@ -108,11 +108,21 @@ test('Canceled state', async () => { }); test('Disabled state', async () => { - render( + const { rerender } = render( + + + + ); + + await userEvent.click(screen.getByLabelText('Search session loading')); + + expect(screen.getByRole('button', { name: 'Save session' })).toBeDisabled(); + + rerender( - + ); - expect(screen.getByTestId('searchSessionIndicator').querySelector('button')).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Save session' })).toBeDisabled(); }); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx index 9ac537829a670d..eb58039ff58f7d 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx @@ -31,8 +31,10 @@ export interface SearchSessionIndicatorProps { onCancel?: () => void; viewSearchSessionsLink?: string; onSaveResults?: () => void; - disabled?: boolean; - disabledReasonText?: string; + + saveDisabled?: boolean; + saveDisabledReasonText?: string; + onOpened?: (openedState: SearchSessionState) => void; } @@ -55,17 +57,22 @@ const CancelButton = ({ onCancel = () => {}, buttonProps = {} }: ActionButtonPro const ContinueInBackgroundButton = ({ onContinueInBackground = () => {}, buttonProps = {}, + saveDisabled = false, + saveDisabledReasonText, }: ActionButtonProps) => ( - - - + + + + + ); const ViewAllSearchSessionsButton = ({ @@ -84,17 +91,25 @@ const ViewAllSearchSessionsButton = ({ ); -const SaveButton = ({ onSaveResults = () => {}, buttonProps = {} }: ActionButtonProps) => ( - - - +const SaveButton = ({ + onSaveResults = () => {}, + buttonProps = {}, + saveDisabled = false, + saveDisabledReasonText, +}: ActionButtonProps) => ( + + + + + ); const searchSessionIndicatorViewStateToProps: { @@ -325,19 +340,16 @@ export const SearchSessionIndicator = React.forwardRef< className="searchSessionIndicator" data-test-subj={'searchSessionIndicator'} data-state={props.state} + data-save-disabled={props.saveDisabled ?? false} panelClassName={'searchSessionIndicator__panel'} repositionOnScroll={true} button={ - + } diff --git a/x-pack/test/send_search_to_background_integration/services/search_sessions.ts b/x-pack/test/send_search_to_background_integration/services/search_sessions.ts index 69b3e05946345a..bf79d35178a60d 100644 --- a/x-pack/test/send_search_to_background_integration/services/search_sessions.ts +++ b/x-pack/test/send_search_to_background_integration/services/search_sessions.ts @@ -47,9 +47,7 @@ export function SearchSessionsProvider({ getService }: FtrProviderContext) { public async disabledOrFail() { await this.exists(); - await expect(await (await (await this.find()).findByTagName('button')).isEnabled()).to.be( - false - ); + await expect(await (await this.find()).getAttribute('data-save-disabled')).to.be('true'); } public async expectState(state: SessionStateType) { From 14d41c1952335af4c4b8e93f164939354901bfe9 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Mon, 8 Feb 2021 07:47:36 -0800 Subject: [PATCH 29/51] [DOCS] More cleanup in developer docs (#90506) --- docs/developer/contributing/development-ci-metrics.asciidoc | 6 ------ .../getting-started/development-plugin-resources.asciidoc | 4 ++-- src/core/CONVENTIONS.md | 5 +---- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/docs/developer/contributing/development-ci-metrics.asciidoc b/docs/developer/contributing/development-ci-metrics.asciidoc index 9c54ef9c8a916c..3e49686fb67f0e 100644 --- a/docs/developer/contributing/development-ci-metrics.asciidoc +++ b/docs/developer/contributing/development-ci-metrics.asciidoc @@ -44,15 +44,9 @@ All metrics are collected from the `tar.gz` archive produced for the linux platf [[ci-metric-distributable-file-count]] `distributable file count` :: The number of files included in the default distributable. -[[ci-metric-oss-distributable-file-count]] `oss distributable file count` :: -The number of files included in the OSS distributable. - [[ci-metric-distributable-size]] `distributable size` :: The size, in bytes, of the default distributable. _(not reported on PRs)_ -[[ci-metric-oss-distributable-size]] `oss distributable size` :: -The size, in bytes, of the OSS distributable. _(not reported on PRs)_ - [[ci-metric-types-saved-object-field-counts]] ==== Saved Object field counts diff --git a/docs/developer/getting-started/development-plugin-resources.asciidoc b/docs/developer/getting-started/development-plugin-resources.asciidoc index 863a67f3c42f04..9aefeabb32a55c 100644 --- a/docs/developer/getting-started/development-plugin-resources.asciidoc +++ b/docs/developer/getting-started/development-plugin-resources.asciidoc @@ -14,8 +14,8 @@ You can use the <> to get a basic structure for a ne {kib} repo should be developed inside the `plugins` folder. If you are building a new plugin to check in to the {kib} repo, you will choose between a few locations: - - {kib-repo}tree/{branch}/x-pack/plugins[x-pack/plugins] for commercially licensed plugins - - {kib-repo}tree/{branch}/src/plugins[src/plugins] for open source licensed plugins + - {kib-repo}tree/{branch}/x-pack/plugins[x-pack/plugins] for plugins related to subscription features + - {kib-repo}tree/{branch}/src/plugins[src/plugins] for plugins related to free features - {kib-repo}tree/{branch}/examples[examples] for developer example plugins (these will not be included in the distributables) [discrete] diff --git a/src/core/CONVENTIONS.md b/src/core/CONVENTIONS.md index a4f50e73f1c57c..56da185d023a95 100644 --- a/src/core/CONVENTIONS.md +++ b/src/core/CONVENTIONS.md @@ -19,10 +19,7 @@ Definition of done for a feature: - has been verified manually by at least one reviewer - can be used by first & third party plugins - there is no contradiction between client and server API -- works for OSS version - - works with and without a `server.basePath` configured - - cannot crash the Kibana server when it fails -- works for the commercial version with a license +- works with the subscription features - for a logged-in user - for anonymous user - compatible with Spaces From d804f4ff760832bc20dc29bc299b4b8c92bef3a5 Mon Sep 17 00:00:00 2001 From: ymao1 Date: Mon, 8 Feb 2021 11:14:11 -0500 Subject: [PATCH 30/51] Remove extraneous period (#90214) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../application/sections/alert_form/alert_notify_when.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx index b6676cfeed140b..ee0f1c4c0ceb8a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx @@ -34,7 +34,7 @@ const NOTIFY_WHEN_OPTIONS: Array> = [ inputDisplay: i18n.translate( 'xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActionGroupChange.display', { - defaultMessage: 'Only on status change.', + defaultMessage: 'Only on status change', } ), 'data-test-subj': 'onActionGroupChange', From 4b29e35246208a6e0aff3dcaef15c7331ae7241a Mon Sep 17 00:00:00 2001 From: ymao1 Date: Mon, 8 Feb 2021 11:16:36 -0500 Subject: [PATCH 31/51] [Alerting] Fixing bug with Index Threshold alert when selecting "Of" expression (#90174) * Fixing bug * Updating functional test * Fixing functional test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../alert_types/threshold/expression.tsx | 12 ++--- .../public/common/expression_items/of.tsx | 2 + .../common/expression_items/when.test.tsx | 2 + .../public/common/expression_items/when.tsx | 1 + .../alert_create_flyout.ts | 54 ++++++++++++++++--- 5 files changed, 56 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx index 4cccd826731245..aed115a53fa260 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx @@ -124,15 +124,13 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< }); if (indexArray.length > 0) { - await refreshEsFields(); + await refreshEsFields(indexArray); } }; - const refreshEsFields = async () => { - if (indexArray.length > 0) { - const currentEsFields = await getFields(http, indexArray); - setEsFields(currentEsFields); - } + const refreshEsFields = async (indices: string[]) => { + const currentEsFields = await getFields(http, indices); + setEsFields(currentEsFields); }; useEffect(() => { @@ -181,7 +179,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< timeField: '', }); } else { - await refreshEsFields(); + await refreshEsFields(indices); } }} onTimeFieldChange={(updatedTimeField: string) => diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx index be54427b90c572..fbc66914559896 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx @@ -91,6 +91,7 @@ export const OfExpression = ({ defaultMessage: 'of', } )} + data-test-subj="ofExpressionPopover" display={display === 'inline' ? 'inline' : 'columns'} value={aggField || firstFieldOption.text} isActive={aggFieldPopoverOpen || !aggField} @@ -119,6 +120,7 @@ export const OfExpression = ({ 0 && aggField !== undefined} error={errors.aggField} diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.test.tsx index cde6980e146b2b..d97526d89b62bb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.test.tsx @@ -20,6 +20,7 @@ describe('when expression', () => { { { diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts index d38ad278d3f64a..6a051cc9fc5e62 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts @@ -15,6 +15,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const supertest = getService('supertest'); const find = getService('find'); const retry = getService('retry'); + const comboBox = getService('comboBox'); async function getAlertsByName(name: string) { const { @@ -30,15 +31,14 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); } - async function defineAlert(alertName: string, alertType?: string) { - alertType = alertType || '.index-threshold'; + async function defineEsQueryAlert(alertName: string) { await pageObjects.triggersActionsUI.clickCreateAlertButton(); await testSubjects.setValue('alertNameInput', alertName); - await testSubjects.click(`${alertType}-SelectOption`); + await testSubjects.click(`.es-query-SelectOption`); await testSubjects.click('selectIndexExpression'); - const comboBox = await find.byCssSelector('#indexSelectSearchBox'); - await comboBox.click(); - await comboBox.type('k'); + const indexComboBox = await find.byCssSelector('#indexSelectSearchBox'); + await indexComboBox.click(); + await indexComboBox.type('k'); const filterSelectItem = await find.byCssSelector(`.euiFilterSelectItem`); await filterSelectItem.click(); await testSubjects.click('thresholdAlertTimeFieldSelect'); @@ -53,6 +53,44 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await nameInput.click(); } + async function defineIndexThresholdAlert(alertName: string) { + await pageObjects.triggersActionsUI.clickCreateAlertButton(); + await testSubjects.setValue('alertNameInput', alertName); + await testSubjects.click(`.index-threshold-SelectOption`); + await testSubjects.click('selectIndexExpression'); + const indexComboBox = await find.byCssSelector('#indexSelectSearchBox'); + await indexComboBox.click(); + await indexComboBox.type('k'); + const filterSelectItem = await find.byCssSelector(`.euiFilterSelectItem`); + await filterSelectItem.click(); + await testSubjects.click('thresholdAlertTimeFieldSelect'); + await retry.try(async () => { + const fieldOptions = await find.allByCssSelector('#thresholdTimeField option'); + expect(fieldOptions[1]).not.to.be(undefined); + await fieldOptions[1].click(); + }); + await testSubjects.click('closePopover'); + // need this two out of popup clicks to close them + const nameInput = await testSubjects.find('alertNameInput'); + await nameInput.click(); + + await testSubjects.click('whenExpression'); + await testSubjects.click('whenExpressionSelect'); + await retry.try(async () => { + const aggTypeOptions = await find.allByCssSelector('#aggTypeField option'); + expect(aggTypeOptions[1]).not.to.be(undefined); + await aggTypeOptions[1].click(); + }); + + await testSubjects.click('ofExpressionPopover'); + const ofComboBox = await find.byCssSelector('#ofField'); + await ofComboBox.click(); + const ofOptionsString = await comboBox.getOptionsList('availablefieldsOptionsComboBox'); + const ofOptions = ofOptionsString.trim().split('\n'); + expect(ofOptions.length > 0).to.be(true); + await comboBox.set('availablefieldsOptionsComboBox', ofOptions[0]); + } + async function defineAlwaysFiringAlert(alertName: string) { await pageObjects.triggersActionsUI.clickCreateAlertButton(); await testSubjects.setValue('alertNameInput', alertName); @@ -67,7 +105,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should create an alert', async () => { const alertName = generateUniqueKey(); - await defineAlert(alertName); + await defineIndexThresholdAlert(alertName); await testSubjects.click('notifyWhenSelect'); await testSubjects.click('onThrottleInterval'); @@ -222,7 +260,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should successfully test valid es_query alert', async () => { const alertName = generateUniqueKey(); - await defineAlert(alertName, '.es-query'); + await defineEsQueryAlert(alertName); // Valid query await testSubjects.setValue('queryJsonEditor', '{"query":{"match_all":{}}}', { From f6a8d6edc472797482c74b9b369d07fb0475bf43 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 8 Feb 2021 18:45:31 +0200 Subject: [PATCH 32/51] [Security Solution][Case] Fix unhandled promise when updating alert status (#90605) --- x-pack/plugins/case/server/client/comments/add.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/case/server/client/comments/add.ts b/x-pack/plugins/case/server/client/comments/add.ts index 5cfa4d70290f09..58d7c9abcbfd3a 100644 --- a/x-pack/plugins/case/server/client/comments/add.ts +++ b/x-pack/plugins/case/server/client/comments/add.ts @@ -87,7 +87,7 @@ export const addComment = ({ // If the case is synced with alerts the newly attached alert must match the status of the case. if (newComment.attributes.type === CommentType.alert && myCase.attributes.settings.syncAlerts) { - caseClient.updateAlertsStatus({ + await caseClient.updateAlertsStatus({ ids: [newComment.attributes.alertId], status: myCase.attributes.status, }); From ec672f5df22e16a1a2ba97d38ec04774c3592a57 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Mon, 8 Feb 2021 17:48:14 +0100 Subject: [PATCH 33/51] [ML] Handle invalid job ids payload in the Anomaly swim lane (#90597) * [ML] handle invalid job ids payload * [ML] set type for error * [ML] set entire error object --- .../swimlane_input_resolver.test.ts | 36 ++++++++++++++++--- .../swimlane_input_resolver.ts | 16 +++++++-- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts index 2576e5377b39d4..3fffd1588b9b98 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts @@ -57,14 +57,17 @@ describe('useSwimlaneInputResolver', () => { ), }, anomalyDetectorService: { - getJobs$: jest.fn(() => - of([ + getJobs$: jest.fn((jobId: string[]) => { + if (jobId.includes('invalid-job-id')) { + throw new Error('Invalid job'); + } + return of([ { job_id: 'cw_multi_1', analysis_config: { bucket_span: '15m' }, }, - ]) - ), + ]); + }), }, } as unknown) as AnomalySwimlaneServices, ]; @@ -128,6 +131,31 @@ describe('useSwimlaneInputResolver', () => { expect(services[2].anomalyDetectorService.getJobs$).toHaveBeenCalledTimes(2); expect(services[2].anomalyTimelineService.loadOverallData).toHaveBeenCalledTimes(3); }); + + test('should not complete the observable on error', async () => { + const { result } = renderHook(() => + useSwimlaneInputResolver( + embeddableInput as Observable, + onInputChange, + refresh, + services, + 1000, + 1 + ) + ); + + await act(async () => { + embeddableInput.next({ + id: 'test-swimlane-embeddable', + jobIds: ['invalid-job-id'], + swimlaneType: SWIMLANE_TYPE.OVERALL, + filters: [], + query: { language: 'kuery', query: '' }, + } as Partial); + }); + + expect(result.current[6]?.message).toBe('Invalid job'); + }); }); describe('processFilters', () => { diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts index 5b256b9c5924c2..0d75db64a01b93 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts @@ -47,12 +47,17 @@ const FETCH_RESULTS_DEBOUNCE_MS = 500; function getJobsObservable( embeddableInput: Observable, - anomalyDetectorService: AnomalyDetectorService + anomalyDetectorService: AnomalyDetectorService, + setErrorHandler: (e: Error) => void ) { return embeddableInput.pipe( pluck('jobIds'), distinctUntilChanged(isEqual), - switchMap((jobsIds) => anomalyDetectorService.getJobs$(jobsIds)) + switchMap((jobsIds) => anomalyDetectorService.getJobs$(jobsIds)), + catchError((e) => { + setErrorHandler(e.body ?? e); + return of(undefined); + }) ); } @@ -95,7 +100,7 @@ export function useSwimlaneInputResolver( useEffect(() => { const subscription = combineLatest([ - getJobsObservable(embeddableInput, anomalyDetectorService), + getJobsObservable(embeddableInput, anomalyDetectorService, setError), embeddableInput, chartWidth$.pipe(skipWhile((v) => !v)), fromPage$, @@ -112,6 +117,11 @@ export function useSwimlaneInputResolver( tap(setIsLoading.bind(null, true)), debounceTime(FETCH_RESULTS_DEBOUNCE_MS), switchMap(([jobs, input, swimlaneContainerWidth, fromPageInput, perPageFromState]) => { + if (!jobs) { + // couldn't load the list of jobs + return of(undefined); + } + const { viewBy, swimlaneType: swimlaneTypeInput, From bda7b2816f00d288aee774fc3661ed022bd0f270 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Mon, 8 Feb 2021 12:13:55 -0500 Subject: [PATCH 34/51] [Fleet] Cannot delete a managed agent policy (#90505) ## Summary Managed policy cannot be deleted via API or UI closes https://github.com/elastic/kibana/issues/90448 ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios #### Manual testing
UI screenshot Screen Shot 2021-02-05 at 1 56 13 PM
API commands ``` ## Create a managed policy curl --user elastic:changeme -X POST localhost:5601/api/fleet/agent_policies -H 'Content-Type: application/json' -d'{ "name": "User created MANAGED", "namespace": "default", "is_managed": true}' -H 'kbn-xsrf: true' {"item":{"id":"17ebd160-67ee-11eb-adb2-f16c6e20580c","name":"User created MANAGED","namespace":"default","is_managed":true,"revision":1,"updated_at":"2021-02-05T20:09:46.614Z","updated_by":"elastic"}} ## Cannot delete it curl --user elastic:changeme -X POST 'http://localhost:5601/api/fleet/agent_policies/delete' -H 'kbn-xsrf: abc' -H 'Content-Type: application/json' --data-raw '{"agentPolicyId": "17ebd160-67ee-11eb-adb2-f16c6e20580c" }' { "statusCode": 400, "error": "Bad Request", "message": "Cannot delete managed policy 17ebd160-67ee-11eb-adb2-f16c6e20580c" } ## Set policy to unmanaged curl --user elastic:changeme -X PUT localhost:5601/api/fleet/agent_policies/17ebd160-67ee-11eb-adb2-f16c6e20580c -H 'Content-Type: application/json' -d'{ "name": "User created MANAGED", "namespace": "default", "is_managed": false}' -H 'kbn-xsrf: true' { "item": { "id": "17ebd160-67ee-11eb-adb2-f16c6e20580c", "name": "User created MANAGED", "namespace": "default", "is_managed": false, "revision": 3, "updated_at": "2021-02-05T20:10:45.383Z", "updated_by": "elastic", "package_policies": [] } } ## Can delete curl --user elastic:changeme -X POST 'http://localhost:5601/api/fleet/agent_policies/delete' -H 'kbn-xsrf: abc' -H 'Content-Type: application/json' --data-raw '{"agentPolicyId": "17ebd160-67ee-11eb-adb2-f16c6e20580c" }' { "id": "17ebd160-67ee-11eb-adb2-f16c6e20580c", "name": "User created MANAGED" } ```
--- x-pack/plugins/fleet/server/errors/index.ts | 1 + .../fleet/server/services/agent_policy.ts | 6 +- .../apis/agent_policy/agent_policy.ts | 93 +++++++++++++++++-- 3 files changed, 93 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts index a903de01380392..b34568b5fc6afd 100644 --- a/x-pack/plugins/fleet/server/errors/index.ts +++ b/x-pack/plugins/fleet/server/errors/index.ts @@ -34,3 +34,4 @@ export class FleetAdminUserInvalidError extends IngestManagerError {} export class ConcurrentInstallOperationError extends IngestManagerError {} export class AgentReassignmentError extends IngestManagerError {} export class AgentUnenrollmentError extends IngestManagerError {} +export class AgentPolicyDeletionError extends IngestManagerError {} diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index ca131efeff68cc..9800ddf95f7b22 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -36,7 +36,7 @@ import { FleetServerPolicy, AGENT_POLICY_INDEX, } from '../../common'; -import { AgentPolicyNameExistsError } from '../errors'; +import { AgentPolicyNameExistsError, AgentPolicyDeletionError } from '../errors'; import { createAgentPolicyAction, listAgents } from './agents'; import { packagePolicyService } from './package_policy'; import { outputService } from './output'; @@ -448,6 +448,10 @@ class AgentPolicyService { throw new Error('Agent policy not found'); } + if (agentPolicy.is_managed) { + throw new AgentPolicyDeletionError(`Cannot delete managed policy ${id}`); + } + const { defaultAgentPolicy: { id: defaultAgentPolicyId }, } = await this.ensureDefaultAgentPolicy(soClient, esClient); diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts index 9f016ab044a90e..2ba83bff6f1b13 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts @@ -38,9 +38,8 @@ export default function ({ getService }: FtrProviderContext) { }) .expect(200); - const getRes = await supertest.get(`/api/fleet/agent_policies/${createdPolicy.id}`); - const json = getRes.body; - expect(json.item.is_managed).to.equal(false); + const { body } = await supertest.get(`/api/fleet/agent_policies/${createdPolicy.id}`); + expect(body.item.is_managed).to.equal(false); }); it('sets given is_managed value', async () => { @@ -56,9 +55,25 @@ export default function ({ getService }: FtrProviderContext) { }) .expect(200); - const getRes = await supertest.get(`/api/fleet/agent_policies/${createdPolicy.id}`); - const json = getRes.body; - expect(json.item.is_managed).to.equal(true); + const { body } = await supertest.get(`/api/fleet/agent_policies/${createdPolicy.id}`); + expect(body.item.is_managed).to.equal(true); + + const { + body: { item: createdPolicy2 }, + } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'TEST3', + namespace: 'default', + is_managed: false, + }) + .expect(200); + + const { + body: { item: policy2 }, + } = await supertest.get(`/api/fleet/agent_policies/${createdPolicy2.id}`); + expect(policy2.is_managed).to.equal(false); }); it('should return a 400 with an empty namespace', async () => { @@ -242,6 +257,23 @@ export default function ({ getService }: FtrProviderContext) { const getRes = await supertest.get(`/api/fleet/agent_policies/${createdPolicy.id}`); const json = getRes.body; expect(json.item.is_managed).to.equal(true); + + const { + body: { item: createdPolicy2 }, + } = await supertest + .put(`/api/fleet/agent_policies/${agentPolicyId}`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'TEST2', + namespace: 'default', + is_managed: false, + }) + .expect(200); + + const { + body: { item: policy2 }, + } = await supertest.get(`/api/fleet/agent_policies/${createdPolicy2.id}`); + expect(policy2.is_managed).to.equal(false); }); it('should return a 409 if policy already exists with name given', async () => { @@ -276,5 +308,54 @@ export default function ({ getService }: FtrProviderContext) { expect(body.message).to.match(/already exists?/); }); }); + + describe('POST /api/fleet/agent_policies/delete', () => { + let managedPolicy: any | undefined; + it('should prevent managed policies being deleted', async () => { + const { + body: { item: createdPolicy }, + } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Managed policy', + namespace: 'default', + is_managed: true, + }) + .expect(200); + managedPolicy = createdPolicy; + const { body } = await supertest + .post('/api/fleet/agent_policies/delete') + .set('kbn-xsrf', 'xxx') + .send({ agentPolicyId: managedPolicy.id }) + .expect(400); + + expect(body.message).to.contain('Cannot delete managed policy'); + }); + + it('should allow unmanaged policies being deleted', async () => { + const { + body: { item: unmanagedPolicy }, + } = await supertest + .put(`/api/fleet/agent_policies/${managedPolicy.id}`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Unmanaged policy', + namespace: 'default', + is_managed: false, + }) + .expect(200); + + const { body } = await supertest + .post('/api/fleet/agent_policies/delete') + .set('kbn-xsrf', 'xxx') + .send({ agentPolicyId: unmanagedPolicy.id }); + + expect(body).to.eql({ + id: unmanagedPolicy.id, + name: 'Unmanaged policy', + }); + }); + }); }); } From c306a444f5550faee08d40612386e52731fc657f Mon Sep 17 00:00:00 2001 From: Sonja Krause-Harder Date: Mon, 8 Feb 2021 18:22:30 +0100 Subject: [PATCH 35/51] [EPM] Conditionally generate ES index pattern name based on dataset_is_prefix (#89870) * Explicitly generate ES index pattern name. * Adjust tests. * Adjust and reenable tests. * Set template priority based on dataset_is_prefix * Refactor indexPatternName -> templateIndexPattern * Add unit tests. * Use more realistic index pattern in test. * Fix unit test. * Add unit test for installTemplate(). Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/fleet/common/types/models/epm.ts | 1 + .../elasticsearch/template/install.test.ts | 110 ++++++++++++++++++ .../epm/elasticsearch/template/install.ts | 14 ++- .../elasticsearch/template/template.test.ts | 99 +++++++++++++--- .../epm/elasticsearch/template/template.ts | 55 +++++++-- .../fleet_api_integration/apis/epm/index.js | 2 +- .../apis/epm/template.ts | 21 +++- 7 files changed, 268 insertions(+), 34 deletions(-) create mode 100644 x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 0f59befc2e4673..e7e5a931b7429f 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -221,6 +221,7 @@ export interface RegistryDataStream { path: string; ingest_pipeline: string; elasticsearch?: RegistryElasticsearch; + dataset_is_prefix?: boolean; } export interface RegistryElasticsearch { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts new file mode 100644 index 00000000000000..be9213aff360d0 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RegistryDataStream } from '../../../../types'; +import { Field } from '../../fields/field'; + +import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import { installTemplate } from './install'; + +test('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix not set', async () => { + const callCluster = elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser; + const fields: Field[] = []; + const dataStreamDatasetIsPrefixUnset = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + } as RegistryDataStream; + const pkg = { + name: 'package', + version: '0.0.1', + }; + const templateIndexPatternDatasetIsPrefixUnset = 'metrics-package.dataset-*'; + const templatePriorityDatasetIsPrefixUnset = 200; + await installTemplate({ + callCluster, + fields, + dataStream: dataStreamDatasetIsPrefixUnset, + packageVersion: pkg.version, + packageName: pkg.name, + }); + // @ts-ignore + const sentTemplate = callCluster.mock.calls[0][1].body; + expect(sentTemplate).toBeDefined(); + expect(sentTemplate.priority).toBe(templatePriorityDatasetIsPrefixUnset); + expect(sentTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixUnset]); +}); + +test('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix set to false', async () => { + const callCluster = elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser; + const fields: Field[] = []; + const dataStreamDatasetIsPrefixFalse = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + dataset_is_prefix: false, + } as RegistryDataStream; + const pkg = { + name: 'package', + version: '0.0.1', + }; + const templateIndexPatternDatasetIsPrefixFalse = 'metrics-package.dataset-*'; + const templatePriorityDatasetIsPrefixFalse = 200; + await installTemplate({ + callCluster, + fields, + dataStream: dataStreamDatasetIsPrefixFalse, + packageVersion: pkg.version, + packageName: pkg.name, + }); + // @ts-ignore + const sentTemplate = callCluster.mock.calls[0][1].body; + expect(sentTemplate).toBeDefined(); + expect(sentTemplate.priority).toBe(templatePriorityDatasetIsPrefixFalse); + expect(sentTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixFalse]); +}); + +test('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix set to true', async () => { + const callCluster = elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser; + const fields: Field[] = []; + const dataStreamDatasetIsPrefixTrue = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + dataset_is_prefix: true, + } as RegistryDataStream; + const pkg = { + name: 'package', + version: '0.0.1', + }; + const templateIndexPatternDatasetIsPrefixTrue = 'metrics-package.dataset.*-*'; + const templatePriorityDatasetIsPrefixTrue = 150; + await installTemplate({ + callCluster, + fields, + dataStream: dataStreamDatasetIsPrefixTrue, + packageVersion: pkg.version, + packageName: pkg.name, + }); + // @ts-ignore + const sentTemplate = callCluster.mock.calls[0][1].body; + expect(sentTemplate).toBeDefined(); + expect(sentTemplate.priority).toBe(templatePriorityDatasetIsPrefixTrue); + expect(sentTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixTrue]); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts index 10e94d93bbc8e2..f5f1b4bea788d4 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts @@ -17,7 +17,13 @@ import { import { CallESAsCurrentUser } from '../../../../types'; import { Field, loadFieldsFromYaml, processFields } from '../../fields/field'; import { getPipelineNameForInstallation } from '../ingest_pipeline/install'; -import { generateMappings, generateTemplateName, getTemplate } from './template'; +import { + generateMappings, + generateTemplateName, + generateTemplateIndexPattern, + getTemplate, + getTemplatePriority, +} from './template'; import { getAsset, getPathParts } from '../../archive'; import { removeAssetsFromInstalledEsByType, saveInstalledEsRefs } from '../../packages/install'; @@ -293,6 +299,9 @@ export async function installTemplate({ }): Promise { const mappings = generateMappings(processFields(fields)); const templateName = generateTemplateName(dataStream); + const templateIndexPattern = generateTemplateIndexPattern(dataStream); + const templatePriority = getTemplatePriority(dataStream); + let pipelineName; if (dataStream.ingest_pipeline) { pipelineName = getPipelineNameForInstallation({ @@ -310,11 +319,12 @@ export async function installTemplate({ const template = getTemplate({ type: dataStream.type, - templateName, + templateIndexPattern, mappings, pipelineName, packageName, composedOfTemplates, + templatePriority, ilmPolicy: dataStream.ilm_policy, hidden: dataStream.hidden, }); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts index 80386a2a0dd56c..a176805307845c 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts @@ -8,8 +8,14 @@ import { readFileSync } from 'fs'; import { safeLoad } from 'js-yaml'; import path from 'path'; +import { RegistryDataStream } from '../../../../types'; import { Field, processFields } from '../../fields/field'; -import { generateMappings, getTemplate } from './template'; +import { + generateMappings, + getTemplate, + getTemplatePriority, + generateTemplateIndexPattern, +} from './template'; // Add our own serialiser to just do JSON.stringify expect.addSnapshotSerializer({ @@ -23,16 +29,17 @@ expect.addSnapshotSerializer({ }); test('get template', () => { - const templateName = 'logs-nginx-access-abcd'; + const templateIndexPattern = 'logs-nginx.access-abcd-*'; const template = getTemplate({ type: 'logs', - templateName, + templateIndexPattern, packageName: 'nginx', mappings: { properties: {} }, composedOfTemplates: [], + templatePriority: 200, }); - expect(template.index_patterns).toStrictEqual([`${templateName}-*`]); + expect(template.index_patterns).toStrictEqual([templateIndexPattern]); }); test('adds composed_of correctly', () => { @@ -40,10 +47,11 @@ test('adds composed_of correctly', () => { const template = getTemplate({ type: 'logs', - templateName: 'name', + templateIndexPattern: 'name-*', packageName: 'nginx', mappings: { properties: {} }, composedOfTemplates, + templatePriority: 200, }); expect(template.composed_of).toStrictEqual(composedOfTemplates); }); @@ -53,35 +61,36 @@ test('adds empty composed_of correctly', () => { const template = getTemplate({ type: 'logs', - templateName: 'name', + templateIndexPattern: 'name-*', packageName: 'nginx', mappings: { properties: {} }, composedOfTemplates, + templatePriority: 200, }); expect(template.composed_of).toStrictEqual(composedOfTemplates); }); test('adds hidden field correctly', () => { - const templateWithHiddenName = 'logs-nginx-access-abcd'; + const templateIndexPattern = 'logs-nginx.access-abcd-*'; const templateWithHidden = getTemplate({ type: 'logs', - templateName: templateWithHiddenName, + templateIndexPattern, packageName: 'nginx', mappings: { properties: {} }, composedOfTemplates: [], + templatePriority: 200, hidden: true, }); expect(templateWithHidden.data_stream.hidden).toEqual(true); - const templateWithoutHiddenName = 'logs-nginx-access-efgh'; - const templateWithoutHidden = getTemplate({ type: 'logs', - templateName: templateWithoutHiddenName, + templateIndexPattern, packageName: 'nginx', mappings: { properties: {} }, composedOfTemplates: [], + templatePriority: 200, }); expect(templateWithoutHidden.data_stream.hidden).toEqual(undefined); }); @@ -95,10 +104,11 @@ test('tests loading base.yml', () => { const mappings = generateMappings(processedFields); const template = getTemplate({ type: 'logs', - templateName: 'foo', + templateIndexPattern: 'foo-*', packageName: 'nginx', mappings, composedOfTemplates: [], + templatePriority: 200, }); expect(template).toMatchSnapshot(path.basename(ymlPath)); @@ -113,10 +123,11 @@ test('tests loading coredns.logs.yml', () => { const mappings = generateMappings(processedFields); const template = getTemplate({ type: 'logs', - templateName: 'foo', + templateIndexPattern: 'foo-*', packageName: 'coredns', mappings, composedOfTemplates: [], + templatePriority: 200, }); expect(template).toMatchSnapshot(path.basename(ymlPath)); @@ -131,10 +142,11 @@ test('tests loading system.yml', () => { const mappings = generateMappings(processedFields); const template = getTemplate({ type: 'metrics', - templateName: 'whatsthis', + templateIndexPattern: 'whatsthis-*', packageName: 'system', mappings, composedOfTemplates: [], + templatePriority: 200, }); expect(template).toMatchSnapshot(path.basename(ymlPath)); @@ -520,3 +532,62 @@ test('tests constant_keyword field type handling', () => { const mappings = generateMappings(processedFields); expect(JSON.stringify(mappings)).toEqual(JSON.stringify(constantKeywordMapping)); }); + +test('tests priority and index pattern for data stream without dataset_is_prefix', () => { + const dataStreamDatasetIsPrefixUnset = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + } as RegistryDataStream; + const templateIndexPatternDatasetIsPrefixUnset = 'metrics-package.dataset-*'; + const templatePriorityDatasetIsPrefixUnset = 200; + const templateIndexPattern = generateTemplateIndexPattern(dataStreamDatasetIsPrefixUnset); + const templatePriority = getTemplatePriority(dataStreamDatasetIsPrefixUnset); + + expect(templateIndexPattern).toEqual(templateIndexPatternDatasetIsPrefixUnset); + expect(templatePriority).toEqual(templatePriorityDatasetIsPrefixUnset); +}); + +test('tests priority and index pattern for data stream with dataset_is_prefix set to false', () => { + const dataStreamDatasetIsPrefixFalse = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + dataset_is_prefix: false, + } as RegistryDataStream; + const templateIndexPatternDatasetIsPrefixFalse = 'metrics-package.dataset-*'; + const templatePriorityDatasetIsPrefixFalse = 200; + const templateIndexPattern = generateTemplateIndexPattern(dataStreamDatasetIsPrefixFalse); + const templatePriority = getTemplatePriority(dataStreamDatasetIsPrefixFalse); + + expect(templateIndexPattern).toEqual(templateIndexPatternDatasetIsPrefixFalse); + expect(templatePriority).toEqual(templatePriorityDatasetIsPrefixFalse); +}); + +test('tests priority and index pattern for data stream with dataset_is_prefix set to true', () => { + const dataStreamDatasetIsPrefixTrue = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + dataset_is_prefix: true, + } as RegistryDataStream; + const templateIndexPatternDatasetIsPrefixTrue = 'metrics-package.dataset.*-*'; + const templatePriorityDatasetIsPrefixTrue = 150; + const templateIndexPattern = generateTemplateIndexPattern(dataStreamDatasetIsPrefixTrue); + const templatePriority = getTemplatePriority(dataStreamDatasetIsPrefixTrue); + + expect(templateIndexPattern).toEqual(templateIndexPatternDatasetIsPrefixTrue); + expect(templatePriority).toEqual(templatePriorityDatasetIsPrefixTrue); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts index ea0bb5dc53a1e4..b86c989f8c24c8 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts @@ -33,6 +33,10 @@ export interface CurrentDataStream { const DEFAULT_SCALING_FACTOR = 1000; const DEFAULT_IGNORE_ABOVE = 1024; +// see discussion in https://github.com/elastic/kibana/issues/88307 +const DEFAULT_TEMPLATE_PRIORITY = 200; +const DATASET_IS_PREFIX_TEMPLATE_PRIORITY = 150; + /** * getTemplate retrieves the default template but overwrites the index pattern with the given value. * @@ -40,29 +44,32 @@ const DEFAULT_IGNORE_ABOVE = 1024; */ export function getTemplate({ type, - templateName, + templateIndexPattern, mappings, pipelineName, packageName, composedOfTemplates, + templatePriority, ilmPolicy, hidden, }: { type: string; - templateName: string; + templateIndexPattern: string; mappings: IndexTemplateMappings; pipelineName?: string | undefined; packageName: string; composedOfTemplates: string[]; + templatePriority: number; ilmPolicy?: string | undefined; hidden?: boolean; }): IndexTemplate { const template = getBaseTemplate( type, - templateName, + templateIndexPattern, mappings, packageName, composedOfTemplates, + templatePriority, ilmPolicy, hidden ); @@ -242,6 +249,35 @@ export function generateTemplateName(dataStream: RegistryDataStream): string { return getRegistryDataStreamAssetBaseName(dataStream); } +export function generateTemplateIndexPattern(dataStream: RegistryDataStream): string { + // undefined or explicitly set to false + // See also https://github.com/elastic/package-spec/pull/102 + if (!dataStream.dataset_is_prefix) { + return getRegistryDataStreamAssetBaseName(dataStream) + '-*'; + } else { + return getRegistryDataStreamAssetBaseName(dataStream) + '.*-*'; + } +} + +// Template priorities are discussed in https://github.com/elastic/kibana/issues/88307 +// See also https://www.elastic.co/guide/en/elasticsearch/reference/current/index-templates.html +// +// Built-in templates like logs-*-* and metrics-*-* have priority 100 +// +// EPM generated templates for data streams have priority 200 (DEFAULT_TEMPLATE_PRIORITY) +// +// EPM generated templates for data streams with dataset_is_prefix: true have priority 150 (DATASET_IS_PREFIX_TEMPLATE_PRIORITY) + +export function getTemplatePriority(dataStream: RegistryDataStream): number { + // undefined or explicitly set to false + // See also https://github.com/elastic/package-spec/pull/102 + if (!dataStream.dataset_is_prefix) { + return DEFAULT_TEMPLATE_PRIORITY; + } else { + return DATASET_IS_PREFIX_TEMPLATE_PRIORITY; + } +} + /** * Returns a map of the data stream path fields to elasticsearch index pattern. * @param dataStreams an array of RegistryDataStream objects @@ -255,17 +291,18 @@ export function generateESIndexPatterns( const patterns: Record = {}; for (const dataStream of dataStreams) { - patterns[dataStream.path] = generateTemplateName(dataStream) + '-*'; + patterns[dataStream.path] = generateTemplateIndexPattern(dataStream); } return patterns; } function getBaseTemplate( type: string, - templateName: string, + templateIndexPattern: string, mappings: IndexTemplateMappings, packageName: string, composedOfTemplates: string[], + templatePriority: number, ilmPolicy?: string | undefined, hidden?: boolean ): IndexTemplate { @@ -279,13 +316,9 @@ function getBaseTemplate( }; return { - // This takes precedence over all index templates installed by ES by default (logs-*-* and metrics-*-*) - // if this number is lower than the ES value (which is 100) this template will never be applied when a data stream - // is created. I'm using 200 here to give some room for users to create their own template and fit it between the - // default and the one the ingest manager uses. - priority: 200, + priority: templatePriority, // To be completed with the correct index patterns - index_patterns: [`${templateName}-*`], + index_patterns: [templateIndexPattern], template: { settings: { index: { diff --git a/x-pack/test/fleet_api_integration/apis/epm/index.js b/x-pack/test/fleet_api_integration/apis/epm/index.js index 23b7464a317e90..0020e6bdf1bb01 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/index.js +++ b/x-pack/test/fleet_api_integration/apis/epm/index.js @@ -11,7 +11,7 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./setup')); loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./file')); - //loadTestFile(require.resolve('./template')); + loadTestFile(require.resolve('./template')); loadTestFile(require.resolve('./ilm')); loadTestFile(require.resolve('./install_by_upload')); loadTestFile(require.resolve('./install_overrides')); diff --git a/x-pack/test/fleet_api_integration/apis/epm/template.ts b/x-pack/test/fleet_api_integration/apis/epm/template.ts index c7e9e211552578..d79452ca0eb6f2 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/template.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/template.ts @@ -10,8 +10,8 @@ import { FtrProviderContext } from '../../../api_integration/ftr_provider_contex import { getTemplate } from '../../../../plugins/fleet/server/services/epm/elasticsearch/template/template'; export default function ({ getService }: FtrProviderContext) { - const indexPattern = 'foo'; const templateName = 'bar'; + const templateIndexPattern = 'bar-*'; const es = getService('es'); const mappings = { properties: { @@ -25,27 +25,36 @@ export default function ({ getService }: FtrProviderContext) { it('can be loaded', async () => { const template = getTemplate({ type: 'logs', - templateName, + templateIndexPattern, mappings, packageName: 'system', composedOfTemplates: [], + templatePriority: 200, }); // This test is not an API integration test with Kibana // We want to test here if the template is valid and for this we need a running ES instance. // If the ES instance takes the template, we assume it is a valid template. - const { body: response1 } = await es.indices.putTemplate({ - name: templateName, + const { body: response1 } = await es.transport.request({ + method: 'PUT', + path: `/_index_template/${templateName}`, body: template, }); + // Checks if template loading worked as expected expect(response1).to.eql({ acknowledged: true }); - const { body: response2 } = await es.indices.getTemplate({ name: templateName }); + const { body: response2 } = await es.transport.request({ + method: 'GET', + path: `/_index_template/${templateName}`, + }); + // Checks if the content of the template that was loaded is as expected // We already know based on the above test that the template was valid // but we check here also if we wrote the index pattern inside the template as expected - expect(response2[templateName].index_patterns).to.eql([`${indexPattern}-*`]); + expect(response2.index_templates[0].index_template.index_patterns).to.eql([ + templateIndexPattern, + ]); }); }); } From a0ce7b5aa887d34a7a892553c66f25d72e38d827 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 8 Feb 2021 09:47:55 -0800 Subject: [PATCH 36/51] [kbn/optimizer][ci-stats] ship metrics separate from build (#90482) Co-authored-by: spalger Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 2 + .../src/ci_stats_reporter/index.ts | 1 + .../ci_stats_reporter/ship_ci_stats_cli.ts | 48 +++++++ packages/kbn-optimizer/src/cli.ts | 17 +-- .../kbn-optimizer/src/common/bundle.test.ts | 2 + packages/kbn-optimizer/src/common/bundle.ts | 16 ++- .../src/common/bundle_cache.test.ts | 14 +- .../kbn-optimizer/src/common/bundle_cache.ts | 22 ++- packages/kbn-optimizer/src/index.ts | 1 - .../basic_optimization.test.ts.snap | 35 ++++- .../basic_optimization.test.ts | 15 +- packages/kbn-optimizer/src/limits.ts | 21 ++- .../src/optimizer/get_output_stats.ts | 118 ---------------- .../src/optimizer/get_plugin_bundles.test.ts | 10 +- .../src/optimizer/get_plugin_bundles.ts | 5 +- packages/kbn-optimizer/src/optimizer/index.ts | 1 - .../src/optimizer/optimizer_config.test.ts | 8 +- .../src/optimizer/optimizer_config.ts | 10 +- .../src/report_optimizer_stats.ts | 46 ------ .../src/worker/bundle_metrics_plugin.ts | 108 ++++++++++++++ .../src/worker/emit_stats_plugin.ts | 34 +++++ .../worker/populate_bundle_cache_plugin.ts | 132 ++++++++++++++++++ .../kbn-optimizer/src/worker/run_compilers.ts | 122 +--------------- .../src/worker/webpack.config.ts | 6 + .../src/integration_tests/build.test.ts | 3 +- .../kbn-plugin-helpers/src/tasks/optimize.ts | 8 +- scripts/ship_ci_stats.js | 10 ++ .../tasks/build_kibana_platform_plugins.ts | 39 ++++-- test/scripts/jenkins_baseline.sh | 4 + test/scripts/jenkins_build_kibana.sh | 3 + test/scripts/jenkins_xpack_baseline.sh | 4 + test/scripts/jenkins_xpack_build_kibana.sh | 4 + yarn.lock | 2 +- 33 files changed, 518 insertions(+), 353 deletions(-) create mode 100644 packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts delete mode 100644 packages/kbn-optimizer/src/optimizer/get_output_stats.ts delete mode 100644 packages/kbn-optimizer/src/report_optimizer_stats.ts create mode 100644 packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts create mode 100644 packages/kbn-optimizer/src/worker/emit_stats_plugin.ts create mode 100644 packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts create mode 100644 scripts/ship_ci_stats.js diff --git a/package.json b/package.json index b224f0c1ae0d5b..7144745f2ae358 100644 --- a/package.json +++ b/package.json @@ -558,6 +558,7 @@ "@types/webpack": "^4.41.3", "@types/webpack-env": "^1.15.3", "@types/webpack-merge": "^4.1.5", + "@types/webpack-sources": "^0.1.4", "@types/write-pkg": "^3.1.0", "@types/xml-crypto": "^1.4.1", "@types/xml2js": "^0.4.5", @@ -843,6 +844,7 @@ "webpack-cli": "^3.3.12", "webpack-dev-server": "^3.11.0", "webpack-merge": "^4.2.2", + "webpack-sources": "^1.4.1", "write-pkg": "^4.0.0", "xml-crypto": "^2.0.0", "xmlbuilder": "13.0.2", diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts index 165239cbebb89d..d99217c38b410a 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts @@ -7,3 +7,4 @@ */ export * from './ci_stats_reporter'; +export * from './ship_ci_stats_cli'; diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts new file mode 100644 index 00000000000000..244af7b6574183 --- /dev/null +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; +import Fs from 'fs'; + +import { CiStatsReporter } from './ci_stats_reporter'; +import { run, createFlagError } from '../run'; + +export function shipCiStatsCli() { + run( + async ({ log, flags }) => { + let metricPaths = flags.metrics; + if (typeof metricPaths === 'string') { + metricPaths = [metricPaths]; + } else if (!Array.isArray(metricPaths) || !metricPaths.every((p) => typeof p === 'string')) { + throw createFlagError('expected --metrics to be a string'); + } + + const reporter = CiStatsReporter.fromEnv(log); + for (const path of metricPaths) { + // resolve path from CLI relative to CWD + const abs = Path.resolve(path); + const json = Fs.readFileSync(abs, 'utf8'); + await reporter.metrics(JSON.parse(json)); + log.success('shipped metrics from', path); + } + }, + { + description: 'ship ci-stats which have been written to files', + usage: `node scripts/ship_ci_stats`, + log: { + defaultLevel: 'debug', + }, + flags: { + string: ['metrics'], + help: ` + --metrics [path] A path to a JSON file that includes metrics which should be sent. Multiple instances supported + `, + }, + } + ); +} diff --git a/packages/kbn-optimizer/src/cli.ts b/packages/kbn-optimizer/src/cli.ts index 3021982b8ed6a6..8fb906aa4603e8 100644 --- a/packages/kbn-optimizer/src/cli.ts +++ b/packages/kbn-optimizer/src/cli.ts @@ -12,11 +12,10 @@ import Path from 'path'; import { REPO_ROOT } from '@kbn/utils'; import { lastValueFrom } from '@kbn/std'; -import { run, createFlagError, CiStatsReporter } from '@kbn/dev-utils'; +import { run, createFlagError } from '@kbn/dev-utils'; import { logOptimizerState } from './log_optimizer_state'; import { OptimizerConfig } from './optimizer'; -import { reportOptimizerStats } from './report_optimizer_stats'; import { runOptimizer } from './run_optimizer'; import { validateLimitsForAllBundles, updateBundleLimits } from './limits'; @@ -120,17 +119,7 @@ run( return; } - let update$ = runOptimizer(config); - - if (reportStats) { - const reporter = CiStatsReporter.fromEnv(log); - - if (!reporter.isEnabled()) { - log.warning('Unable to initialize CiStatsReporter from env'); - } - - update$ = update$.pipe(reportOptimizerStats(reporter, config, log)); - } + const update$ = runOptimizer(config); await lastValueFrom(update$.pipe(logOptimizerState(log, config))); @@ -153,7 +142,6 @@ run( 'cache', 'profile', 'inspect-workers', - 'report-stats', 'validate-limits', 'update-limits', ], @@ -179,7 +167,6 @@ run( --dist create bundles that are suitable for inclusion in the Kibana distributable, enabled when running with --update-limits --scan-dir add a directory to the list of directories scanned for plugins (specify as many times as necessary) --no-inspect-workers when inspecting the parent process, don't inspect the workers - --report-stats attempt to report stats about this execution of the build to the kibana-ci-stats service using this name --validate-limits validate the limits.yml config to ensure that there are limits defined for every bundle --update-limits run a build and rewrite the limits file to include the current bundle sizes +5kb `, diff --git a/packages/kbn-optimizer/src/common/bundle.test.ts b/packages/kbn-optimizer/src/common/bundle.test.ts index b6d25f69e58b4d..ff9aa6fd906280 100644 --- a/packages/kbn-optimizer/src/common/bundle.test.ts +++ b/packages/kbn-optimizer/src/common/bundle.test.ts @@ -42,6 +42,7 @@ it('creates cache keys', () => { "id": "bar", "manifestPath": undefined, "outputDir": "/foo/bar/target", + "pageLoadAssetSizeLimit": undefined, "publicDirNames": Array [ "public", ], @@ -79,6 +80,7 @@ it('parses bundles from JSON specs', () => { "id": "bar", "manifestPath": undefined, "outputDir": "/foo/bar/target", + "pageLoadAssetSizeLimit": undefined, "publicDirNames": Array [ "public", ], diff --git a/packages/kbn-optimizer/src/common/bundle.ts b/packages/kbn-optimizer/src/common/bundle.ts index cb6096759739bf..64b44de0dd1b3e 100644 --- a/packages/kbn-optimizer/src/common/bundle.ts +++ b/packages/kbn-optimizer/src/common/bundle.ts @@ -36,6 +36,8 @@ export interface BundleSpec { readonly banner?: string; /** Absolute path to a kibana.json manifest file, if omitted we assume there are not dependenices */ readonly manifestPath?: string; + /** Maximum allowed page load asset size for the bundles page load asset */ + readonly pageLoadAssetSizeLimit?: number; } export class Bundle { @@ -63,6 +65,8 @@ export class Bundle { * Every bundle mentioned in the `requiredBundles` must be built together. */ public readonly manifestPath: BundleSpec['manifestPath']; + /** Maximum allowed page load asset size for the bundles page load asset */ + public readonly pageLoadAssetSizeLimit: BundleSpec['pageLoadAssetSizeLimit']; public readonly cache: BundleCache; @@ -75,8 +79,9 @@ export class Bundle { this.outputDir = spec.outputDir; this.manifestPath = spec.manifestPath; this.banner = spec.banner; + this.pageLoadAssetSizeLimit = spec.pageLoadAssetSizeLimit; - this.cache = new BundleCache(Path.resolve(this.outputDir, '.kbn-optimizer-cache')); + this.cache = new BundleCache(this.outputDir); } /** @@ -107,6 +112,7 @@ export class Bundle { outputDir: this.outputDir, manifestPath: this.manifestPath, banner: this.banner, + pageLoadAssetSizeLimit: this.pageLoadAssetSizeLimit, }; } @@ -222,6 +228,13 @@ export function parseBundles(json: string) { } } + const { pageLoadAssetSizeLimit } = spec; + if (pageLoadAssetSizeLimit !== undefined) { + if (!(typeof pageLoadAssetSizeLimit === 'number')) { + throw new Error('`bundles[]` must have a numeric `pageLoadAssetSizeLimit` property'); + } + } + return new Bundle({ type, id, @@ -231,6 +244,7 @@ export function parseBundles(json: string) { outputDir, banner, manifestPath, + pageLoadAssetSizeLimit, }); } ); diff --git a/packages/kbn-optimizer/src/common/bundle_cache.test.ts b/packages/kbn-optimizer/src/common/bundle_cache.test.ts index 82a8c0debb83c4..e903a687908b9f 100644 --- a/packages/kbn-optimizer/src/common/bundle_cache.test.ts +++ b/packages/kbn-optimizer/src/common/bundle_cache.test.ts @@ -25,12 +25,12 @@ beforeEach(() => { }); it(`doesn't complain if files are not on disk`, () => { - const cache = new BundleCache('/foo/bar.json'); + const cache = new BundleCache('/foo'); expect(cache.get()).toEqual({}); }); it(`updates files on disk when calling set()`, () => { - const cache = new BundleCache('/foo/bar.json'); + const cache = new BundleCache('/foo'); cache.set(SOME_STATE); expect(mockReadFileSync).not.toHaveBeenCalled(); expect(mockMkdirSync.mock.calls).toMatchInlineSnapshot(` @@ -46,7 +46,7 @@ it(`updates files on disk when calling set()`, () => { expect(mockWriteFileSync.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/foo/bar.json", + "/foo/.kbn-optimizer-cache", "{ \\"cacheKey\\": \\"abc\\", \\"files\\": [ @@ -61,7 +61,7 @@ it(`updates files on disk when calling set()`, () => { }); it(`serves updated state from memory`, () => { - const cache = new BundleCache('/foo/bar.json'); + const cache = new BundleCache('/foo'); cache.set(SOME_STATE); jest.clearAllMocks(); @@ -72,7 +72,7 @@ it(`serves updated state from memory`, () => { }); it('reads state from disk on get() after refresh()', () => { - const cache = new BundleCache('/foo/bar.json'); + const cache = new BundleCache('/foo'); cache.set(SOME_STATE); cache.refresh(); jest.clearAllMocks(); @@ -83,7 +83,7 @@ it('reads state from disk on get() after refresh()', () => { expect(mockReadFileSync.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/foo/bar.json", + "/foo/.kbn-optimizer-cache", "utf8", ], ] @@ -91,7 +91,7 @@ it('reads state from disk on get() after refresh()', () => { }); it('provides accessors to specific state properties', () => { - const cache = new BundleCache('/foo/bar.json'); + const cache = new BundleCache('/foo'); expect(cache.getModuleCount()).toBe(undefined); expect(cache.getReferencedFiles()).toEqual(undefined); diff --git a/packages/kbn-optimizer/src/common/bundle_cache.ts b/packages/kbn-optimizer/src/common/bundle_cache.ts index 39b52095c819a5..7c0770caa26235 100644 --- a/packages/kbn-optimizer/src/common/bundle_cache.ts +++ b/packages/kbn-optimizer/src/common/bundle_cache.ts @@ -9,6 +9,9 @@ import Fs from 'fs'; import Path from 'path'; +import webpack from 'webpack'; +import { RawSource } from 'webpack-sources'; + export interface State { optimizerCacheKey?: unknown; cacheKey?: unknown; @@ -20,13 +23,17 @@ export interface State { const DEFAULT_STATE: State = {}; const DEFAULT_STATE_JSON = JSON.stringify(DEFAULT_STATE); +const CACHE_FILENAME = '.kbn-optimizer-cache'; /** * Helper to read and update metadata for bundles. */ export class BundleCache { private state: State | undefined = undefined; - constructor(private readonly path: string | false) {} + private readonly path: string | false; + constructor(outputDir: string | false) { + this.path = outputDir === false ? false : Path.resolve(outputDir, CACHE_FILENAME); + } refresh() { this.state = undefined; @@ -63,6 +70,7 @@ export class BundleCache { set(updated: State) { this.state = updated; + if (this.path) { const directory = Path.dirname(this.path); Fs.mkdirSync(directory, { recursive: true }); @@ -107,4 +115,16 @@ export class BundleCache { } } } + + public writeWebpackAsset(compilation: webpack.compilation.Compilation) { + if (!this.path) { + return; + } + + const source = new RawSource(JSON.stringify(this.state, null, 2)); + + // see https://github.com/jantimon/html-webpack-plugin/blob/33d69f49e6e9787796402715d1b9cd59f80b628f/index.js#L266 + // @ts-expect-error undocumented, used to add assets to the output + compilation.emitAsset(CACHE_FILENAME, source); + } } diff --git a/packages/kbn-optimizer/src/index.ts b/packages/kbn-optimizer/src/index.ts index a74679bfff5363..551d2ffacfcfbf 100644 --- a/packages/kbn-optimizer/src/index.ts +++ b/packages/kbn-optimizer/src/index.ts @@ -9,6 +9,5 @@ export { OptimizerConfig } from './optimizer'; export * from './run_optimizer'; export * from './log_optimizer_state'; -export * from './report_optimizer_stats'; export * from './node'; export * from './limits'; diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 1ed1b92f9c2d90..9e9e8960da21bb 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -13,6 +13,7 @@ OptimizerConfig { "id": "bar", "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/kibana.json, "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/target/public, + "pageLoadAssetSizeLimit": undefined, "publicDirNames": Array [ "public", ], @@ -29,6 +30,7 @@ OptimizerConfig { "id": "foo", "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/kibana.json, "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/target/public, + "pageLoadAssetSizeLimit": undefined, "publicDirNames": Array [ "public", ], @@ -47,6 +49,7 @@ OptimizerConfig { "id": "baz", "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/kibana.json, "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/target/public, + "pageLoadAssetSizeLimit": undefined, "publicDirNames": Array [ "public", ], @@ -57,7 +60,6 @@ OptimizerConfig { "cache": true, "dist": false, "inspectWorkers": false, - "limits": "", "maxWorkerCount": 1, "plugins": Array [ Object { @@ -109,3 +111,34 @@ exports[`prepares assets for distribution: baz bundle 1`] = ` exports[`prepares assets for distribution: foo async bundle 1`] = `"(window[\\"foo_bundle_jsonpfunction\\"]=window[\\"foo_bundle_jsonpfunction\\"]||[]).push([[1],{3:function(module,__webpack_exports__,__webpack_require__){\\"use strict\\";__webpack_require__.r(__webpack_exports__);__webpack_require__.d(__webpack_exports__,\\"foo\\",(function(){return foo}));function foo(){}}}]);"`; exports[`prepares assets for distribution: foo bundle 1`] = `"(function(modules){function webpackJsonpCallback(data){var chunkIds=data[0];var moreModules=data[1];var moduleId,chunkId,i=0,resolves=[];for(;i { dist: false, }); - expect(config.limits).toEqual(readLimits()); - (config as any).limits = ''; - expect(config).toMatchSnapshot('OptimizerConfig'); const msgs = await allValuesFrom( @@ -235,6 +226,10 @@ it('prepares assets for distribution', async () => { await allValuesFrom(runOptimizer(config).pipe(logOptimizerState(log, config))); + expect( + Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, 'plugins/foo/target/public/metrics.json'), 'utf8') + ).toMatchSnapshot('metrics.json'); + expectFileMatchesSnapshotWithCompression('plugins/foo/target/public/foo.plugin.js', 'foo bundle'); expectFileMatchesSnapshotWithCompression( 'plugins/foo/target/public/foo.chunk.1.js', diff --git a/packages/kbn-optimizer/src/limits.ts b/packages/kbn-optimizer/src/limits.ts index fcfd36664c1f40..292314a4608e40 100644 --- a/packages/kbn-optimizer/src/limits.ts +++ b/packages/kbn-optimizer/src/limits.ts @@ -7,12 +7,13 @@ */ import Fs from 'fs'; +import Path from 'path'; import dedent from 'dedent'; import Yaml from 'js-yaml'; -import { createFailError, ToolingLog } from '@kbn/dev-utils'; +import { createFailError, ToolingLog, CiStatsMetrics } from '@kbn/dev-utils'; -import { OptimizerConfig, getMetrics, Limits } from './optimizer'; +import { OptimizerConfig, Limits } from './optimizer'; const LIMITS_PATH = require.resolve('../limits.yml'); const DEFAULT_BUDGET = 15000; @@ -33,7 +34,7 @@ export function readLimits(): Limits { } export function validateLimitsForAllBundles(log: ToolingLog, config: OptimizerConfig) { - const limitBundleIds = Object.keys(config.limits.pageLoadAssetSize || {}); + const limitBundleIds = Object.keys(readLimits().pageLoadAssetSize || {}); const configBundleIds = config.bundles.map((b) => b.id); const missingBundleIds = diff(configBundleIds, limitBundleIds); @@ -75,15 +76,21 @@ interface UpdateBundleLimitsOptions { } export function updateBundleLimits({ log, config, dropMissing }: UpdateBundleLimitsOptions) { - const metrics = getMetrics(log, config); + const limits = readLimits(); + const metrics: CiStatsMetrics = config.bundles + .map((bundle) => + JSON.parse(Fs.readFileSync(Path.resolve(bundle.outputDir, 'metrics.json'), 'utf-8')) + ) + .flat() + .sort((a, b) => a.id.localeCompare(b.id)); const pageLoadAssetSize: NonNullable = dropMissing ? {} - : config.limits.pageLoadAssetSize ?? {}; + : limits.pageLoadAssetSize ?? {}; - for (const metric of metrics.sort((a, b) => a.id.localeCompare(b.id))) { + for (const metric of metrics) { if (metric.group === 'page load bundle size') { - const existingLimit = config.limits.pageLoadAssetSize?.[metric.id]; + const existingLimit = limits.pageLoadAssetSize?.[metric.id]; pageLoadAssetSize[metric.id] = existingLimit != null && existingLimit >= metric.value ? existingLimit diff --git a/packages/kbn-optimizer/src/optimizer/get_output_stats.ts b/packages/kbn-optimizer/src/optimizer/get_output_stats.ts deleted file mode 100644 index e7059c4d6799cf..00000000000000 --- a/packages/kbn-optimizer/src/optimizer/get_output_stats.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import Fs from 'fs'; -import Path from 'path'; - -import { ToolingLog, CiStatsMetrics } from '@kbn/dev-utils'; -import { OptimizerConfig } from './optimizer_config'; - -const flatten = (arr: Array): T[] => - arr.reduce((acc: T[], item) => acc.concat(item), []); - -interface Entry { - relPath: string; - stats: Fs.Stats; -} - -const IGNORED_EXTNAME = ['.map', '.br', '.gz']; - -const getFiles = (dir: string, parent?: string) => - flatten( - Fs.readdirSync(dir).map((name): Entry | Entry[] => { - const absPath = Path.join(dir, name); - const relPath = parent ? Path.join(parent, name) : name; - const stats = Fs.statSync(absPath); - - if (stats.isDirectory()) { - return getFiles(absPath, relPath); - } - - return { - relPath, - stats, - }; - }) - ).filter((file) => { - const filename = Path.basename(file.relPath); - if (filename.startsWith('.')) { - return false; - } - - const ext = Path.extname(filename); - if (IGNORED_EXTNAME.includes(ext)) { - return false; - } - - return true; - }); - -export function getMetrics(log: ToolingLog, config: OptimizerConfig) { - return flatten( - config.bundles.map((bundle) => { - // make the cache read from the cache file since it was likely updated by the worker - bundle.cache.refresh(); - - const outputFiles = getFiles(bundle.outputDir); - const entryName = `${bundle.id}.${bundle.type}.js`; - const entry = outputFiles.find((f) => f.relPath === entryName); - if (!entry) { - throw new Error( - `Unable to find bundle entry named [${entryName}] in [${bundle.outputDir}]` - ); - } - - const chunkPrefix = `${bundle.id}.chunk.`; - const asyncChunks = outputFiles.filter((f) => f.relPath.startsWith(chunkPrefix)); - const miscFiles = outputFiles.filter((f) => f !== entry && !asyncChunks.includes(f)); - - if (asyncChunks.length) { - log.verbose(bundle.id, 'async chunks', asyncChunks); - } - if (miscFiles.length) { - log.verbose(bundle.id, 'misc files', asyncChunks); - } - - const sumSize = (files: Entry[]) => files.reduce((acc: number, f) => acc + f.stats!.size, 0); - - const bundleMetrics: CiStatsMetrics = [ - { - group: `@kbn/optimizer bundle module count`, - id: bundle.id, - value: bundle.cache.getModuleCount() || 0, - }, - { - group: `page load bundle size`, - id: bundle.id, - value: entry.stats!.size, - limit: config.limits.pageLoadAssetSize?.[bundle.id], - limitConfigPath: `packages/kbn-optimizer/limits.yml`, - }, - { - group: `async chunks size`, - id: bundle.id, - value: sumSize(asyncChunks), - }, - { - group: `async chunk count`, - id: bundle.id, - value: asyncChunks.length, - }, - { - group: `miscellaneous assets size`, - id: bundle.id, - value: sumSize(miscFiles), - }, - ]; - - log.debug(bundle.id, 'metrics', bundleMetrics); - - return bundleMetrics; - }) - ); -} diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts index d921d5e5cca313..e4cdddbf56dcb0 100644 --- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts @@ -48,7 +48,12 @@ it('returns a bundle for core and each plugin', () => { }, ], '/repo', - '/output' + '/output', + { + pageLoadAssetSize: { + box: 123, + }, + } ).map((b) => b.toSpec()) ).toMatchInlineSnapshot(` Array [ @@ -58,6 +63,7 @@ it('returns a bundle for core and each plugin', () => { "id": "foo", "manifestPath": /plugins/foo/kibana.json, "outputDir": /plugins/foo/target/public, + "pageLoadAssetSizeLimit": undefined, "publicDirNames": Array [ "public", ], @@ -70,6 +76,7 @@ it('returns a bundle for core and each plugin', () => { "id": "baz", "manifestPath": /plugins/baz/kibana.json, "outputDir": /plugins/baz/target/public, + "pageLoadAssetSizeLimit": undefined, "publicDirNames": Array [ "public", ], @@ -84,6 +91,7 @@ it('returns a bundle for core and each plugin', () => { "id": "box", "manifestPath": /x-pack/plugins/box/kibana.json, "outputDir": /x-pack/plugins/box/target/public, + "pageLoadAssetSizeLimit": 123, "publicDirNames": Array [ "public", ], diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts index 76a0d51edac82d..8134707561bc0e 100644 --- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts @@ -9,13 +9,15 @@ import Path from 'path'; import { Bundle } from '../common'; +import { Limits } from './optimizer_config'; import { KibanaPlatformPlugin } from './kibana_platform_plugins'; export function getPluginBundles( plugins: KibanaPlatformPlugin[], repoRoot: string, - outputRoot: string + outputRoot: string, + limits: Limits ) { const xpackDirSlash = Path.resolve(repoRoot, 'x-pack') + Path.sep; @@ -39,6 +41,7 @@ export function getPluginBundles( ? `/*! Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one or more contributor license agreements. \n` + ` * Licensed under the Elastic License 2.0; you may not use this file except in compliance with the Elastic License 2.0. */\n` : undefined, + pageLoadAssetSizeLimit: limits.pageLoadAssetSize?.[p.id], }) ); } diff --git a/packages/kbn-optimizer/src/optimizer/index.ts b/packages/kbn-optimizer/src/optimizer/index.ts index ced61463d5edd5..28d206488b0a49 100644 --- a/packages/kbn-optimizer/src/optimizer/index.ts +++ b/packages/kbn-optimizer/src/optimizer/index.ts @@ -14,4 +14,3 @@ export * from './watch_bundles_for_changes'; export * from './run_workers'; export * from './bundle_cache'; export * from './handle_optimizer_completion'; -export * from './get_output_stats'; diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts index 5677719628b6aa..c60d6719cdea78 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts @@ -435,7 +435,6 @@ describe('OptimizerConfig::create()', () => { "cache": Symbol(parsed cache), "dist": Symbol(parsed dist), "inspectWorkers": Symbol(parsed inspect workers), - "limits": Symbol(limits), "maxWorkerCount": Symbol(parsed max worker count), "plugins": Symbol(new platform plugins), "profileWebpack": Symbol(parsed profile webpack), @@ -457,7 +456,7 @@ describe('OptimizerConfig::create()', () => { [Window], ], "invocationCallOrder": Array [ - 21, + 22, ], "results": Array [ Object { @@ -480,7 +479,7 @@ describe('OptimizerConfig::create()', () => { [Window], ], "invocationCallOrder": Array [ - 24, + 25, ], "results": Array [ Object { @@ -498,13 +497,14 @@ describe('OptimizerConfig::create()', () => { Symbol(new platform plugins), Symbol(parsed repo root), Symbol(parsed output root), + Symbol(limits), ], ], "instances": Array [ [Window], ], "invocationCallOrder": Array [ - 22, + 23, ], "results": Array [ Object { diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index b93d7a753c9acd..ed521d32a0a297 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -211,6 +211,7 @@ export class OptimizerConfig { } static create(inputOptions: Options) { + const limits = readLimits(); const options = OptimizerConfig.parseOptions(inputOptions); const plugins = findKibanaPlatformPlugins(options.pluginScanDirs, options.pluginPaths); const bundles = [ @@ -223,10 +224,11 @@ export class OptimizerConfig { sourceRoot: options.repoRoot, contextDir: Path.resolve(options.repoRoot, 'src/core'), outputDir: Path.resolve(options.outputRoot, 'src/core/target/public'), + pageLoadAssetSizeLimit: limits.pageLoadAssetSize?.core, }), ] : []), - ...getPluginBundles(plugins, options.repoRoot, options.outputRoot), + ...getPluginBundles(plugins, options.repoRoot, options.outputRoot, limits), ]; return new OptimizerConfig( @@ -239,8 +241,7 @@ export class OptimizerConfig { options.maxWorkerCount, options.dist, options.profileWebpack, - options.themeTags, - readLimits() + options.themeTags ); } @@ -254,8 +255,7 @@ export class OptimizerConfig { public readonly maxWorkerCount: number, public readonly dist: boolean, public readonly profileWebpack: boolean, - public readonly themeTags: ThemeTags, - public readonly limits: Limits + public readonly themeTags: ThemeTags ) {} getWorkerConfig(optimizerCacheKey: unknown): WorkerConfig { diff --git a/packages/kbn-optimizer/src/report_optimizer_stats.ts b/packages/kbn-optimizer/src/report_optimizer_stats.ts deleted file mode 100644 index eeed2fb1b156c8..00000000000000 --- a/packages/kbn-optimizer/src/report_optimizer_stats.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { materialize, mergeMap, dematerialize } from 'rxjs/operators'; -import { CiStatsReporter, ToolingLog } from '@kbn/dev-utils'; - -import { OptimizerUpdate$ } from './run_optimizer'; -import { OptimizerConfig, getMetrics } from './optimizer'; -import { pipeClosure } from './common'; - -export function reportOptimizerStats( - reporter: CiStatsReporter, - config: OptimizerConfig, - log: ToolingLog -) { - return pipeClosure((update$: OptimizerUpdate$) => - update$.pipe( - materialize(), - mergeMap(async (n) => { - if (n.kind === 'C') { - const metrics = getMetrics(log, config); - - await reporter.metrics(metrics); - - for (const metric of metrics) { - if (metric.limit != null && metric.value > metric.limit) { - const value = metric.value.toLocaleString(); - const limit = metric.limit.toLocaleString(); - log.warning( - `Metric [${metric.group}] for [${metric.id}] of [${value}] over the limit of [${limit}]` - ); - } - } - } - - return n; - }), - dematerialize() - ) - ); -} diff --git a/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts b/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts new file mode 100644 index 00000000000000..909a97a3e11c78 --- /dev/null +++ b/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; + +import webpack from 'webpack'; +import { RawSource } from 'webpack-sources'; +import { CiStatsMetrics } from '@kbn/dev-utils'; + +import { Bundle } from '../common'; + +interface Asset { + name: string; + size: number; +} + +const IGNORED_EXTNAME = ['.map', '.br', '.gz']; + +export class BundleMetricsPlugin { + constructor(private readonly bundle: Bundle) {} + + public apply(compiler: webpack.Compiler) { + const { bundle } = this; + + compiler.hooks.emit.tap('BundleMetricsPlugin', (compilation) => { + const assets = Object.entries(compilation.assets) + .map( + ([name, source]: [string, any]): Asset => ({ + name, + size: source.size(), + }) + ) + .filter((asset) => { + const filename = Path.basename(asset.name); + if (filename.startsWith('.')) { + return false; + } + + const ext = Path.extname(filename); + if (IGNORED_EXTNAME.includes(ext)) { + return false; + } + + return true; + }); + + const entryName = `${bundle.id}.${bundle.type}.js`; + const entry = assets.find((a) => a.name === entryName); + if (!entry) { + throw new Error( + `Unable to find bundle entry named [${entryName}] in [${bundle.outputDir}]` + ); + } + + const chunkPrefix = `${bundle.id}.chunk.`; + const asyncChunks = assets.filter((a) => a.name.startsWith(chunkPrefix)); + const miscFiles = assets.filter((a) => a !== entry && !asyncChunks.includes(a)); + + const sumSize = (files: Asset[]) => files.reduce((acc: number, a) => acc + a.size, 0); + + const moduleCount = bundle.cache.getModuleCount(); + if (moduleCount === undefined) { + throw new Error(`moduleCount wasn't populated by PopulateBundleCachePlugin`); + } + + const bundleMetrics: CiStatsMetrics = [ + { + group: `@kbn/optimizer bundle module count`, + id: bundle.id, + value: moduleCount, + }, + { + group: `page load bundle size`, + id: bundle.id, + value: entry.size, + limit: bundle.pageLoadAssetSizeLimit, + limitConfigPath: `packages/kbn-optimizer/limits.yml`, + }, + { + group: `async chunks size`, + id: bundle.id, + value: sumSize(asyncChunks), + }, + { + group: `async chunk count`, + id: bundle.id, + value: asyncChunks.length, + }, + { + group: `miscellaneous assets size`, + id: bundle.id, + value: sumSize(miscFiles), + }, + ]; + + const metricsSource = new RawSource(JSON.stringify(bundleMetrics, null, 2)); + + // see https://github.com/jantimon/html-webpack-plugin/blob/33d69f49e6e9787796402715d1b9cd59f80b628f/index.js#L266 + // @ts-expect-error undocumented, used to add assets to the output + compilation.emitAsset('metrics.json', metricsSource); + }); + } +} diff --git a/packages/kbn-optimizer/src/worker/emit_stats_plugin.ts b/packages/kbn-optimizer/src/worker/emit_stats_plugin.ts new file mode 100644 index 00000000000000..c964219e1fed61 --- /dev/null +++ b/packages/kbn-optimizer/src/worker/emit_stats_plugin.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Fs from 'fs'; +import Path from 'path'; + +import webpack from 'webpack'; + +import { Bundle } from '../common'; + +export class EmitStatsPlugin { + constructor(private readonly bundle: Bundle) {} + + public apply(compiler: webpack.Compiler) { + compiler.hooks.done.tap( + { + name: 'EmitStatsPlugin', + // run at the very end, ensure that it's after clean-webpack-plugin + stage: 10, + }, + (stats) => { + Fs.writeFileSync( + Path.resolve(this.bundle.outputDir, 'stats.json'), + JSON.stringify(stats.toJson()) + ); + } + ); + } +} diff --git a/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts b/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts new file mode 100644 index 00000000000000..6d296b9be089c0 --- /dev/null +++ b/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import webpack from 'webpack'; + +import Path from 'path'; +import { inspect } from 'util'; + +import { Bundle, WorkerConfig, ascending, parseFilePath } from '../common'; +import { BundleRefModule } from './bundle_ref_module'; +import { + isExternalModule, + isNormalModule, + isIgnoredModule, + isConcatenatedModule, + getModulePath, +} from './webpack_helpers'; + +/** + * sass-loader creates about a 40% overhead on the overall optimizer runtime, and + * so this constant is used to indicate to assignBundlesToWorkers() that there is + * extra work done in a bundle that has a lot of scss imports. The value is + * arbitrary and just intended to weigh the bundles so that they are distributed + * across mulitple workers on machines with lots of cores. + */ +const EXTRA_SCSS_WORK_UNITS = 100; + +export class PopulateBundleCachePlugin { + constructor(private readonly workerConfig: WorkerConfig, private readonly bundle: Bundle) {} + + public apply(compiler: webpack.Compiler) { + const { bundle, workerConfig } = this; + + compiler.hooks.emit.tap( + { + name: 'PopulateBundleCachePlugin', + before: ['BundleMetricsPlugin'], + }, + (compilation) => { + const bundleRefExportIds: string[] = []; + const referencedFiles = new Set(); + let moduleCount = 0; + let workUnits = compilation.fileDependencies.size; + + if (bundle.manifestPath) { + referencedFiles.add(bundle.manifestPath); + } + + for (const module of compilation.modules) { + if (isNormalModule(module)) { + moduleCount += 1; + const path = getModulePath(module); + const parsedPath = parseFilePath(path); + + if (!parsedPath.dirs.includes('node_modules')) { + referencedFiles.add(path); + + if (path.endsWith('.scss')) { + workUnits += EXTRA_SCSS_WORK_UNITS; + + for (const depPath of module.buildInfo.fileDependencies) { + referencedFiles.add(depPath); + } + } + + continue; + } + + const nmIndex = parsedPath.dirs.lastIndexOf('node_modules'); + const isScoped = parsedPath.dirs[nmIndex + 1].startsWith('@'); + referencedFiles.add( + Path.join( + parsedPath.root, + ...parsedPath.dirs.slice(0, nmIndex + 1 + (isScoped ? 2 : 1)), + 'package.json' + ) + ); + continue; + } + + if (module instanceof BundleRefModule) { + bundleRefExportIds.push(module.ref.exportId); + continue; + } + + if (isConcatenatedModule(module)) { + moduleCount += module.modules.length; + continue; + } + + if (isExternalModule(module) || isIgnoredModule(module)) { + continue; + } + + throw new Error(`Unexpected module type: ${inspect(module)}`); + } + + const files = Array.from(referencedFiles).sort(ascending((p) => p)); + const mtimes = new Map( + files.map((path): [string, number | undefined] => { + try { + return [path, compiler.inputFileSystem.statSync(path)?.mtimeMs]; + } catch (error) { + if (error?.code === 'ENOENT') { + return [path, undefined]; + } + + throw error; + } + }) + ); + + bundle.cache.set({ + bundleRefExportIds: bundleRefExportIds.sort(ascending((p) => p)), + optimizerCacheKey: workerConfig.optimizerCacheKey, + cacheKey: bundle.createCacheKey(files, mtimes), + moduleCount, + workUnits, + files, + }); + + // write the cache to the compilation so that it isn't cleaned by clean-webpack-plugin + bundle.cache.writeWebpackAsset(compilation); + } + ); + } +} diff --git a/packages/kbn-optimizer/src/worker/run_compilers.ts b/packages/kbn-optimizer/src/worker/run_compilers.ts index 61f9c243a4def2..4f5bb23c3550d2 100644 --- a/packages/kbn-optimizer/src/worker/run_compilers.ts +++ b/packages/kbn-optimizer/src/worker/run_compilers.ts @@ -8,46 +8,16 @@ import 'source-map-support/register'; -import Fs from 'fs'; -import Path from 'path'; -import { inspect } from 'util'; - import webpack, { Stats } from 'webpack'; import * as Rx from 'rxjs'; import { mergeMap, map, mapTo, takeUntil } from 'rxjs/operators'; -import { - CompilerMsgs, - CompilerMsg, - maybeMap, - Bundle, - WorkerConfig, - ascending, - parseFilePath, - BundleRefs, -} from '../common'; -import { BundleRefModule } from './bundle_ref_module'; +import { CompilerMsgs, CompilerMsg, maybeMap, Bundle, WorkerConfig, BundleRefs } from '../common'; import { getWebpackConfig } from './webpack.config'; import { isFailureStats, failedStatsToErrorMessage } from './webpack_helpers'; -import { - isExternalModule, - isNormalModule, - isIgnoredModule, - isConcatenatedModule, - getModulePath, -} from './webpack_helpers'; const PLUGIN_NAME = '@kbn/optimizer'; -/** - * sass-loader creates about a 40% overhead on the overall optimizer runtime, and - * so this constant is used to indicate to assignBundlesToWorkers() that there is - * extra work done in a bundle that has a lot of scss imports. The value is - * arbitrary and just intended to weigh the bundles so that they are distributed - * across mulitple workers on machines with lots of cores. - */ -const EXTRA_SCSS_WORK_UNITS = 100; - /** * Create an Observable for a specific child compiler + bundle */ @@ -80,13 +50,6 @@ const observeCompiler = ( return undefined; } - if (workerConfig.profileWebpack) { - Fs.writeFileSync( - Path.resolve(bundle.outputDir, 'stats.json'), - JSON.stringify(stats.toJson()) - ); - } - if (!workerConfig.watch) { process.nextTick(() => done$.next()); } @@ -97,88 +60,11 @@ const observeCompiler = ( }); } - const bundleRefExportIds: string[] = []; - const referencedFiles = new Set(); - let moduleCount = 0; - let workUnits = stats.compilation.fileDependencies.size; - - if (bundle.manifestPath) { - referencedFiles.add(bundle.manifestPath); - } - - for (const module of stats.compilation.modules) { - if (isNormalModule(module)) { - moduleCount += 1; - const path = getModulePath(module); - const parsedPath = parseFilePath(path); - - if (!parsedPath.dirs.includes('node_modules')) { - referencedFiles.add(path); - - if (path.endsWith('.scss')) { - workUnits += EXTRA_SCSS_WORK_UNITS; - - for (const depPath of module.buildInfo.fileDependencies) { - referencedFiles.add(depPath); - } - } - - continue; - } - - const nmIndex = parsedPath.dirs.lastIndexOf('node_modules'); - const isScoped = parsedPath.dirs[nmIndex + 1].startsWith('@'); - referencedFiles.add( - Path.join( - parsedPath.root, - ...parsedPath.dirs.slice(0, nmIndex + 1 + (isScoped ? 2 : 1)), - 'package.json' - ) - ); - continue; - } - - if (module instanceof BundleRefModule) { - bundleRefExportIds.push(module.ref.exportId); - continue; - } - - if (isConcatenatedModule(module)) { - moduleCount += module.modules.length; - continue; - } - - if (isExternalModule(module) || isIgnoredModule(module)) { - continue; - } - - throw new Error(`Unexpected module type: ${inspect(module)}`); + const moduleCount = bundle.cache.getModuleCount(); + if (moduleCount === undefined) { + throw new Error(`moduleCount wasn't populated by PopulateBundleCachePlugin`); } - const files = Array.from(referencedFiles).sort(ascending((p) => p)); - const mtimes = new Map( - files.map((path): [string, number | undefined] => { - try { - return [path, compiler.inputFileSystem.statSync(path)?.mtimeMs]; - } catch (error) { - if (error?.code === 'ENOENT') { - return [path, undefined]; - } - - throw error; - } - }) - ); - - bundle.cache.set({ - bundleRefExportIds: bundleRefExportIds.sort(ascending((p) => p)), - optimizerCacheKey: workerConfig.optimizerCacheKey, - cacheKey: bundle.createCacheKey(files, mtimes), - moduleCount, - workUnits, - files, - }); - return compilerMsgs.compilerSuccess({ moduleCount, }); diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 331fbde6ea0bac..c4beb959284cc7 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -19,6 +19,9 @@ import * as UiSharedDeps from '@kbn/ui-shared-deps'; import { Bundle, BundleRefs, WorkerConfig } from '../common'; import { BundleRefsPlugin } from './bundle_refs_plugin'; +import { BundleMetricsPlugin } from './bundle_metrics_plugin'; +import { EmitStatsPlugin } from './emit_stats_plugin'; +import { PopulateBundleCachePlugin } from './populate_bundle_cache_plugin'; const IS_CODE_COVERAGE = !!process.env.CODE_COVERAGE; const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset'); @@ -67,6 +70,9 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: plugins: [ new CleanWebpackPlugin(), new BundleRefsPlugin(bundle, bundleRefs), + new PopulateBundleCachePlugin(worker, bundle), + new BundleMetricsPlugin(bundle), + ...(worker.profileWebpack ? [new EmitStatsPlugin(bundle)] : []), ...(bundle.banner ? [new webpack.BannerPlugin({ banner: bundle.banner, raw: true })] : []), ], diff --git a/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts b/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts index 559d9da35c3206..9723c0107cf8e4 100644 --- a/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts +++ b/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts @@ -74,13 +74,14 @@ it('builds a generated plugin into a viable archive', async () => { await extract(PLUGIN_ARCHIVE, { dir: TMP_DIR }); - const files = await globby(['**/*'], { cwd: TMP_DIR }); + const files = await globby(['**/*'], { cwd: TMP_DIR, dot: true }); files.sort((a, b) => a.localeCompare(b)); expect(files).toMatchInlineSnapshot(` Array [ "kibana/fooTestPlugin/common/index.js", "kibana/fooTestPlugin/kibana.json", + "kibana/fooTestPlugin/node_modules/.yarn-integrity", "kibana/fooTestPlugin/package.json", "kibana/fooTestPlugin/server/index.js", "kibana/fooTestPlugin/server/plugin.js", diff --git a/packages/kbn-plugin-helpers/src/tasks/optimize.ts b/packages/kbn-plugin-helpers/src/tasks/optimize.ts index 0f0ac93086c9e7..2478947e79f188 100644 --- a/packages/kbn-plugin-helpers/src/tasks/optimize.ts +++ b/packages/kbn-plugin-helpers/src/tasks/optimize.ts @@ -34,9 +34,15 @@ export async function optimize({ log, plugin, sourceDir, buildDir }: BuildContex pluginScanDirs: [], }); + const target = Path.resolve(sourceDir, 'target'); + await runOptimizer(config).pipe(logOptimizerState(log, config)).toPromise(); + // clean up unnecessary files + Fs.unlinkSync(Path.resolve(target, 'public/metrics.json')); + Fs.unlinkSync(Path.resolve(target, 'public/.kbn-optimizer-cache')); + // move target into buildDir - await asyncRename(Path.resolve(sourceDir, 'target'), Path.resolve(buildDir, 'target')); + await asyncRename(target, Path.resolve(buildDir, 'target')); log.indent(-2); } diff --git a/scripts/ship_ci_stats.js b/scripts/ship_ci_stats.js new file mode 100644 index 00000000000000..5aed9fc446240d --- /dev/null +++ b/scripts/ship_ci_stats.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +require('../src/setup_node_env/no_transpilation'); +require('@kbn/dev-utils').shipCiStatsCli(); diff --git a/src/dev/build/tasks/build_kibana_platform_plugins.ts b/src/dev/build/tasks/build_kibana_platform_plugins.ts index 91fad2ca52617f..d2d2d3275270b8 100644 --- a/src/dev/build/tasks/build_kibana_platform_plugins.ts +++ b/src/dev/build/tasks/build_kibana_platform_plugins.ts @@ -6,20 +6,18 @@ * Side Public License, v 1. */ +import Path from 'path'; + import { REPO_ROOT } from '@kbn/utils'; -import { CiStatsReporter } from '@kbn/dev-utils'; -import { - runOptimizer, - OptimizerConfig, - logOptimizerState, - reportOptimizerStats, -} from '@kbn/optimizer'; +import { lastValueFrom } from '@kbn/std'; +import { CiStatsMetrics } from '@kbn/dev-utils'; +import { runOptimizer, OptimizerConfig, logOptimizerState } from '@kbn/optimizer'; -import { Task } from '../lib'; +import { Task, deleteAll, write, read } from '../lib'; export const BuildKibanaPlatformPlugins: Task = { description: 'Building distributable versions of Kibana platform plugins', - async run(_, log, build) { + async run(buildConfig, log, build) { const config = OptimizerConfig.create({ repoRoot: REPO_ROOT, outputRoot: build.resolvePath(), @@ -31,12 +29,27 @@ export const BuildKibanaPlatformPlugins: Task = { includeCoreBundle: true, }); - const reporter = CiStatsReporter.fromEnv(log); + await lastValueFrom(runOptimizer(config).pipe(logOptimizerState(log, config))); + + const combinedMetrics: CiStatsMetrics = []; + const metricFilePaths: string[] = []; + for (const bundle of config.bundles) { + const path = Path.resolve(bundle.outputDir, 'metrics.json'); + const metrics: CiStatsMetrics = JSON.parse(await read(path)); + combinedMetrics.push(...metrics); + metricFilePaths.push(path); + } + + // write combined metrics to target + await write( + buildConfig.resolveFromTarget('optimizer_bundle_metrics.json'), + JSON.stringify(combinedMetrics, null, 2) + ); - await runOptimizer(config) - .pipe(reportOptimizerStats(reporter, config, log), logOptimizerState(log, config)) - .toPromise(); + // delete all metric files + await deleteAll(metricFilePaths, log); + // delete all bundle cache files await Promise.all(config.bundles.map((b) => b.cache.clear())); }, }; diff --git a/test/scripts/jenkins_baseline.sh b/test/scripts/jenkins_baseline.sh index e679ac7f31bd15..60926238576c77 100755 --- a/test/scripts/jenkins_baseline.sh +++ b/test/scripts/jenkins_baseline.sh @@ -5,6 +5,10 @@ source "$KIBANA_DIR/src/dev/ci_setup/setup_percy.sh" echo " -> building and extracting OSS Kibana distributable for use in functional tests" node scripts/build --debug --oss + +echo " -> shipping metrics from build to ci-stats" +node scripts/ship_ci_stats --metrics target/optimizer_bundle_metrics.json + linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" installDir="$PARENT_DIR/install/kibana" mkdir -p "$installDir" diff --git a/test/scripts/jenkins_build_kibana.sh b/test/scripts/jenkins_build_kibana.sh index 6184708ea3fc62..5819a3ce6765e1 100755 --- a/test/scripts/jenkins_build_kibana.sh +++ b/test/scripts/jenkins_build_kibana.sh @@ -17,6 +17,9 @@ if [[ -z "$CODE_COVERAGE" ]] ; then echo " -> building and extracting OSS Kibana distributable for use in functional tests" node scripts/build --debug --oss + echo " -> shipping metrics from build to ci-stats" + node scripts/ship_ci_stats --metrics target/optimizer_bundle_metrics.json + mkdir -p "$WORKSPACE/kibana-build-oss" cp -pR build/oss/kibana-*-SNAPSHOT-linux-x86_64/. $WORKSPACE/kibana-build-oss/ fi diff --git a/test/scripts/jenkins_xpack_baseline.sh b/test/scripts/jenkins_xpack_baseline.sh index 7577b6927d166f..aaacdd4ea3aaec 100755 --- a/test/scripts/jenkins_xpack_baseline.sh +++ b/test/scripts/jenkins_xpack_baseline.sh @@ -6,6 +6,10 @@ source "$KIBANA_DIR/src/dev/ci_setup/setup_percy.sh" echo " -> building and extracting default Kibana distributable" cd "$KIBANA_DIR" node scripts/build --debug --no-oss + +echo " -> shipping metrics from build to ci-stats" +node scripts/ship_ci_stats --metrics target/optimizer_bundle_metrics.json + linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" installDir="$KIBANA_DIR/install/kibana" mkdir -p "$installDir" diff --git a/test/scripts/jenkins_xpack_build_kibana.sh b/test/scripts/jenkins_xpack_build_kibana.sh index a9e603f63bd42b..36865ce7c4967a 100755 --- a/test/scripts/jenkins_xpack_build_kibana.sh +++ b/test/scripts/jenkins_xpack_build_kibana.sh @@ -32,6 +32,10 @@ if [[ -z "$CODE_COVERAGE" ]] ; then echo " -> building and extracting default Kibana distributable for use in functional tests" cd "$KIBANA_DIR" node scripts/build --debug --no-oss + + echo " -> shipping metrics from build to ci-stats" + node scripts/ship_ci_stats --metrics target/optimizer_bundle_metrics.json + linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" installDir="$KIBANA_DIR/install/kibana" mkdir -p "$installDir" diff --git a/yarn.lock b/yarn.lock index 6df258e9715b78..ec6cf338a43da2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6803,7 +6803,7 @@ dependencies: "@types/webpack" "*" -"@types/webpack-sources@*": +"@types/webpack-sources@*", "@types/webpack-sources@^0.1.4": version "0.1.5" resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.5.tgz#be47c10f783d3d6efe1471ff7f042611bd464a92" integrity sha512-zfvjpp7jiafSmrzJ2/i3LqOyTYTuJ7u1KOXlKgDlvsj9Rr0x7ZiYu5lZbXwobL7lmsRNtPXlBfmaUD8eU2Hu8w== From 9684661da4e674f77a00bd59b9e5aa3897d418eb Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Mon, 8 Feb 2021 13:28:18 -0600 Subject: [PATCH 37/51] [Metrics UI] Add ability to filter anomaly detection datafeed (#89721) * Add null check for empty process data * Add Ability to filter datafeed for ml jobs * Merge user-defined query with default query Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/containers/ml/infra_ml_module.tsx | 9 ++- .../containers/ml/infra_ml_module_types.ts | 3 +- .../metrics_hosts/module_descriptor.ts | 18 ++++- .../modules/metrics_k8s/module_descriptor.ts | 18 ++++- .../ml/anomaly_detection/job_setup_screen.tsx | 65 +++++++++++++++++-- 5 files changed, 98 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx b/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx index a94f2dd57c482f..b55ae65e58e91c 100644 --- a/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx +++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx @@ -6,7 +6,6 @@ */ import { useCallback, useMemo } from 'react'; -import { DatasetFilter } from '../../../common/infra_ml'; import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; import { useTrackedPromise } from '../../utils/use_tracked_promise'; import { useModuleStatus } from './infra_ml_module_status'; @@ -52,7 +51,7 @@ export const useInfraMLModule = ({ selectedIndices: string[], start: number | undefined, end: number | undefined, - datasetFilter: DatasetFilter, + filter: string, partitionField?: string ) => { dispatchModuleStatus({ type: 'startedSetup' }); @@ -60,7 +59,7 @@ export const useInfraMLModule = ({ { start, end, - datasetFilter, + filter, moduleSourceConfiguration: { indices: selectedIndices, sourceId, @@ -114,13 +113,13 @@ export const useInfraMLModule = ({ selectedIndices: string[], start: number | undefined, end: number | undefined, - datasetFilter: DatasetFilter, + filter: string, partitionField?: string ) => { dispatchModuleStatus({ type: 'startedSetup' }); cleanUpModule() .then(() => { - setUpModule(selectedIndices, start, end, datasetFilter, partitionField); + setUpModule(selectedIndices, start, end, filter, partitionField); }) .catch(() => { dispatchModuleStatus({ type: 'failedSetup' }); diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts index e681290570b8cc..5a5272f7830530 100644 --- a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts +++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts @@ -10,7 +10,6 @@ import { ValidateLogEntryDatasetsResponsePayload, ValidationIndicesResponsePayload, } from '../../../common/http_api/log_analysis'; -import { DatasetFilter } from '../../../common/infra_ml'; import { DeleteJobsResponsePayload } from './api/ml_cleanup'; import { FetchJobStatusResponsePayload } from './api/ml_get_jobs_summary_api'; import { GetMlModuleResponsePayload } from './api/ml_get_module'; @@ -21,7 +20,7 @@ export { JobModelSizeStats, JobSummary } from './api/ml_get_jobs_summary_api'; export interface SetUpModuleArgs { start?: number | undefined; end?: number | undefined; - datasetFilter?: DatasetFilter; + filter?: any; moduleSourceConfiguration: ModuleSourceConfiguration; partitionField?: string; } diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts index b8d09fdb5e3250..a7ab948d052aab 100644 --- a/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts +++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts @@ -67,6 +67,7 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) const { start, end, + filter, moduleSourceConfiguration: { spaceId, sourceId, indices, timestampField }, partitionField, } = setUpModuleArgs; @@ -107,10 +108,23 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) const datafeedOverrides = jobIds.map((id) => { const { datafeed: defaultDatafeedConfig } = getDefaultJobConfigs(id); + const config = { ...defaultDatafeedConfig }; + + if (filter) { + const query = JSON.parse(filter); + + config.query.bool = { + ...config.query.bool, + ...query.bool, + }; + } if (!partitionField || id === 'hosts_memory_usage') { // Since the host memory usage doesn't have custom aggs, we don't need to do anything to add a partition field - return defaultDatafeedConfig; + return { + ...config, + job_id: id, + }; } // If we have a partition field, we need to change the aggregation to do a terms agg at the top level @@ -126,7 +140,7 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) }; return { - ...defaultDatafeedConfig, + ...config, job_id: id, aggregations, }; diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts index fe92b290dfde3c..4c5eb5fd4bf239 100644 --- a/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts +++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts @@ -68,6 +68,7 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) const { start, end, + filter, moduleSourceConfiguration: { spaceId, sourceId, indices, timestampField }, partitionField, } = setUpModuleArgs; @@ -107,10 +108,23 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) const datafeedOverrides = jobIds.map((id) => { const { datafeed: defaultDatafeedConfig } = getDefaultJobConfigs(id); + const config = { ...defaultDatafeedConfig }; + + if (filter) { + const query = JSON.parse(filter); + + config.query.bool = { + ...config.query.bool, + ...query.bool, + }; + } if (!partitionField || id === 'k8s_memory_usage') { // Since the host memory usage doesn't have custom aggs, we don't need to do anything to add a partition field - return defaultDatafeedConfig; + return { + ...config, + job_id: id, + }; } // Because the ML K8s jobs ship with a default partition field of {kubernetes.namespace}, ignore that agg and wrap it in our own agg. @@ -131,7 +145,7 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) }; return { - ...defaultDatafeedConfig, + ...config, job_id: id, aggregations, }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx index 3236cbc59a07b1..894f76318bcfe3 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { debounce } from 'lodash'; import React, { useState, useCallback, useMemo, useEffect } from 'react'; import { EuiForm, EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; import { EuiText, EuiSpacer } from '@elastic/eui'; @@ -22,6 +22,8 @@ import { useMetricK8sModuleContext } from '../../../../../../containers/ml/modul import { useMetricHostsModuleContext } from '../../../../../../containers/ml/modules/metrics_hosts/module'; import { FixedDatePicker } from '../../../../../../components/fixed_datepicker'; import { DEFAULT_K8S_PARTITION_FIELD } from '../../../../../../containers/ml/modules/metrics_k8s/module_descriptor'; +import { MetricsExplorerKueryBar } from '../../../../metrics_explorer/components/kuery_bar'; +import { convertKueryToElasticSearchQuery } from '../../../../../../utils/kuery'; interface Props { jobType: 'hosts' | 'kubernetes'; @@ -36,6 +38,8 @@ export const JobSetupScreen = (props: Props) => { const [partitionField, setPartitionField] = useState(null); const h = useMetricHostsModuleContext(); const k = useMetricK8sModuleContext(); + const [filter, setFilter] = useState(''); + const [filterQuery, setFilterQuery] = useState(''); const { createDerivedIndexPattern } = useSourceViaHttp({ sourceId: 'default', type: 'metrics', @@ -89,7 +93,7 @@ export const JobSetupScreen = (props: Props) => { indicies, moment(startDate).toDate().getTime(), undefined, - { type: 'includeAll' }, + filterQuery, partitionField ? partitionField[0] : undefined ); } else { @@ -97,11 +101,30 @@ export const JobSetupScreen = (props: Props) => { indicies, moment(startDate).toDate().getTime(), undefined, - { type: 'includeAll' }, + filterQuery, partitionField ? partitionField[0] : undefined ); } - }, [cleanUpAndSetUpModule, setUpModule, hasSummaries, indicies, partitionField, startDate]); + }, [ + cleanUpAndSetUpModule, + filterQuery, + setUpModule, + hasSummaries, + indicies, + partitionField, + startDate, + ]); + + const onFilterChange = useCallback( + (f: string) => { + setFilter(f || ''); + setFilterQuery(convertKueryToElasticSearchQuery(f, derivedIndexPattern) || ''); + }, + [derivedIndexPattern] + ); + + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + const debouncedOnFilterChange = useCallback(debounce(onFilterChange, 500), [onFilterChange]); const onPartitionFieldChange = useCallback((value: Array<{ label: string }>) => { setPartitionField(value.map((v) => v.label)); @@ -250,6 +273,40 @@ export const JobSetupScreen = (props: Props) => { />
+ + + + + } + description={ + + } + > + + } + > + + + )} From 3722bea42f03fc7d2799d88fed6bb1aaba945055 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 8 Feb 2021 12:53:54 -0700 Subject: [PATCH 38/51] [Maps] clamp MVT too many features polygon to tile boundary (#90444) * [Maps] clamp MVT too many features polygon to tile boundary * add mapbox_styles to index.js Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/maps/server/mvt/get_tile.ts | 84 ++-- x-pack/test/functional/apps/maps/index.js | 1 + x-pack/test/functional/apps/maps/joins.js | 34 -- .../functional/apps/maps/mapbox_styles.js | 358 +++++++++++------- 4 files changed, 242 insertions(+), 235 deletions(-) diff --git a/x-pack/plugins/maps/server/mvt/get_tile.ts b/x-pack/plugins/maps/server/mvt/get_tile.ts index 3116838d26fb5a..50c2014275a0f2 100644 --- a/x-pack/plugins/maps/server/mvt/get_tile.ts +++ b/x-pack/plugins/maps/server/mvt/get_tile.ts @@ -25,7 +25,7 @@ import { import { convertRegularRespToGeoJson, hitsToGeoJson } from '../../common/elasticsearch_util'; import { flattenHit } from './util'; -import { ESBounds, tile2lat, tile2long, tileToESBbox } from '../../common/geo_tile_utils'; +import { ESBounds, tileToESBbox } from '../../common/geo_tile_utils'; import { getCentroidFeatures } from '../../common/get_centroid_features'; export async function getGridTile({ @@ -53,35 +53,14 @@ export async function getGridTile({ geoFieldType: ES_GEO_FIELD_TYPE; searchSessionId?: string; }): Promise { - const esBbox: ESBounds = tileToESBbox(x, y, z); try { - let bboxFilter; - if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT) { - bboxFilter = { - geo_bounding_box: { - [geometryFieldName]: esBbox, - }, - }; - } else if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_SHAPE) { - const geojsonPolygon = tileToGeoJsonPolygon(x, y, z); - bboxFilter = { - geo_shape: { - [geometryFieldName]: { - shape: geojsonPolygon, - relation: 'INTERSECTS', - }, - }, - }; - } else { - throw new Error(`${geoFieldType} is not valid geo field-type`); - } - requestBody.query.bool.filter.push(bboxFilter); - + const tileBounds: ESBounds = tileToESBbox(x, y, z); + requestBody.query.bool.filter.push(getTileSpatialFilter(geometryFieldName, tileBounds)); requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.precision = Math.min( z + SUPER_FINE_ZOOM_DELTA, MAX_ZOOM ); - requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.bounds = esBbox; + requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.bounds = tileBounds; const response = await context .search!.search( @@ -134,14 +113,9 @@ export async function getTile({ }): Promise { let features: Feature[]; try { - requestBody.query.bool.filter.push({ - geo_shape: { - [geometryFieldName]: { - shape: tileToGeoJsonPolygon(x, y, z), - relation: 'INTERSECTS', - }, - }, - }); + requestBody.query.bool.filter.push( + getTileSpatialFilter(geometryFieldName, tileToESBbox(x, y, z)) + ); const searchOptions = { sessionId: searchSessionId, @@ -193,7 +167,8 @@ export async function getTile({ [KBN_TOO_MANY_FEATURES_PROPERTY]: true, }, geometry: esBboxToGeoJsonPolygon( - bboxResponse.rawResponse.aggregations.data_bounds.bounds + bboxResponse.rawResponse.aggregations.data_bounds.bounds, + tileToESBbox(x, y, z) ), }, ]; @@ -244,32 +219,31 @@ export async function getTile({ } } -function tileToGeoJsonPolygon(x: number, y: number, z: number): Polygon { - const wLon = tile2long(x, z); - const sLat = tile2lat(y + 1, z); - const eLon = tile2long(x + 1, z); - const nLat = tile2lat(y, z); - +function getTileSpatialFilter(geometryFieldName: string, tileBounds: ESBounds): unknown { return { - type: 'Polygon', - coordinates: [ - [ - [wLon, sLat], - [wLon, nLat], - [eLon, nLat], - [eLon, sLat], - [wLon, sLat], - ], - ], + geo_shape: { + [geometryFieldName]: { + shape: { + type: 'envelope', + // upper left and lower right points of the shape to represent a bounding rectangle in the format [[minLon, maxLat], [maxLon, minLat]] + coordinates: [ + [tileBounds.top_left.lon, tileBounds.top_left.lat], + [tileBounds.bottom_right.lon, tileBounds.bottom_right.lat], + ], + }, + relation: 'INTERSECTS', + }, + }, }; } -function esBboxToGeoJsonPolygon(esBounds: ESBounds): Polygon { - let minLon = esBounds.top_left.lon; - const maxLon = esBounds.bottom_right.lon; +function esBboxToGeoJsonPolygon(esBounds: ESBounds, tileBounds: ESBounds): Polygon { + // Intersecting geo_shapes may push bounding box outside of tile so need to clamp to tile bounds. + let minLon = Math.max(esBounds.top_left.lon, tileBounds.top_left.lon); + const maxLon = Math.min(esBounds.bottom_right.lon, tileBounds.bottom_right.lon); minLon = minLon > maxLon ? minLon - 360 : minLon; // fixes an ES bbox to straddle dateline - const minLat = esBounds.bottom_right.lat; - const maxLat = esBounds.top_left.lat; + const minLat = Math.max(esBounds.bottom_right.lat, tileBounds.bottom_right.lat); + const maxLat = Math.min(esBounds.top_left.lat, tileBounds.top_left.lat); return { type: 'Polygon', diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index d76afb7ebdc249..dd20ed58afbc67 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -47,6 +47,7 @@ export default function ({ loadTestFile, getService }) { loadTestFile(require.resolve('./es_geo_grid_source')); loadTestFile(require.resolve('./es_pew_pew_source')); loadTestFile(require.resolve('./joins')); + loadTestFile(require.resolve('./mapbox_styles')); loadTestFile(require.resolve('./mvt_scaling')); loadTestFile(require.resolve('./mvt_super_fine')); loadTestFile(require.resolve('./add_layer_panel')); diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js index 094f5335cd05ff..49717016f9c607 100644 --- a/x-pack/test/functional/apps/maps/joins.js +++ b/x-pack/test/functional/apps/maps/joins.js @@ -7,8 +7,6 @@ import expect from '@kbn/expect'; -import { MAPBOX_STYLES } from './mapbox_styles'; - const JOIN_PROPERTY_NAME = '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1'; const EXPECTED_JOIN_VALUES = { alpha: 10, @@ -18,10 +16,6 @@ const EXPECTED_JOIN_VALUES = { }; const VECTOR_SOURCE_ID = 'n1t6f'; -const CIRCLE_STYLE_LAYER_INDEX = 0; -const FILL_STYLE_LAYER_INDEX = 2; -const LINE_STYLE_LAYER_INDEX = 3; -const TOO_MANY_FEATURES_LAYER_INDEX = 4; export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); @@ -95,34 +89,6 @@ export default function ({ getPageObjects, getService }) { }); }); - it('should style fills, points, lines, and bounding-boxes independently', async () => { - const mapboxStyle = await PageObjects.maps.getMapboxStyle(); - const layersForVectorSource = mapboxStyle.layers.filter((mbLayer) => { - return mbLayer.id.startsWith(VECTOR_SOURCE_ID); - }); - - //circle layer for points - expect(layersForVectorSource[CIRCLE_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.POINT_LAYER); - - //fill layer - expect(layersForVectorSource[FILL_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.FILL_LAYER); - - //line layer for borders - expect(layersForVectorSource[LINE_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.LINE_LAYER); - - //Too many features layer (this is a static style config) - expect(layersForVectorSource[TOO_MANY_FEATURES_LAYER_INDEX]).to.eql({ - id: 'n1t6f_toomanyfeatures', - type: 'fill', - source: 'n1t6f', - minzoom: 0, - maxzoom: 24, - filter: ['==', ['get', '__kbn_too_many_features__'], true], - layout: { visibility: 'visible' }, - paint: { 'fill-pattern': '__kbn_too_many_features_image_id__', 'fill-opacity': 0.75 }, - }); - }); - it('should flag only the joined features as visible', async () => { const mapboxStyle = await PageObjects.maps.getMapboxStyle(); const vectorSource = mapboxStyle.sources[VECTOR_SOURCE_ID]; diff --git a/x-pack/test/functional/apps/maps/mapbox_styles.js b/x-pack/test/functional/apps/maps/mapbox_styles.js index d4496f13b8bef4..b483b95e0ca1fc 100644 --- a/x-pack/test/functional/apps/maps/mapbox_styles.js +++ b/x-pack/test/functional/apps/maps/mapbox_styles.js @@ -5,176 +5,242 @@ * 2.0. */ -export const MAPBOX_STYLES = { - POINT_LAYER: { - id: 'n1t6f_circle', - type: 'circle', - source: 'n1t6f', - minzoom: 0, - maxzoom: 24, - filter: [ - 'all', - ['==', ['get', '__kbn_isvisibleduetojoin__'], true], - [ - 'all', - ['!=', ['get', '__kbn_too_many_features__'], true], - ['!=', ['get', '__kbn_is_centroid_feature__'], true], - ['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']], - ], - ], - layout: { visibility: 'visible' }, - paint: { - 'circle-color': [ - 'interpolate', - ['linear'], - [ - 'coalesce', +import expect from '@kbn/expect'; + +export default function ({ getPageObjects, getService }) { + const PageObjects = getPageObjects(['maps']); + const inspector = getService('inspector'); + const security = getService('security'); + + describe('mapbox styles', () => { + let mapboxStyle; + before(async () => { + await security.testUser.setRoles( + ['global_maps_all', 'geoshape_data_reader', 'meta_for_geoshape_data_reader'], + false + ); + await PageObjects.maps.loadSavedMap('join example'); + mapboxStyle = await PageObjects.maps.getMapboxStyle(); + }); + + after(async () => { + await inspector.close(); + await security.testUser.restoreDefaults(); + }); + + it('should style circle layer as expected', async () => { + const layer = mapboxStyle.layers.find((mbLayer) => { + return mbLayer.id === 'n1t6f_circle'; + }); + expect(layer).to.eql({ + id: 'n1t6f_circle', + type: 'circle', + source: 'n1t6f', + minzoom: 0, + maxzoom: 24, + filter: [ + 'all', + ['==', ['get', '__kbn_isvisibleduetojoin__'], true], [ - 'case', - [ - '==', - ['feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1'], - null, - ], - 2, + 'all', + ['!=', ['get', '__kbn_too_many_features__'], true], + ['!=', ['get', '__kbn_is_centroid_feature__'], true], + ['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']], + ], + ], + layout: { visibility: 'visible' }, + paint: { + 'circle-color': [ + 'interpolate', + ['linear'], [ - 'max', + 'coalesce', [ - 'min', + 'case', [ - 'to-number', + '==', [ 'feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', ], + null, + ], + 2, + [ + 'max', + [ + 'min', + [ + 'to-number', + [ + 'feature-state', + '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', + ], + ], + 12, + ], + 3, ], - 12, ], - 3, + 2, ], + 2, + 'rgba(0,0,0,0)', + 3, + '#ecf1f7', + 4.125, + '#d9e3ef', + 5.25, + '#c5d5e7', + 6.375, + '#b2c7df', + 7.5, + '#9eb9d8', + 8.625, + '#8bacd0', + 9.75, + '#769fc8', + 10.875, + '#6092c0', ], - 2, - ], - 2, - 'rgba(0,0,0,0)', - 3, - '#ecf1f7', - 4.125, - '#d9e3ef', - 5.25, - '#c5d5e7', - 6.375, - '#b2c7df', - 7.5, - '#9eb9d8', - 8.625, - '#8bacd0', - 9.75, - '#769fc8', - 10.875, - '#6092c0', - ], - 'circle-opacity': 0.75, - 'circle-stroke-color': '#41937c', - 'circle-stroke-opacity': 0.75, - 'circle-stroke-width': 1, - 'circle-radius': 10, - }, - }, - FILL_LAYER: { - id: 'n1t6f_fill', - type: 'fill', - source: 'n1t6f', - minzoom: 0, - maxzoom: 24, - filter: [ - 'all', - ['==', ['get', '__kbn_isvisibleduetojoin__'], true], - [ - 'all', - ['!=', ['get', '__kbn_too_many_features__'], true], - ['!=', ['get', '__kbn_is_centroid_feature__'], true], - ['any', ['==', ['geometry-type'], 'Polygon'], ['==', ['geometry-type'], 'MultiPolygon']], - ], - ], - layout: { visibility: 'visible' }, - paint: { - 'fill-color': [ - 'interpolate', - ['linear'], - [ - 'coalesce', + 'circle-opacity': 0.75, + 'circle-stroke-color': '#41937c', + 'circle-stroke-opacity': 0.75, + 'circle-stroke-width': 1, + 'circle-radius': 10, + }, + }); + }); + + it('should style fill layer as expected', async () => { + const layer = mapboxStyle.layers.find((mbLayer) => { + return mbLayer.id === 'n1t6f_fill'; + }); + expect(layer).to.eql({ + id: 'n1t6f_fill', + type: 'fill', + source: 'n1t6f', + minzoom: 0, + maxzoom: 24, + filter: [ + 'all', + ['==', ['get', '__kbn_isvisibleduetojoin__'], true], [ - 'case', + 'all', + ['!=', ['get', '__kbn_too_many_features__'], true], + ['!=', ['get', '__kbn_is_centroid_feature__'], true], [ - '==', - ['feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1'], - null, + 'any', + ['==', ['geometry-type'], 'Polygon'], + ['==', ['geometry-type'], 'MultiPolygon'], ], - 2, + ], + ], + layout: { visibility: 'visible' }, + paint: { + 'fill-color': [ + 'interpolate', + ['linear'], [ - 'max', + 'coalesce', [ - 'min', + 'case', [ - 'to-number', + '==', [ 'feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', ], + null, + ], + 2, + [ + 'max', + [ + 'min', + [ + 'to-number', + [ + 'feature-state', + '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', + ], + ], + 12, + ], + 3, ], - 12, ], - 3, + 2, + ], + 2, + 'rgba(0,0,0,0)', + 3, + '#ecf1f7', + 4.125, + '#d9e3ef', + 5.25, + '#c5d5e7', + 6.375, + '#b2c7df', + 7.5, + '#9eb9d8', + 8.625, + '#8bacd0', + 9.75, + '#769fc8', + 10.875, + '#6092c0', + ], + 'fill-opacity': 0.75, + }, + }); + }); + + it('should style fill layer as expected', async () => { + const layer = mapboxStyle.layers.find((mbLayer) => { + return mbLayer.id === 'n1t6f_line'; + }); + expect(layer).to.eql({ + id: 'n1t6f_line', + type: 'line', + source: 'n1t6f', + minzoom: 0, + maxzoom: 24, + filter: [ + 'all', + ['==', ['get', '__kbn_isvisibleduetojoin__'], true], + [ + 'all', + ['!=', ['get', '__kbn_too_many_features__'], true], + ['!=', ['get', '__kbn_is_centroid_feature__'], true], + [ + 'any', + ['==', ['geometry-type'], 'Polygon'], + ['==', ['geometry-type'], 'MultiPolygon'], + ['==', ['geometry-type'], 'LineString'], + ['==', ['geometry-type'], 'MultiLineString'], ], ], - 2, - ], - 2, - 'rgba(0,0,0,0)', - 3, - '#ecf1f7', - 4.125, - '#d9e3ef', - 5.25, - '#c5d5e7', - 6.375, - '#b2c7df', - 7.5, - '#9eb9d8', - 8.625, - '#8bacd0', - 9.75, - '#769fc8', - 10.875, - '#6092c0', - ], - 'fill-opacity': 0.75, - }, - }, - LINE_LAYER: { - id: 'n1t6f_line', - type: 'line', - source: 'n1t6f', - minzoom: 0, - maxzoom: 24, - filter: [ - 'all', - ['==', ['get', '__kbn_isvisibleduetojoin__'], true], - [ - 'all', - ['!=', ['get', '__kbn_too_many_features__'], true], - ['!=', ['get', '__kbn_is_centroid_feature__'], true], - [ - 'any', - ['==', ['geometry-type'], 'Polygon'], - ['==', ['geometry-type'], 'MultiPolygon'], - ['==', ['geometry-type'], 'LineString'], - ['==', ['geometry-type'], 'MultiLineString'], ], - ], - ], - layout: { visibility: 'visible' }, - paint: { 'line-color': '#41937c', 'line-opacity': 0.75, 'line-width': 1 }, - }, -}; + layout: { visibility: 'visible' }, + paint: { 'line-color': '#41937c', 'line-opacity': 0.75, 'line-width': 1 }, + }); + }); + + it('should style incomplete data layer as expected', async () => { + const layer = mapboxStyle.layers.find((mbLayer) => { + return mbLayer.id === 'n1t6f_toomanyfeatures'; + }); + expect(layer).to.eql({ + id: 'n1t6f_toomanyfeatures', + type: 'fill', + source: 'n1t6f', + minzoom: 0, + maxzoom: 24, + filter: ['==', ['get', '__kbn_too_many_features__'], true], + layout: { visibility: 'visible' }, + paint: { 'fill-pattern': '__kbn_too_many_features_image_id__', 'fill-opacity': 0.75 }, + }); + }); + }); +} From a1a2536b5bb624d9dce989389319d7d527377d79 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 8 Feb 2021 21:26:57 +0100 Subject: [PATCH 39/51] [Uptime] Waterfall filters (#89185) * WIP * Use multi canvas solution * type * fix test * adde unit tests * reduce item to 150 * update margins * use constant * update z-index * added key * wip * wip * wip filters * reorgnaise components * fix issue * update filter * only highlight button * water fall test * styling * fix styling * test * fix types * update test * update ari hidden * added click telemetry for waterfall filters * added input click telemetry * update filter behaviour * fixed typo * fix type * fix styling * persist original resource number in waterfall sidebar when showing only highlighted resources * update waterfall filter collapse checkbox content * update use_bar_charts to work with filtered data * update network request total label to include filtered requests * adjust telemetry * add accessible text * add waterfall chart view telemetry * updated mime type filter label translations * adjust total network requests to use FormattedMessage * adjust translations and tests * use FormattedMessage in NetworkRequestsTotal * ensure sidebar persists when 0 resources match filter * use destructuring in waterfall sidebar item * reset collapse requests checkbox when filters are removed * update license headers Co-authored-by: Dominique Clarke Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/observability/public/index.ts | 1 + .../waterfall/data_formatting.test.ts | 330 +++++++++++------- .../step_detail/waterfall/data_formatting.ts | 65 +++- .../synthetics/step_detail/waterfall/types.ts | 43 +-- .../waterfall_chart_wrapper.test.tsx | 248 +++++++++++++ .../waterfall/waterfall_chart_wrapper.tsx | 102 +++--- .../waterfall/waterfall_filter.test.tsx | 155 ++++++++ .../waterfall/waterfall_filter.tsx | 188 ++++++++++ .../waterfall/waterfall_sidebar_item.tsx | 56 +++ .../waterfalll_sidebar_item.test.tsx | 51 +++ .../waterfall/components/constants.ts | 2 + .../components/middle_truncated_text.test.tsx | 12 +- .../components/middle_truncated_text.tsx | 9 +- .../network_requests_total.test.tsx | 51 ++- .../components/network_requests_total.tsx | 45 ++- .../waterfall/components/sidebar.tsx | 17 +- .../synthetics/waterfall/components/styles.ts | 50 ++- .../waterfall/components/translations.ts | 50 +++ .../components/use_bar_charts.test.tsx | 46 ++- .../waterfall/components/use_bar_charts.ts | 31 +- .../waterfall/components/waterfall.test.tsx | 70 ++-- .../components/waterfall_bar_chart.tsx | 112 ++++++ .../waterfall/components/waterfall_chart.tsx | 221 ++++-------- .../components/waterfall_chart_fixed_axis.tsx | 65 ++++ .../waterfall/context/waterfall_chart.tsx | 11 +- .../uptime/public/hooks/use_chart_theme.ts | 20 ++ .../public/lib/helper/enzyme_helpers.tsx | 45 ++- .../uptime/public/lib/helper/rtl_helpers.tsx | 8 +- 28 files changed, 1632 insertions(+), 472 deletions(-) create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/translations.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx create mode 100644 x-pack/plugins/uptime/public/hooks/use_chart_theme.ts diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index e9a9bb8146dbfe..1db5f62823e9bc 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -23,6 +23,7 @@ export { getCoreVitalsComponent, HeaderMenuPortal } from './components/shared/'; export { useTrackPageview, useUiTracker, + useTrackMetric, UiTracker, TrackMetricOptions, METRIC_TYPE, diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts index 487daf0332a985..a02116877f49a4 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts @@ -5,10 +5,143 @@ * 2.0. */ -import { colourPalette, getSeriesAndDomain } from './data_formatting'; +import { colourPalette, getSeriesAndDomain, getSidebarItems } from './data_formatting'; import { NetworkItems, MimeType } from './types'; import { WaterfallDataEntry } from '../../waterfall/types'; +const networkItems: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'https://unpkg.com/todomvc-app-css@2.0.4/index.css', + status: 200, + mimeType: 'text/css', + requestSentTime: 18098833.175, + requestStartTime: 18098835.439, + loadEndTime: 18098957.145, + timings: { + connect: 81.10800000213203, + wait: 34.577999998873565, + receive: 0.5520000013348181, + send: 0.3600000018195715, + total: 123.97000000055414, + proxy: -1, + blocked: 0.8540000017092098, + queueing: 2.263999998831423, + ssl: 55.38700000033714, + dns: 3.559999997378327, + }, + }, + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'https://unpkg.com/director@1.2.8/build/director.js', + status: 200, + mimeType: 'application/javascript', + requestSentTime: 18098833.537, + requestStartTime: 18098837.233999997, + loadEndTime: 18098977.648000002, + timings: { + blocked: 84.54599999822676, + receive: 3.068000001803739, + queueing: 3.69700000010198, + proxy: -1, + total: 144.1110000014305, + wait: 52.56100000042352, + connect: -1, + send: 0.2390000008745119, + ssl: -1, + dns: -1, + }, + }, +]; + +const networkItemsWithoutFullTimings: NetworkItems = [ + networkItems[0], + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', + status: 0, + mimeType: 'text/javascript', + requestSentTime: 18098834.097, + loadEndTime: 18098836.889999997, + timings: { + total: 2.7929999996558763, + blocked: -1, + ssl: -1, + wait: -1, + connect: -1, + dns: -1, + queueing: -1, + send: -1, + proxy: -1, + receive: -1, + }, + }, +]; + +const networkItemsWithoutAnyTimings: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', + status: 0, + mimeType: 'text/javascript', + requestSentTime: 18098834.097, + loadEndTime: 18098836.889999997, + timings: { + total: -1, + blocked: -1, + ssl: -1, + wait: -1, + connect: -1, + dns: -1, + queueing: -1, + send: -1, + proxy: -1, + receive: -1, + }, + }, +]; + +const networkItemsWithoutTimingsObject: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', + status: 0, + mimeType: 'text/javascript', + requestSentTime: 18098834.097, + loadEndTime: 18098836.889999997, + }, +]; + +const networkItemsWithUncommonMimeType: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'https://unpkg.com/director@1.2.8/build/director.js', + status: 200, + mimeType: 'application/x-javascript', + requestSentTime: 18098833.537, + requestStartTime: 18098837.233999997, + loadEndTime: 18098977.648000002, + timings: { + blocked: 84.54599999822676, + receive: 3.068000001803739, + queueing: 3.69700000010198, + proxy: -1, + total: 144.1110000014305, + wait: 52.56100000042352, + connect: -1, + send: 0.2390000008745119, + ssl: -1, + dns: -1, + }, + }, +]; + describe('Palettes', () => { it('A colour palette comprising timing and mime type colours is correctly generated', () => { expect(colourPalette).toEqual({ @@ -30,139 +163,6 @@ describe('Palettes', () => { }); describe('getSeriesAndDomain', () => { - const networkItems: NetworkItems = [ - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'https://unpkg.com/todomvc-app-css@2.0.4/index.css', - status: 200, - mimeType: 'text/css', - requestSentTime: 18098833.175, - requestStartTime: 18098835.439, - loadEndTime: 18098957.145, - timings: { - connect: 81.10800000213203, - wait: 34.577999998873565, - receive: 0.5520000013348181, - send: 0.3600000018195715, - total: 123.97000000055414, - proxy: -1, - blocked: 0.8540000017092098, - queueing: 2.263999998831423, - ssl: 55.38700000033714, - dns: 3.559999997378327, - }, - }, - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'https://unpkg.com/director@1.2.8/build/director.js', - status: 200, - mimeType: 'application/javascript', - requestSentTime: 18098833.537, - requestStartTime: 18098837.233999997, - loadEndTime: 18098977.648000002, - timings: { - blocked: 84.54599999822676, - receive: 3.068000001803739, - queueing: 3.69700000010198, - proxy: -1, - total: 144.1110000014305, - wait: 52.56100000042352, - connect: -1, - send: 0.2390000008745119, - ssl: -1, - dns: -1, - }, - }, - ]; - - const networkItemsWithoutFullTimings: NetworkItems = [ - networkItems[0], - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', - status: 0, - mimeType: 'text/javascript', - requestSentTime: 18098834.097, - loadEndTime: 18098836.889999997, - timings: { - total: 2.7929999996558763, - blocked: -1, - ssl: -1, - wait: -1, - connect: -1, - dns: -1, - queueing: -1, - send: -1, - proxy: -1, - receive: -1, - }, - }, - ]; - - const networkItemsWithoutAnyTimings: NetworkItems = [ - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', - status: 0, - mimeType: 'text/javascript', - requestSentTime: 18098834.097, - loadEndTime: 18098836.889999997, - timings: { - total: -1, - blocked: -1, - ssl: -1, - wait: -1, - connect: -1, - dns: -1, - queueing: -1, - send: -1, - proxy: -1, - receive: -1, - }, - }, - ]; - - const networkItemsWithoutTimingsObject: NetworkItems = [ - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', - status: 0, - mimeType: 'text/javascript', - requestSentTime: 18098834.097, - loadEndTime: 18098836.889999997, - }, - ]; - - const networkItemsWithUncommonMimeType: NetworkItems = [ - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'https://unpkg.com/director@1.2.8/build/director.js', - status: 200, - mimeType: 'application/x-javascript', - requestSentTime: 18098833.537, - requestStartTime: 18098837.233999997, - loadEndTime: 18098977.648000002, - timings: { - blocked: 84.54599999822676, - receive: 3.068000001803739, - queueing: 3.69700000010198, - proxy: -1, - total: 144.1110000014305, - wait: 52.56100000042352, - connect: -1, - send: 0.2390000008745119, - ssl: -1, - dns: -1, - }, - }, - ]; - it('formats timings', () => { const actual = getSeriesAndDomain(networkItems); expect(actual).toMatchInlineSnapshot(` @@ -175,6 +175,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#dcd4c4", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#dcd4c4", @@ -188,6 +189,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#54b399", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#54b399", @@ -201,6 +203,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#da8b45", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#da8b45", @@ -214,6 +217,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#edc5a2", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#edc5a2", @@ -227,6 +231,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#d36086", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#d36086", @@ -240,6 +245,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#b0c9e0", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#b0c9e0", @@ -253,6 +259,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#ca8eae", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#ca8eae", @@ -266,6 +273,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#dcd4c4", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#dcd4c4", @@ -279,6 +287,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#d36086", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#d36086", @@ -292,6 +301,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#b0c9e0", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#b0c9e0", @@ -305,6 +315,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#9170b8", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#9170b8", @@ -316,6 +327,7 @@ describe('getSeriesAndDomain', () => { "y0": 137.70799999925657, }, ], + "totalHighlightedRequests": 2, } `); }); @@ -332,6 +344,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#dcd4c4", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#dcd4c4", @@ -345,6 +358,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#54b399", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#54b399", @@ -358,6 +372,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#da8b45", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#da8b45", @@ -371,6 +386,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#edc5a2", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#edc5a2", @@ -384,6 +400,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#d36086", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#d36086", @@ -397,6 +414,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#b0c9e0", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#b0c9e0", @@ -410,6 +428,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#ca8eae", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#ca8eae", @@ -423,6 +442,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#9170b8", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#9170b8", @@ -434,6 +454,7 @@ describe('getSeriesAndDomain', () => { "y0": 0.9219999983906746, }, ], + "totalHighlightedRequests": 2, } `); }); @@ -450,6 +471,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "", + "isHighlighted": true, "showTooltip": false, "tooltipProps": undefined, }, @@ -458,6 +480,7 @@ describe('getSeriesAndDomain', () => { "y0": 0, }, ], + "totalHighlightedRequests": 1, } `); }); @@ -473,6 +496,7 @@ describe('getSeriesAndDomain', () => { "series": Array [ Object { "config": Object { + "isHighlighted": true, "showTooltip": false, }, "x": 0, @@ -480,6 +504,7 @@ describe('getSeriesAndDomain', () => { "y0": 0, }, ], + "totalHighlightedRequests": 1, } `); }); @@ -501,4 +526,41 @@ describe('getSeriesAndDomain', () => { }); expect(contentDownloadedingConfigItem).toBeDefined(); }); + + it('counts the total number of highlighted items', () => { + // only one CSS file in this array of network Items + const actual = getSeriesAndDomain(networkItems, false, '', ['stylesheet']); + expect(actual.totalHighlightedRequests).toBe(1); + }); + + it('adds isHighlighted to waterfall entry when filter matches', () => { + // only one CSS file in this array of network Items + const { series } = getSeriesAndDomain(networkItems, false, '', ['stylesheet']); + series.forEach((item) => { + if (item.x === 0) { + expect(item.config.isHighlighted).toBe(true); + } else { + expect(item.config.isHighlighted).toBe(false); + } + }); + }); + + it('adds isHighlighted to waterfall entry when query matches', () => { + // only the second item matches this query + const { series } = getSeriesAndDomain(networkItems, false, 'director', []); + series.forEach((item) => { + if (item.x === 1) { + expect(item.config.isHighlighted).toBe(true); + } else { + expect(item.config.isHighlighted).toBe(false); + } + }); + }); +}); + +describe('getSidebarItems', () => { + it('passes the item index offset by 1 to offsetIndex for visual display', () => { + const actual = getSidebarItems(networkItems, false, '', []); + expect(actual[0].offsetIndex).toBe(1); + }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts index 0ac93794594c08..46f0d23d0a6b99 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts @@ -55,8 +55,28 @@ const getFriendlyTooltipValue = ({ } return `${label}: ${formatValueForDisplay(value)}ms`; }; +export const isHighlightedItem = ( + item: NetworkItem, + query?: string, + activeFilters: string[] = [] +) => { + if (!query && activeFilters?.length === 0) { + return true; + } + + const matchQuery = query ? item.url?.includes(query) : true; + const matchFilters = + activeFilters.length > 0 ? activeFilters.includes(MimeTypesMap[item.mimeType!]) : true; + + return !!(matchQuery && matchFilters); +}; -export const getSeriesAndDomain = (items: NetworkItems) => { +export const getSeriesAndDomain = ( + items: NetworkItems, + onlyHighlighted = false, + query?: string, + activeFilters?: string[] +) => { const getValueForOffset = (item: NetworkItem) => { return item.requestSentTime; }; @@ -78,13 +98,21 @@ export const getSeriesAndDomain = (items: NetworkItems) => { } }; + let totalHighlightedRequests = 0; + const series = items.reduce((acc, item, index) => { + const isHighlighted = isHighlightedItem(item, query, activeFilters); + if (isHighlighted) { + totalHighlightedRequests++; + } + if (!item.timings) { acc.push({ x: index, y0: 0, y: 0, config: { + isHighlighted, showTooltip: false, }, }); @@ -96,10 +124,13 @@ export const getSeriesAndDomain = (items: NetworkItems) => { let currentOffset = offsetValue - zeroOffset; + let timingValueFound = false; + TIMING_ORDER.forEach((timing) => { const value = getValue(item.timings, timing); - const colour = timing === Timings.Receive ? mimeTypeColour : colourPalette[timing]; if (value && value >= 0) { + timingValueFound = true; + const colour = timing === Timings.Receive ? mimeTypeColour : colourPalette[timing]; const y = currentOffset + value; acc.push({ @@ -108,6 +139,7 @@ export const getSeriesAndDomain = (items: NetworkItems) => { y, config: { colour, + isHighlighted, showTooltip: true, tooltipProps: { value: getFriendlyTooltipValue({ @@ -126,7 +158,7 @@ export const getSeriesAndDomain = (items: NetworkItems) => { /* if no specific timing values are found, use the total time * if total time is not available use 0, set showTooltip to false, * and omit tooltip props */ - if (!acc.find((entry) => entry.x === index)) { + if (!timingValueFound) { const total = item.timings.total; const hasTotal = total !== -1; acc.push({ @@ -134,6 +166,7 @@ export const getSeriesAndDomain = (items: NetworkItems) => { y0: hasTotal ? currentOffset : 0, y: hasTotal ? currentOffset + item.timings.total : 0, config: { + isHighlighted, colour: hasTotal ? mimeTypeColour : '', showTooltip: hasTotal, tooltipProps: hasTotal @@ -154,14 +187,31 @@ export const getSeriesAndDomain = (items: NetworkItems) => { const yValues = series.map((serie) => serie.y); const domain = { min: 0, max: Math.max(...yValues) }; - return { series, domain }; + + let filteredSeries = series; + if (onlyHighlighted) { + filteredSeries = series.filter((item) => item.config.isHighlighted); + } + + return { series: filteredSeries, domain, totalHighlightedRequests }; }; -export const getSidebarItems = (items: NetworkItems): SidebarItems => { - return items.map((item) => { +export const getSidebarItems = ( + items: NetworkItems, + onlyHighlighted: boolean, + query: string, + activeFilters: string[] +): SidebarItems => { + const sideBarItems = items.map((item, index) => { + const isHighlighted = isHighlightedItem(item, query, activeFilters); + const offsetIndex = index + 1; const { url, status, method } = item; - return { url, status, method }; + return { url, status, method, isHighlighted, offsetIndex }; }); + if (onlyHighlighted) { + return sideBarItems.filter((item) => item.isHighlighted); + } + return sideBarItems; }; export const getLegendItems = (): LegendItems => { @@ -184,6 +234,7 @@ export const getLegendItems = (): LegendItems => { { name: FriendlyMimetypeLabels[mimeType], colour: MIME_TYPE_PALETTE[mimeType] }, ]; }); + return [...timingItems, ...mimeTypeItems]; }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts index 8d261edc74bf4b..e22caae0d9eb2a 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts @@ -61,16 +61,13 @@ export const TIMING_ORDER = [ Timings.Receive, ] as const; -export type CalculatedTimings = { - [K in Timings]?: number; -}; - export enum MimeType { Html = 'html', Script = 'script', Stylesheet = 'stylesheet', Media = 'media', Font = 'font', + XHR = 'xhr', Other = 'other', } @@ -99,6 +96,9 @@ export const FriendlyMimetypeLabels = { [MimeType.Font]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.font', { defaultMessage: 'Font', }), + [MimeType.XHR]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.xhr', { + defaultMessage: 'XHR', + }), [MimeType.Other]: i18n.translate( 'xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.other', { @@ -112,7 +112,6 @@ export const FriendlyMimetypeLabels = { export const MimeTypesMap: Record = { 'text/html': MimeType.Html, 'application/javascript': MimeType.Script, - 'application/json': MimeType.Script, 'text/javascript': MimeType.Script, 'text/css': MimeType.Stylesheet, // Images @@ -146,38 +145,18 @@ export const MimeTypesMap: Record = { 'application/font-woff2': MimeType.Font, 'application/vnd.ms-fontobject': MimeType.Font, 'application/font-sfnt': MimeType.Font, + + // XHR + 'application/json': MimeType.XHR, }; export type NetworkItem = NetworkEvent; export type NetworkItems = NetworkItem[]; -// NOTE: A number will always be present if the property exists, but that number might be -1, which represents no value. -export interface PayloadTimings { - dns_start: number; - push_end: number; - worker_fetch_start: number; - worker_respond_with_settled: number; - proxy_end: number; - worker_start: number; - worker_ready: number; - send_end: number; - connect_end: number; - connect_start: number; - send_start: number; - proxy_start: number; - push_start: number; - ssl_end: number; - receive_headers_end: number; - ssl_start: number; - request_time: number; - dns_end: number; -} - -export interface ExtraSeriesConfig { - colour: string; -} - -export type SidebarItem = Pick; +export type SidebarItem = Pick & { + isHighlighted: boolean; + offsetIndex: number; +}; export type SidebarItems = SidebarItem[]; export interface LegendItem { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx new file mode 100644 index 00000000000000..e22f4a4c63f596 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx @@ -0,0 +1,248 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { act, fireEvent } from '@testing-library/react'; +import { WaterfallChartWrapper } from './waterfall_chart_wrapper'; + +import { render } from '../../../../../lib/helper/rtl_helpers'; + +import { extractItems, isHighlightedItem } from './data_formatting'; + +import 'jest-canvas-mock'; +import { BAR_HEIGHT } from '../../waterfall/components/constants'; +import { MimeType } from './types'; +import { + FILTER_POPOVER_OPEN_LABEL, + FILTER_REQUESTS_LABEL, + FILTER_COLLAPSE_REQUESTS_LABEL, +} from '../../waterfall/components/translations'; + +const getHighLightedItems = (query: string, filters: string[]) => { + return NETWORK_EVENTS.events.filter((item) => isHighlightedItem(item, query, filters)); +}; + +describe('waterfall chart wrapper', () => { + jest.useFakeTimers(); + + it('renders the correct sidebar items', () => { + const { getAllByTestId } = render( + + ); + + const sideBarItems = getAllByTestId('middleTruncatedTextSROnly'); + + expect(sideBarItems).toHaveLength(5); + }); + + it('search by query works', () => { + const { getAllByTestId, getByTestId, getByLabelText } = render( + + ); + + const filterInput = getByLabelText(FILTER_REQUESTS_LABEL); + + const searchText = '.js'; + + fireEvent.change(filterInput, { target: { value: searchText } }); + + // inout has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + const highlightedItemsLength = getHighLightedItems(searchText, []).length; + expect(getAllByTestId('sideBarHighlightedItem')).toHaveLength(highlightedItemsLength); + + expect(getAllByTestId('sideBarDimmedItem')).toHaveLength( + NETWORK_EVENTS.events.length - highlightedItemsLength + ); + + const SIDE_BAR_ITEMS_HEIGHT = NETWORK_EVENTS.events.length * BAR_HEIGHT; + expect(getByTestId('wfSidebarContainer')).toHaveAttribute('height', `${SIDE_BAR_ITEMS_HEIGHT}`); + + expect(getByTestId('wfDataOnlyBarChart')).toHaveAttribute('height', `${SIDE_BAR_ITEMS_HEIGHT}`); + }); + + it('search by mime type works', () => { + const { getAllByTestId, getByLabelText, getAllByText } = render( + + ); + + const sideBarItems = getAllByTestId('middleTruncatedTextSROnly'); + + expect(sideBarItems).toHaveLength(5); + + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + + fireEvent.click(getAllByText('XHR')[1]); + + // inout has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + const highlightedItemsLength = getHighLightedItems('', [MimeType.XHR]).length; + + expect(getAllByTestId('sideBarHighlightedItem')).toHaveLength(highlightedItemsLength); + expect(getAllByTestId('sideBarDimmedItem')).toHaveLength( + NETWORK_EVENTS.events.length - highlightedItemsLength + ); + }); + + it('renders sidebar even when filter matches 0 resources', () => { + const { getAllByTestId, getByLabelText, getAllByText, queryAllByTestId } = render( + + ); + + const sideBarItems = getAllByTestId('middleTruncatedTextSROnly'); + + expect(sideBarItems).toHaveLength(5); + + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + + fireEvent.click(getAllByText('CSS')[1]); + + // inout has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + const highlightedItemsLength = getHighLightedItems('', [MimeType.Stylesheet]).length; + + // no CSS items found + expect(queryAllByTestId('sideBarHighlightedItem')).toHaveLength(0); + expect(getAllByTestId('sideBarDimmedItem')).toHaveLength( + NETWORK_EVENTS.events.length - highlightedItemsLength + ); + + fireEvent.click(getByLabelText(FILTER_COLLAPSE_REQUESTS_LABEL)); + + // filter bar is still accessible even when no resources match filter + expect(getByLabelText(FILTER_REQUESTS_LABEL)).toBeInTheDocument(); + + // no resources items are in the chart as none match filter + expect(queryAllByTestId('sideBarHighlightedItem')).toHaveLength(0); + expect(queryAllByTestId('sideBarDimmedItem')).toHaveLength(0); + }); +}); + +const NETWORK_EVENTS = { + events: [ + { + timestamp: '2021-01-21T10:31:21.537Z', + method: 'GET', + url: + 'https://apv-static.minute.ly/videos/v-c2a526c7-450d-428e-1244649-a390-fb639ffead96-s45.746-54.421m.mp4', + status: 206, + mimeType: 'video/mp4', + requestSentTime: 241114127.474, + requestStartTime: 241114129.214, + loadEndTime: 241116573.402, + timings: { + total: 2445.928000001004, + queueing: 1.7399999778717756, + blocked: 0.391999987186864, + receive: 2283.964000031119, + connect: 91.5709999972023, + wait: 28.795999998692423, + proxy: -1, + dns: 36.952000024029985, + send: 0.10000000474974513, + ssl: 64.28900000173599, + }, + }, + { + timestamp: '2021-01-21T10:31:22.174Z', + method: 'GET', + url: 'https://dpm.demdex.net/ibs:dpid=73426&dpuuid=31597189268188866891125449924942215949', + status: 200, + mimeType: 'image/gif', + requestSentTime: 241114749.202, + requestStartTime: 241114750.426, + loadEndTime: 241114805.541, + timings: { + queueing: 1.2240000069141388, + receive: 2.218999987235293, + proxy: -1, + dns: -1, + send: 0.14200000441633165, + blocked: 1.033000007737428, + total: 56.33900000248104, + wait: 51.72099999617785, + ssl: -1, + connect: -1, + }, + }, + { + timestamp: '2021-01-21T10:31:21.679Z', + method: 'GET', + url: 'https://dapi.cms.mlbinfra.com/v2/content/en-us/sel-t119-homepage-mediawall', + status: 200, + mimeType: 'application/json', + requestSentTime: 241114268.04299998, + requestStartTime: 241114270.184, + loadEndTime: 241114665.609, + timings: { + total: 397.5659999996424, + dns: 29.5429999823682, + wait: 221.6830000106711, + queueing: 2.1410000044852495, + connect: 106.95499999565072, + ssl: 69.06899999012239, + receive: 2.027999988058582, + blocked: 0.877000013133511, + send: 23.719999997410923, + proxy: -1, + }, + }, + { + timestamp: '2021-01-21T10:31:21.740Z', + method: 'GET', + url: 'https://platform.twitter.com/embed/embed.runtime.b313577971db9c857801.js', + status: 200, + mimeType: 'application/javascript', + requestSentTime: 241114303.84899998, + requestStartTime: 241114306.416, + loadEndTime: 241114370.361, + timings: { + send: 1.357000001007691, + wait: 40.12299998430535, + receive: 16.78500001435168, + ssl: -1, + queueing: 2.5670000177342445, + total: 66.51200001942925, + connect: -1, + blocked: 5.680000002030283, + proxy: -1, + dns: -1, + }, + }, + { + timestamp: '2021-01-21T10:31:21.740Z', + method: 'GET', + url: 'https://platform.twitter.com/embed/embed.modules.7a266e7acfd42f2581a5.js', + status: 200, + mimeType: 'application/javascript', + requestSentTime: 241114305.939, + requestStartTime: 241114310.393, + loadEndTime: 241114938.264, + timings: { + wait: 51.61500000394881, + dns: -1, + ssl: -1, + receive: 506.5750000067055, + proxy: -1, + connect: -1, + blocked: 69.51599998865277, + queueing: 4.453999979887158, + total: 632.324999984121, + send: 0.16500000492669642, + }, + }, + ], +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx index 91657981e7f890..8a0e9729a635b0 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx @@ -5,44 +5,14 @@ * 2.0. */ -import React, { useMemo, useState } from 'react'; -import { EuiHealth, EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; +import { EuiHealth } from '@elastic/eui'; +import { useTrackMetric, METRIC_TYPE } from '../../../../../../../observability/public'; import { getSeriesAndDomain, getSidebarItems, getLegendItems } from './data_formatting'; import { SidebarItem, LegendItem, NetworkItems } from './types'; -import { - WaterfallProvider, - WaterfallChart, - MiddleTruncatedText, - RenderItem, -} from '../../waterfall'; - -export const renderSidebarItem: RenderItem = (item, index) => { - const { status } = item; - - const isErrorStatusCode = (statusCode: number) => { - const is400 = statusCode >= 400 && statusCode <= 499; - const is500 = statusCode >= 500 && statusCode <= 599; - const isSpecific300 = statusCode === 301 || statusCode === 307 || statusCode === 308; - return is400 || is500 || isSpecific300; - }; - - return ( - <> - {!status || !isErrorStatusCode(status) ? ( - - ) : ( - - - - - - {status} - - - )} - - ); -}; +import { WaterfallProvider, WaterfallChart, RenderItem } from '../../waterfall'; +import { WaterfallFilter } from './waterfall_filter'; +import { WaterfallSidebarItem } from './waterfall_sidebar_item'; export const renderLegendItem: RenderItem = (item) => { return {item.name}; @@ -54,23 +24,64 @@ interface Props { } export const WaterfallChartWrapper: React.FC = ({ data, total }) => { + const [query, setQuery] = useState(''); + const [activeFilters, setActiveFilters] = useState([]); + const [onlyHighlighted, setOnlyHighlighted] = useState(false); + const [networkData] = useState(data); - const { series, domain } = useMemo(() => { - return getSeriesAndDomain(networkData); - }, [networkData]); + const hasFilters = activeFilters.length > 0; + + const { series, domain, totalHighlightedRequests } = useMemo(() => { + return getSeriesAndDomain(networkData, onlyHighlighted, query, activeFilters); + }, [networkData, query, activeFilters, onlyHighlighted]); const sidebarItems = useMemo(() => { - return getSidebarItems(networkData); - }, [networkData]); + return getSidebarItems(networkData, onlyHighlighted, query, activeFilters); + }, [networkData, query, activeFilters, onlyHighlighted]); const legendItems = getLegendItems(); + const renderFilter = useCallback(() => { + return ( + + ); + }, [activeFilters, setActiveFilters, onlyHighlighted, setOnlyHighlighted, query, setQuery]); + + const renderSidebarItem: RenderItem = useCallback( + (item) => { + return ( + + ); + }, + [hasFilters, onlyHighlighted] + ); + + useTrackMetric({ app: 'uptime', metric: 'waterfall_chart_view', metricType: METRIC_TYPE.COUNT }); + useTrackMetric({ + app: 'uptime', + metric: 'waterfall_chart_view', + metricType: METRIC_TYPE.COUNT, + delay: 15000, + }); + return ( { @@ -81,10 +92,19 @@ export const WaterfallChartWrapper: React.FC = ({ data, total }) => { tickFormat={(d: number) => `${Number(d).toFixed(0)} ms`} domain={domain} barStyleAccessor={(datum) => { + if (!datum.datum.config.isHighlighted) { + return { + rect: { + fill: datum.datum.config.colour, + opacity: '0.1', + }, + }; + } return datum.datum.config.colour; }} renderSidebarItem={renderSidebarItem} renderLegendItem={renderLegendItem} + renderFilter={renderFilter} fullHeight={true} /> diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.test.tsx new file mode 100644 index 00000000000000..3acf6a269fb38f --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.test.tsx @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { act, fireEvent } from '@testing-library/react'; + +import { render } from '../../../../../lib/helper/rtl_helpers'; + +import 'jest-canvas-mock'; +import { MIME_FILTERS, WaterfallFilter } from './waterfall_filter'; +import { + FILTER_REQUESTS_LABEL, + FILTER_COLLAPSE_REQUESTS_LABEL, + FILTER_POPOVER_OPEN_LABEL, +} from '../../waterfall/components/translations'; + +describe('waterfall filter', () => { + jest.useFakeTimers(); + + it('renders correctly', () => { + const { getByLabelText, getByTitle } = render( + + ); + + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + + MIME_FILTERS.forEach((filter) => { + expect(getByTitle(filter.label)); + }); + }); + + it('filter icon changes color on active/inactive filters', () => { + const Component = () => { + const [activeFilters, setActiveFilters] = useState([]); + + return ( + + ); + }; + const { getByLabelText, getByTitle } = render(); + + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + + fireEvent.click(getByTitle('XHR')); + + expect(getByLabelText(FILTER_POPOVER_OPEN_LABEL)).toHaveAttribute( + 'class', + 'euiButtonIcon euiButtonIcon--primary' + ); + + // toggle it back to inactive + fireEvent.click(getByTitle('XHR')); + + expect(getByLabelText(FILTER_POPOVER_OPEN_LABEL)).toHaveAttribute( + 'class', + 'euiButtonIcon euiButtonIcon--text' + ); + }); + + it('search input is working properly', () => { + const setQuery = jest.fn(); + + const Component = () => { + return ( + + ); + }; + const { getByLabelText } = render(); + + const testText = 'js'; + + fireEvent.change(getByLabelText(FILTER_REQUESTS_LABEL), { target: { value: testText } }); + + // inout has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + expect(setQuery).toHaveBeenCalledWith(testText); + }); + + it('resets checkbox when filters are removed', () => { + const Component = () => { + const [onlyHighlighted, setOnlyHighlighted] = useState(false); + const [query, setQuery] = useState(''); + const [activeFilters, setActiveFilters] = useState([]); + return ( + + ); + }; + const { getByLabelText, getByTitle } = render(); + const input = getByLabelText(FILTER_REQUESTS_LABEL); + // apply filters + const testText = 'js'; + fireEvent.change(input, { target: { value: testText } }); + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + const filterGroupButton = getByTitle('XHR'); + fireEvent.click(filterGroupButton); + + // input has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + const collapseCheckbox = getByLabelText(FILTER_COLLAPSE_REQUESTS_LABEL) as HTMLInputElement; + expect(collapseCheckbox).not.toBeDisabled(); + fireEvent.click(collapseCheckbox); + expect(collapseCheckbox).toBeChecked(); + + // remove filters + fireEvent.change(input, { target: { value: '' } }); + fireEvent.click(filterGroupButton); + + // input has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + // expect the checkbox to reset to disabled and unchecked + expect(collapseCheckbox).not.toBeChecked(); + expect(collapseCheckbox).toBeDisabled(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.tsx new file mode 100644 index 00000000000000..42c2df4553b4c2 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.tsx @@ -0,0 +1,188 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import { + EuiButtonIcon, + EuiCheckbox, + EuiFieldSearch, + EuiFilterButton, + EuiFilterGroup, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiSpacer, +} from '@elastic/eui'; +import useDebounce from 'react-use/lib/useDebounce'; +import { + FILTER_REQUESTS_LABEL, + FILTER_SCREENREADER_LABEL, + FILTER_REMOVE_SCREENREADER_LABEL, + FILTER_POPOVER_OPEN_LABEL, + FILTER_COLLAPSE_REQUESTS_LABEL, +} from '../../waterfall/components/translations'; +import { MimeType, FriendlyMimetypeLabels } from './types'; +import { METRIC_TYPE, useUiTracker } from '../../../../../../../observability/public'; + +interface Props { + query: string; + activeFilters: string[]; + setActiveFilters: Dispatch>; + setQuery: (val: string) => void; + onlyHighlighted: boolean; + setOnlyHighlighted: (val: boolean) => void; +} + +export const MIME_FILTERS = [ + { + label: FriendlyMimetypeLabels[MimeType.XHR], + mimeType: MimeType.XHR, + }, + { + label: FriendlyMimetypeLabels[MimeType.Html], + mimeType: MimeType.Html, + }, + { + label: FriendlyMimetypeLabels[MimeType.Script], + mimeType: MimeType.Script, + }, + { + label: FriendlyMimetypeLabels[MimeType.Stylesheet], + mimeType: MimeType.Stylesheet, + }, + { + label: FriendlyMimetypeLabels[MimeType.Font], + mimeType: MimeType.Font, + }, + { + label: FriendlyMimetypeLabels[MimeType.Media], + mimeType: MimeType.Media, + }, +]; + +export const WaterfallFilter = ({ + query, + setQuery, + activeFilters, + setActiveFilters, + onlyHighlighted, + setOnlyHighlighted, +}: Props) => { + const [value, setValue] = useState(query); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const trackMetric = useUiTracker({ app: 'uptime' }); + + const toggleFilters = (val: string) => { + setActiveFilters((prevState) => + prevState.includes(val) ? prevState.filter((filter) => filter !== val) : [...prevState, val] + ); + }; + useDebounce( + () => { + setQuery(value); + }, + 250, + [value] + ); + + /* reset checkbox when there is no query or active filters + * this prevents the checkbox from being checked in a disabled state */ + useEffect(() => { + if (!(query || activeFilters.length > 0)) { + setOnlyHighlighted(false); + } + }, [activeFilters.length, setOnlyHighlighted, query]); + + // indicates use of the query input box + useEffect(() => { + if (query) { + trackMetric({ metric: 'waterfall_filter_input_changed', metricType: METRIC_TYPE.CLICK }); + } + }, [query, trackMetric]); + + // indicates the collapse to show only highlighted checkbox has been clicked + useEffect(() => { + if (onlyHighlighted) { + trackMetric({ + metric: 'waterfall_filter_collapse_checked', + metricType: METRIC_TYPE.CLICK, + }); + } + }, [onlyHighlighted, trackMetric]); + + // indicates filters have been applied or changed + useEffect(() => { + if (activeFilters.length > 0) { + trackMetric({ + metric: `waterfall_filters_applied_changed`, + metricType: METRIC_TYPE.CLICK, + }); + } + }, [activeFilters, trackMetric]); + + return ( + + + { + setValue(evt.target.value); + }} + value={value} + /> + + + setIsPopoverOpen((prevState) => !prevState)} + color={activeFilters.length > 0 ? 'primary' : 'text'} + isSelected={activeFilters.length > 0} + /> + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + anchorPosition="rightCenter" + > + + {MIME_FILTERS.map(({ label, mimeType }) => ( + toggleFilters(mimeType)} + key={label} + withNext={true} + aria-label={`${ + activeFilters.includes(mimeType) + ? FILTER_REMOVE_SCREENREADER_LABEL + : FILTER_SCREENREADER_LABEL + } ${label}`} + > + {label} + + ))} + + + 0)} + id="onlyHighlighted" + label={FILTER_COLLAPSE_REQUESTS_LABEL} + checked={onlyHighlighted} + onChange={(e) => { + setOnlyHighlighted(e.target.checked); + }} + /> + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx new file mode 100644 index 00000000000000..25b577ef9403aa --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import { SidebarItem } from '../waterfall/types'; +import { MiddleTruncatedText } from '../../waterfall'; +import { SideBarItemHighlighter } from '../../waterfall/components/styles'; +import { SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL } from '../../waterfall/components/translations'; + +interface SidebarItemProps { + item: SidebarItem; + renderFilterScreenReaderText?: boolean; +} + +export const WaterfallSidebarItem = ({ item, renderFilterScreenReaderText }: SidebarItemProps) => { + const { status, offsetIndex, isHighlighted } = item; + + const isErrorStatusCode = (statusCode: number) => { + const is400 = statusCode >= 400 && statusCode <= 499; + const is500 = statusCode >= 500 && statusCode <= 599; + const isSpecific300 = statusCode === 301 || statusCode === 307 || statusCode === 308; + return is400 || is500 || isSpecific300; + }; + + const text = `${offsetIndex}. ${item.url}`; + const ariaLabel = `${ + isHighlighted && renderFilterScreenReaderText + ? `${SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL} ` + : '' + }${text}`; + + return ( + + {!status || !isErrorStatusCode(status) ? ( + + ) : ( + + + + + + {status} + + + )} + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx new file mode 100644 index 00000000000000..578d66a1ea3f1d --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { SidebarItem } from '../waterfall/types'; + +import { render } from '../../../../../lib/helper/rtl_helpers'; + +import 'jest-canvas-mock'; +import { WaterfallSidebarItem } from './waterfall_sidebar_item'; +import { SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL } from '../../waterfall/components/translations'; + +describe('waterfall filter', () => { + const url = 'http://www.elastic.co'; + const offsetIndex = 1; + const item: SidebarItem = { + url, + isHighlighted: true, + offsetIndex, + }; + + it('renders sidbar item', () => { + const { getByText } = render(); + + expect(getByText(`${offsetIndex}. ${url}`)); + }); + + it('render screen reader text when renderFilterScreenReaderText is true', () => { + const { getByLabelText } = render( + + ); + + expect( + getByLabelText(`${SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL} ${offsetIndex}. ${url}`) + ).toBeInTheDocument(); + }); + + it('does not render screen reader text when renderFilterScreenReaderText is false', () => { + const { queryByLabelText } = render( + + ); + + expect( + queryByLabelText(`${SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL} ${offsetIndex}. ${url}`) + ).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts index 543d6004b8955b..a4b75174543a81 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts @@ -17,3 +17,5 @@ export const FIXED_AXIS_HEIGHT = 32; // number of items to display in canvas, since canvas can only have limited size export const CANVAS_MAX_ITEMS = 150; + +export const CHART_LEGEND_PADDING = 62; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx index 9a3d4efb63a3a8..d6c1d777a40a78 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx @@ -25,15 +25,21 @@ describe('getChunks', () => { }); describe('Component', () => { - it('renders truncated text', () => { - const { getByText } = render(); + it('renders truncated text and aria label', () => { + const { getByText, getByLabelText } = render( + + ); expect(getByText(first)).toBeInTheDocument(); expect(getByText(last)).toBeInTheDocument(); + + expect(getByLabelText(longString)).toBeInTheDocument(); }); it('renders screen reader only text', () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + ); const { getByText } = within(getByTestId('middleTruncatedTextSROnly')); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx index 9c263312f78f54..ec363ed2b40a4e 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx @@ -10,6 +10,11 @@ import styled from 'styled-components'; import { EuiScreenReaderOnly, EuiToolTip } from '@elastic/eui'; import { FIXED_AXIS_HEIGHT } from './constants'; +interface Props { + ariaLabel: string; + text: string; +} + const OuterContainer = styled.div` width: 100%; height: 100%; @@ -50,14 +55,14 @@ export const getChunks = (text: string) => { // Helper component for adding middle text truncation, e.g. // really-really-really-long....ompressed.js // Can be used to accomodate content in sidebar item rendering. -export const MiddleTruncatedText = ({ text }: { text: string }) => { +export const MiddleTruncatedText = ({ ariaLabel, text }: Props) => { const chunks = useMemo(() => { return getChunks(text); }, [text]); return ( <> - + {text} diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx index f46bab8c33a85b..63b4d2945a51c3 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx @@ -12,7 +12,11 @@ import { render } from '../../../../../lib/helper/rtl_helpers'; describe('NetworkRequestsTotal', () => { it('message in case total is greater than fetched', () => { const { getByText, getByLabelText } = render( - + ); expect(getByText('First 1000/1100 network requests')).toBeInTheDocument(); @@ -21,9 +25,52 @@ describe('NetworkRequestsTotal', () => { it('message in case total is equal to fetched requests', () => { const { getByText } = render( - + ); expect(getByText('500 network requests')).toBeInTheDocument(); }); + + it('does not show highlighted item message when showHighlightedNetworkEvents is false', () => { + const { queryByText } = render( + + ); + + expect(queryByText(/match the filter/)).not.toBeInTheDocument(); + }); + + it('does not show highlighted item message when highlightedNetworkEvents is less than 0', () => { + const { queryByText } = render( + + ); + + expect(queryByText(/match the filter/)).not.toBeInTheDocument(); + }); + + it('show highlighted item message when highlightedNetworkEvents is greater than 0 and showHighlightedNetworkEvents is true', () => { + const { getByText } = render( + + ); + + expect(getByText(/\(20 match the filter\)/)).toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx index fce86c6b5c29d8..5ccd60b0ce7a88 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { EuiIconTip } from '@elastic/eui'; import { NetworkRequestsTotalStyle } from './styles'; @@ -13,24 +14,44 @@ import { NetworkRequestsTotalStyle } from './styles'; interface Props { totalNetworkRequests: number; fetchedNetworkRequests: number; + highlightedNetworkRequests: number; + showHighlightedNetworkRequests?: boolean; } -export const NetworkRequestsTotal = ({ totalNetworkRequests, fetchedNetworkRequests }: Props) => { +export const NetworkRequestsTotal = ({ + totalNetworkRequests, + fetchedNetworkRequests, + highlightedNetworkRequests, + showHighlightedNetworkRequests, +}: Props) => { return ( - {i18n.translate('xpack.uptime.synthetics.waterfall.requestsTotalMessage', { - defaultMessage: '{numNetworkRequests} network requests', - values: { + fetchedNetworkRequests - ? i18n.translate('xpack.uptime.synthetics.waterfall.requestsTotalMessage.first', { - defaultMessage: 'First {count}', - values: { count: `${fetchedNetworkRequests}/${totalNetworkRequests}` }, - }) - : totalNetworkRequests, - }, - })} + totalNetworkRequests > fetchedNetworkRequests ? ( + + ) : ( + totalNetworkRequests + ), + }} + />{' '} + {showHighlightedNetworkRequests && highlightedNetworkRequests >= 0 && ( + + )} {totalNetworkRequests > fetchedNetworkRequests && ( = ({ items, render }) => { return ( - + - {items.map((item, index) => { - return ( - - {render(item, index)} - - ); - })} + {items.map((item) => ( + + {render(item)} + + ))} diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts index 333acd6e043df7..c00c04b1140450 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts @@ -14,10 +14,7 @@ interface WaterfallChartOuterContainerProps { height?: string; } -export const WaterfallChartOuterContainer = euiStyled.div` - height: ${(props) => (props.height ? `${props.height}` : 'auto')}; - overflow-y: ${(props) => (props.height ? 'scroll' : 'visible')}; - overflow-x: hidden; +const StyledScrollDiv = euiStyled.div` &::-webkit-scrollbar { height: ${({ theme }) => theme.eui.euiScrollBar}; width: ${({ theme }) => theme.eui.euiScrollBar}; @@ -33,11 +30,27 @@ export const WaterfallChartOuterContainer = euiStyled.div` + height: ${(props) => (props.height ? `${props.height}` : 'auto')}; + overflow-y: ${(props) => (props.height ? 'scroll' : 'visible')}; + overflow-x: hidden; +`; + +export const WaterfallChartFixedTopContainer = euiStyled(StyledScrollDiv)` position: sticky; top: 0; z-index: ${(props) => props.theme.eui.euiZLevel4}; - border-bottom: ${(props) => `1px solid ${props.theme.eui.euiColorLightShade}`}; + overflow-y: scroll; + overflow-x: hidden; +`; + +export const WaterfallChartAxisOnlyContainer = euiStyled(EuiFlexItem)` + margin-left: -22px; +`; + +export const WaterfallChartTopContainer = euiStyled(EuiFlexGroup)` `; export const WaterfallChartFixedTopContainerSidebarCover = euiStyled(EuiPanel)` @@ -46,9 +59,18 @@ export const WaterfallChartFixedTopContainerSidebarCover = euiStyled(EuiPanel)` border: none; `; // NOTE: border-radius !important is here as the "border" prop isn't working +export const WaterfallChartFilterContainer = euiStyled.div` + && { + padding: 16px; + z-index: ${(props) => props.theme.eui.euiZLevel5}; + border-bottom: 0.3px solid ${(props) => props.theme.eui.euiColorLightShade}; + } +`; // NOTE: border-radius !important is here as the "border" prop isn't working + export const WaterfallChartFixedAxisContainer = euiStyled.div` height: ${FIXED_AXIS_HEIGHT}px; z-index: ${(props) => props.theme.eui.euiZLevel4}; + height: 100%; `; interface WaterfallChartSidebarContainer { @@ -74,6 +96,12 @@ export const WaterfallChartSidebarFlexItem = euiStyled(EuiFlexItem)` min-width: 0; padding-left: ${(props) => props.theme.eui.paddingSizes.m}; padding-right: ${(props) => props.theme.eui.paddingSizes.m}; + z-index: ${(props) => props.theme.eui.euiZLevel4}; +`; + +export const SideBarItemHighlighter = euiStyled.span<{ isHighlighted: boolean }>` + opacity: ${(props) => (props.isHighlighted ? 1 : 0.4)}; + height: 100%; `; interface WaterfallChartChartContainer { @@ -106,6 +134,12 @@ export const WaterfallChartTooltip = euiStyled.div` `; export const NetworkRequestsTotalStyle = euiStyled(EuiText)` - line-height: ${FIXED_AXIS_HEIGHT}px; - margin-left: ${(props) => props.theme.eui.paddingSizes.m} + line-height: 28px; + padding: 0 ${(props) => props.theme.eui.paddingSizes.m}; + border-bottom: 0.3px solid ${(props) => props.theme.eui.euiColorLightShade}; + z-index: ${(props) => props.theme.eui.euiZLevel5}; +`; + +export const RelativeContainer = euiStyled.div` + position: relative; `; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/translations.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/translations.ts new file mode 100644 index 00000000000000..b63ffacaadd2e9 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/translations.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const FILTER_REQUESTS_LABEL = i18n.translate( + 'xpack.uptime.synthetics.waterfall.searchBox.placeholder', + { + defaultMessage: 'Filter network requests', + } +); + +export const FILTER_SCREENREADER_LABEL = i18n.translate( + 'xpack.uptime.synthetics.waterfall.filterGroup.filterScreenreaderLabel', + { + defaultMessage: 'Filter by', + } +); + +export const FILTER_REMOVE_SCREENREADER_LABEL = i18n.translate( + 'xpack.uptime.synthetics.waterfall.filterGroup.removeFilterScreenReaderLabel', + { + defaultMessage: 'Remove filter by', + } +); + +export const FILTER_POPOVER_OPEN_LABEL = i18n.translate( + 'xpack.uptime.pingList.synthetics.waterfall.filters.popover', + { + defaultMessage: 'Click to open waterfall filters', + } +); + +export const FILTER_COLLAPSE_REQUESTS_LABEL = i18n.translate( + 'xpack.uptime.pingList.synthetics.waterfall.filters.collapseRequestsLabel', + { + defaultMessage: 'Collapse to only show matching requests', + } +); + +export const SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL = i18n.translate( + 'xpack.uptime.synthetics.waterfall.sidebar.filterMatchesScreenReaderLabel', + { + defaultMessage: 'Resource matches filter', + } +); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx index 1ce46fc0d6e7b5..a963fb1e2939c7 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx @@ -10,9 +10,14 @@ import { renderHook } from '@testing-library/react-hooks'; import { IWaterfallContext } from '../context/waterfall_chart'; import { CANVAS_MAX_ITEMS } from './constants'; -const generateTestData = (): IWaterfallContext['data'] => { +const generateTestData = ( + { + xMultiplier, + }: { + xMultiplier: number; + } = { xMultiplier: 1 } +): IWaterfallContext['data'] => { const numberOfItems = 1000; - const data: IWaterfallContext['data'] = []; const testItem = { x: 0, @@ -29,11 +34,11 @@ const generateTestData = (): IWaterfallContext['data'] => { data.push( { ...testItem, - x: i, + x: xMultiplier * i, }, { ...testItem, - x: i, + x: xMultiplier * i, y0: 7, y: 25, } @@ -44,7 +49,7 @@ const generateTestData = (): IWaterfallContext['data'] => { }; describe('useBarChartsHooks', () => { - it('returns result as expected', () => { + it('returns result as expected for non filtered data', () => { const { result, rerender } = renderHook((props) => useBarCharts(props), { initialProps: { data: [] as IWaterfallContext['data'] }, }); @@ -70,4 +75,35 @@ describe('useBarChartsHooks', () => { expect(lastChartItems[0].x).toBe(CANVAS_MAX_ITEMS * 4); expect(lastChartItems[lastChartItems.length - 1].x).toBe(CANVAS_MAX_ITEMS * 5 - 1); }); + + it('returns result as expected for filtered data', () => { + /* multiply x values to simulate filtered data, where x values can have gaps in the + * sequential order */ + const xMultiplier = 2; + const { result, rerender } = renderHook((props) => useBarCharts(props), { + initialProps: { data: [] as IWaterfallContext['data'] }, + }); + + expect(result.current).toHaveLength(0); + const newData = generateTestData({ xMultiplier }); + + rerender({ data: newData }); + + // Thousands items will result in 7 Canvas + expect(result.current.length).toBe(7); + + const firstChartItems = result.current[0]; + const lastChartItems = result.current[4]; + + // first chart items last item should be x 149, since we only display 150 items + expect(firstChartItems[firstChartItems.length - 1].x).toBe( + (CANVAS_MAX_ITEMS - 1) * xMultiplier + ); + + // since here are 5 charts, last chart first item should be x 600 + expect(lastChartItems[0].x).toBe(CANVAS_MAX_ITEMS * 4 * xMultiplier); + expect(lastChartItems[lastChartItems.length - 1].x).toBe( + (CANVAS_MAX_ITEMS * 5 - 1) * xMultiplier + ); + }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts index 79fd437039afed..2baf8955049113 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts @@ -13,27 +13,36 @@ export interface UseBarHookProps { data: IWaterfallContext['data']; } -export const useBarCharts = ({ data = [] }: UseBarHookProps) => { +export const useBarCharts = ({ data }: UseBarHookProps) => { const [charts, setCharts] = useState>([]); useEffect(() => { - if (data.length > 0) { - let chartIndex = 0; - - const chartsN: Array = []; + const chartsN: Array = []; + if (data?.length > 0) { + let chartIndex = 0; + /* We want at most CANVAS_MAX_ITEMS **RESOURCES** per array. + * Resources !== individual timing items, but are comprised of many individual timing + * items. The X value of each item can be used as an id for the resource. + * We must keep track of the number of unique resources added to the each array. */ + const uniqueResources = new Set(); + let lastIndex: number; data.forEach((item) => { - // Subtract 1 to account for x value starting from 0 - if (item.x === CANVAS_MAX_ITEMS * chartIndex && !chartsN[item.x / CANVAS_MAX_ITEMS]) { - chartsN.push([item]); + if (uniqueResources.size === CANVAS_MAX_ITEMS && item.x > lastIndex) { chartIndex++; + uniqueResources.clear(); + } + uniqueResources.add(item.x); + lastIndex = item.x; + if (!chartsN[chartIndex]) { + chartsN.push([item]); return; } - chartsN[chartIndex - 1].push(item); + chartsN[chartIndex].push(item); }); - - setCharts(chartsN); } + + setCharts(chartsN); }, [data]); return charts; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx index 7c9051e8f6acfe..528d749f576fce 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx @@ -6,64 +6,38 @@ */ import React from 'react'; -import { of } from 'rxjs'; -import { MountWithReduxProvider, mountWithRouter } from '../../../../../lib'; -import { KibanaContextProvider } from '../../../../../../../../../src/plugins/kibana_react/public'; import { WaterfallChart } from './waterfall_chart'; -import { - renderLegendItem, - renderSidebarItem, -} from '../../step_detail/waterfall/waterfall_chart_wrapper'; -import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common'; -import { WaterfallChartOuterContainer } from './styles'; +import { renderLegendItem } from '../../step_detail/waterfall/waterfall_chart_wrapper'; +import { render } from '../../../../../lib/helper/rtl_helpers'; + +import 'jest-canvas-mock'; describe('waterfall', () => { it('sets the correct height in case of full height', () => { - const core = mockCore(); - const Component = () => { return ( - `${Number(d).toFixed(0)} ms`} - domain={{ - max: 3371, - min: 0, - }} - barStyleAccessor={(datum) => { - return datum.datum.config.colour; - }} - renderSidebarItem={renderSidebarItem} - renderLegendItem={renderLegendItem} - fullHeight={true} - /> +
+ `${Number(d).toFixed(0)} ms`} + domain={{ + max: 3371, + min: 0, + }} + barStyleAccessor={(datum) => { + return datum.datum.config.colour; + }} + renderSidebarItem={undefined} + renderLegendItem={renderLegendItem} + fullHeight={true} + /> +
); }; - const component = mountWithRouter( - - - - - - - - ); + const { getByTestId } = render(); - const chartWrapper = component.find(WaterfallChartOuterContainer); + const chartWrapper = getByTestId('waterfallOuterContainer'); - expect(chartWrapper.get(0).props.height).toBe('calc(100vh - 0px)'); + expect(chartWrapper).toHaveStyleRule('height', 'calc(100vh - 62px)'); }); }); - -const mockCore: () => any = () => { - return { - application: { - getUrlForApp: () => '/app/uptime', - navigateToUrl: jest.fn(), - }, - uiSettings: { - get: (key: string) => 'MMM D, YYYY @ HH:mm:ss.SSS', - get$: (key: string) => of('MMM D, YYYY @ HH:mm:ss.SSS'), - }, - }; -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx new file mode 100644 index 00000000000000..df00df147fc6c5 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + Axis, + BarSeries, + BarStyleAccessor, + Chart, + DomainRange, + Position, + ScaleType, + Settings, + TickFormatter, + TooltipInfo, +} from '@elastic/charts'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { BAR_HEIGHT } from './constants'; +import { useChartTheme } from '../../../../../hooks/use_chart_theme'; +import { WaterfallChartChartContainer, WaterfallChartTooltip } from './styles'; +import { useWaterfallContext, WaterfallData } from '..'; + +const getChartHeight = (data: WaterfallData): number => { + // We get the last item x(number of bars) and adds 1 to cater for 0 index + const noOfXBars = new Set(data.map((item) => item.x)).size; + + return noOfXBars * BAR_HEIGHT; +}; + +const Tooltip = (tooltipInfo: TooltipInfo) => { + const { data, renderTooltipItem } = useWaterfallContext(); + const relevantItems = data.filter((item) => { + return ( + item.x === tooltipInfo.header?.value && item.config.showTooltip && item.config.tooltipProps + ); + }); + return relevantItems.length ? ( + + + {relevantItems.map((item, index) => { + return ( + {renderTooltipItem(item.config.tooltipProps)} + ); + })} + + + ) : null; +}; + +interface Props { + index: number; + chartData: WaterfallData; + tickFormat: TickFormatter; + domain: DomainRange; + barStyleAccessor: BarStyleAccessor; +} + +export const WaterfallBarChart = ({ + chartData, + tickFormat, + domain, + barStyleAccessor, + index, +}: Props) => { + const theme = useChartTheme(); + + return ( + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx index 8f831d0629b25c..e0e5165b41e498 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx @@ -5,62 +5,30 @@ * 2.0. */ -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { - Axis, - BarSeries, - Chart, - Position, - ScaleType, - Settings, - TickFormatter, - DomainRange, - BarStyleAccessor, - TooltipInfo, - TooltipType, -} from '@elastic/charts'; -import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; -// NOTE: The WaterfallChart has a hard requirement that consumers / solutions are making use of KibanaReactContext, and useKibana etc -// can therefore be accessed. -import { useUiSetting$ } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { TickFormatter, DomainRange, BarStyleAccessor } from '@elastic/charts'; + import { useWaterfallContext } from '../context/waterfall_chart'; import { WaterfallChartOuterContainer, WaterfallChartFixedTopContainer, WaterfallChartFixedTopContainerSidebarCover, - WaterfallChartFixedAxisContainer, - WaterfallChartChartContainer, - WaterfallChartTooltip, + WaterfallChartTopContainer, + RelativeContainer, + WaterfallChartFilterContainer, + WaterfallChartAxisOnlyContainer, } from './styles'; -import { WaterfallData } from '../types'; -import { BAR_HEIGHT, CANVAS_MAX_ITEMS, MAIN_GROW_SIZE, SIDEBAR_GROW_SIZE } from './constants'; +import { CHART_LEGEND_PADDING, MAIN_GROW_SIZE, SIDEBAR_GROW_SIZE } from './constants'; import { Sidebar } from './sidebar'; import { Legend } from './legend'; import { useBarCharts } from './use_bar_charts'; +import { WaterfallBarChart } from './waterfall_bar_chart'; +import { WaterfallChartFixedAxis } from './waterfall_chart_fixed_axis'; import { NetworkRequestsTotal } from './network_requests_total'; -const Tooltip = (tooltipInfo: TooltipInfo) => { - const { data, renderTooltipItem } = useWaterfallContext(); - const relevantItems = data.filter((item) => { - return ( - item.x === tooltipInfo.header?.value && item.config.showTooltip && item.config.tooltipProps - ); - }); - return relevantItems.length ? ( - - - {relevantItems.map((item, index) => { - return ( - {renderTooltipItem(item.config.tooltipProps)} - ); - })} - - - ) : null; -}; - -export type RenderItem = (item: I, index: number) => JSX.Element; +export type RenderItem = (item: I, index?: number) => JSX.Element; +export type RenderFilter = () => JSX.Element; export interface WaterfallChartProps { tickFormat: TickFormatter; @@ -68,159 +36,100 @@ export interface WaterfallChartProps { barStyleAccessor: BarStyleAccessor; renderSidebarItem?: RenderItem; renderLegendItem?: RenderItem; + renderFilter?: RenderFilter; maxHeight?: string; fullHeight?: boolean; } -const getChartHeight = (data: WaterfallData, ind: number): number => { - // We get the last item x(number of bars) and adds 1 to cater for 0 index - return (data[data.length - 1]?.x + 1 - ind * CANVAS_MAX_ITEMS) * BAR_HEIGHT; -}; - export const WaterfallChart = ({ tickFormat, domain, barStyleAccessor, renderSidebarItem, renderLegendItem, + renderFilter, maxHeight = '800px', fullHeight = false, }: WaterfallChartProps) => { const { data, + showOnlyHighlightedNetworkRequests, sidebarItems, legendItems, totalNetworkRequests, + highlightedNetworkRequests, fetchedNetworkRequests, } = useWaterfallContext(); - const [darkMode] = useUiSetting$('theme:darkMode'); - - const theme = useMemo(() => { - return darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; - }, [darkMode]); - const chartWrapperDivRef = useRef(null); const [height, setHeight] = useState(maxHeight); - const shouldRenderSidebar = !!(sidebarItems && sidebarItems.length > 0 && renderSidebarItem); + const shouldRenderSidebar = !!(sidebarItems && renderSidebarItem); const shouldRenderLegend = !!(legendItems && legendItems.length > 0 && renderLegendItem); useEffect(() => { if (fullHeight && chartWrapperDivRef.current) { const chartOffset = chartWrapperDivRef.current.getBoundingClientRect().top; - setHeight(`calc(100vh - ${chartOffset}px)`); + setHeight(`calc(100vh - ${chartOffset + CHART_LEGEND_PADDING}px)`); } }, [chartWrapperDivRef, fullHeight]); const chartsToDisplay = useBarCharts({ data }); return ( - - <> - - - {shouldRenderSidebar && ( - - - - - - )} - - - - - - - - - - + + + + {shouldRenderSidebar && ( + + + + {renderFilter && ( + {renderFilter()} + )} - - - + )} + + + + + + + + {shouldRenderSidebar && } - + + {chartsToDisplay.map((chartData, ind) => ( - - - - - - - - - + chartData={chartData} + domain={domain} + barStyleAccessor={barStyleAccessor} + tickFormat={tickFormat} + /> ))} - + - {shouldRenderLegend && } - - + + {shouldRenderLegend && } + ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx new file mode 100644 index 00000000000000..3a7ab421b6277b --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + Axis, + BarSeries, + BarStyleAccessor, + Chart, + DomainRange, + Position, + ScaleType, + Settings, + TickFormatter, + TooltipType, +} from '@elastic/charts'; +import { useChartTheme } from '../../../../../hooks/use_chart_theme'; +import { WaterfallChartFixedAxisContainer } from './styles'; + +interface Props { + tickFormat: TickFormatter; + domain: DomainRange; + barStyleAccessor: BarStyleAccessor; +} + +export const WaterfallChartFixedAxis = ({ tickFormat, domain, barStyleAccessor }: Props) => { + const theme = useChartTheme(); + + return ( + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx index 68d24514a37d3e..9e87d69ce38a82 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx @@ -7,12 +7,15 @@ import React, { createContext, useContext, Context } from 'react'; import { WaterfallData, WaterfallDataEntry } from '../types'; +import { SidebarItems } from '../../step_detail/waterfall/types'; export interface IWaterfallContext { totalNetworkRequests: number; + highlightedNetworkRequests: number; fetchedNetworkRequests: number; data: WaterfallData; - sidebarItems?: unknown[]; + showOnlyHighlightedNetworkRequests: boolean; + sidebarItems?: SidebarItems; legendItems?: unknown[]; renderTooltipItem: ( item: WaterfallDataEntry['config']['tooltipProps'], @@ -24,8 +27,10 @@ export const WaterfallContext = createContext>({}); interface ProviderProps { totalNetworkRequests: number; + highlightedNetworkRequests: number; fetchedNetworkRequests: number; data: IWaterfallContext['data']; + showOnlyHighlightedNetworkRequests: IWaterfallContext['showOnlyHighlightedNetworkRequests']; sidebarItems?: IWaterfallContext['sidebarItems']; legendItems?: IWaterfallContext['legendItems']; renderTooltipItem: IWaterfallContext['renderTooltipItem']; @@ -34,20 +39,24 @@ interface ProviderProps { export const WaterfallProvider: React.FC = ({ children, data, + showOnlyHighlightedNetworkRequests, sidebarItems, legendItems, renderTooltipItem, totalNetworkRequests, + highlightedNetworkRequests, fetchedNetworkRequests, }) => { return ( diff --git a/x-pack/plugins/uptime/public/hooks/use_chart_theme.ts b/x-pack/plugins/uptime/public/hooks/use_chart_theme.ts new file mode 100644 index 00000000000000..f9231abaa75a80 --- /dev/null +++ b/x-pack/plugins/uptime/public/hooks/use_chart_theme.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; +import { useMemo } from 'react'; +import { useUiSetting$ } from '../../../../../src/plugins/kibana_react/public'; + +export const useChartTheme = () => { + const [darkMode] = useUiSetting$('theme:darkMode'); + + const theme = useMemo(() => { + return darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; + }, [darkMode]); + + return theme; +}; diff --git a/x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx index 9656c63274a13e..4c81247fb2cf16 100644 --- a/x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx @@ -8,10 +8,17 @@ import React, { ReactElement } from 'react'; import { Router } from 'react-router-dom'; import { MemoryHistory } from 'history/createMemoryHistory'; -import { createMemoryHistory } from 'history'; +import { createMemoryHistory, History } from 'history'; import { mountWithIntl, renderWithIntl, shallowWithIntl } from '@kbn/test/jest'; import { MountWithReduxProvider } from './helper_with_redux'; import { AppState } from '../../state'; +import { mockState } from '../__mocks__/uptime_store.mock'; +import { KibanaProviderOptions, MockRouter } from './rtl_helpers'; + +interface RenderRouterOptions extends KibanaProviderOptions { + history?: History; + state?: Partial; +} const helperWithRouter: ( helper: (node: ReactElement) => R, @@ -67,3 +74,39 @@ export const mountWithRouterRedux = ( options?.storeState ); }; + +/* Custom enzyme render */ +export function render( + ui: ReactElement, + { history, core, kibanaProps, state }: RenderRouterOptions = {} +) { + const testState: AppState = { + ...mockState, + ...state, + }; + return renderWithIntl( + + + {ui} + + + ); +} + +/* Custom enzyme render */ +export function mount( + ui: ReactElement, + { history, core, kibanaProps, state }: RenderRouterOptions = {} +) { + const testState: AppState = { + ...mockState, + ...state, + }; + return mountWithIntl( + + + {ui} + + + ); +} diff --git a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx index abc0451bf8efad..e02a2c6f9832f6 100644 --- a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx @@ -6,6 +6,7 @@ */ import React, { ReactElement } from 'react'; +import { of } from 'rxjs'; import { render as reactTestLibRender, RenderOptions } from '@testing-library/react'; import { Router } from 'react-router-dom'; import { createMemoryHistory, History } from 'history'; @@ -26,7 +27,7 @@ interface KibanaProps { services?: KibanaServices; } -interface KibanaProviderOptions { +export interface KibanaProviderOptions { core?: Partial & ExtraCore; kibanaProps?: KibanaProps; } @@ -54,6 +55,11 @@ const mockCore: () => any = () => { getUrlForApp: () => '/app/uptime', navigateToUrl: jest.fn(), }, + uiSettings: { + get: (key: string) => 'MMM D, YYYY @ HH:mm:ss.SSS', + get$: (key: string) => of('MMM D, YYYY @ HH:mm:ss.SSS'), + }, + usageCollection: { reportUiCounter: () => {} }, }; return core; From 31a3ec5934b0eb566d66797d43518933599fbfdc Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Mon, 8 Feb 2021 15:31:09 -0500 Subject: [PATCH 40/51] [Time To Visualize] Make State Transfer App Specific (#89804) * made state transfer app specific --- ...mbeddablestatetransfer.cleareditorstate.md | 11 ++- ...blestatetransfer.getincomingeditorstate.md | 5 +- ...tetransfer.getincomingembeddablepackage.md | 5 +- ...beddable-public.embeddablestatetransfer.md | 6 +- .../hooks/use_dashboard_container.ts | 6 +- .../embeddable_state_transfer.test.ts | 98 ++++++++++++++++--- .../embeddable_state_transfer.ts | 39 ++++++-- src/plugins/embeddable/public/public.api.md | 7 +- .../components/visualize_byvalue_editor.tsx | 2 +- .../components/visualize_editor.tsx | 4 +- .../components/visualize_listing.tsx | 2 +- .../application/utils/get_top_nav_config.tsx | 2 +- .../public/application/visualize_constants.ts | 1 + src/plugins/visualize/public/plugin.ts | 6 +- x-pack/plugins/lens/common/constants.ts | 1 + x-pack/plugins/lens/public/app_plugin/app.tsx | 4 +- .../lens/public/app_plugin/mounter.tsx | 4 +- x-pack/plugins/lens/public/plugin.ts | 4 +- x-pack/plugins/maps/public/render_app.tsx | 3 +- .../routes/list_page/load_list_and_render.tsx | 4 +- .../routes/map_page/saved_map/saved_map.ts | 4 +- 21 files changed, 162 insertions(+), 56 deletions(-) diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md index 5c1a6a0393c2e6..034f9c70e389fe 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md @@ -4,11 +4,20 @@ ## EmbeddableStateTransfer.clearEditorState() method +Clears the [editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) from the sessionStorage for the provided app id + Signature: ```typescript -clearEditorState(): void; +clearEditorState(appId: string): void; ``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| appId | string | The app to fetch incomingEditorState for | + Returns: `void` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md index 1434de2c9870e0..cd261bff5905b1 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md @@ -4,18 +4,19 @@ ## EmbeddableStateTransfer.getIncomingEditorState() method -Fetches an [originating app](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) argument from the sessionStorage +Fetches an [editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) from the sessionStorage for the provided app id Signature: ```typescript -getIncomingEditorState(removeAfterFetch?: boolean): EmbeddableEditorState | undefined; +getIncomingEditorState(appId: string, removeAfterFetch?: boolean): EmbeddableEditorState | undefined; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | +| appId | string | The app to fetch incomingEditorState for | | removeAfterFetch | boolean | Whether to remove the package state after fetch to prevent duplicates. | Returns: diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md index 9ead71f0bb22c2..47873c8e91e413 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md @@ -4,18 +4,19 @@ ## EmbeddableStateTransfer.getIncomingEmbeddablePackage() method -Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) argument from the sessionStorage +Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) from the sessionStorage for the given AppId Signature: ```typescript -getIncomingEmbeddablePackage(removeAfterFetch?: boolean): EmbeddablePackageState | undefined; +getIncomingEmbeddablePackage(appId: string, removeAfterFetch?: boolean): EmbeddablePackageState | undefined; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | +| appId | string | The app to fetch EmbeddablePackageState for | | removeAfterFetch | boolean | Whether to remove the package state after fetch to prevent duplicates. | Returns: diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.md index 76b6708b93bd12..13c6c8c0325f1e 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.md @@ -29,9 +29,9 @@ export declare class EmbeddableStateTransfer | Method | Modifiers | Description | | --- | --- | --- | -| [clearEditorState()](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md) | | | -| [getIncomingEditorState(removeAfterFetch)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md) | | Fetches an [originating app](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) argument from the sessionStorage | -| [getIncomingEmbeddablePackage(removeAfterFetch)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md) | | Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) argument from the sessionStorage | +| [clearEditorState(appId)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md) | | Clears the [editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) from the sessionStorage for the provided app id | +| [getIncomingEditorState(appId, removeAfterFetch)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md) | | Fetches an [editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) from the sessionStorage for the provided app id | +| [getIncomingEmbeddablePackage(appId, removeAfterFetch)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md) | | Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) from the sessionStorage for the given AppId | | [navigateToEditor(appId, options)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.navigatetoeditor.md) | | A wrapper around the method which navigates to the specified appId with [embeddable editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) | | [navigateToWithEmbeddablePackage(appId, options)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.navigatetowithembeddablepackage.md) | | A wrapper around the method which navigates to the specified appId with [embeddable package state](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) | diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts index b27322b6bec534..d12fea07bdd418 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts @@ -21,7 +21,7 @@ import { import { DashboardStateManager } from '../dashboard_state_manager'; import { getDashboardContainerInput, getSearchSessionIdFromURL } from '../dashboard_app_functions'; -import { DashboardContainer, DashboardContainerInput } from '../..'; +import { DashboardConstants, DashboardContainer, DashboardContainerInput } from '../..'; import { DashboardAppServices } from '../types'; import { DASHBOARD_CONTAINER_TYPE } from '..'; @@ -68,7 +68,9 @@ export const useDashboardContainer = ( searchSession.restore(searchSessionIdFromURL); } - const incomingEmbeddable = embeddable.getStateTransfer().getIncomingEmbeddablePackage(true); + const incomingEmbeddable = embeddable + .getStateTransfer() + .getIncomingEmbeddablePackage(DashboardConstants.DASHBOARDS_ID, true); let canceled = false; let pendingContainer: DashboardContainer | ErrorEmbeddable | null | undefined; diff --git a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts index 763186fc17c0cd..a8ecb384f782b4 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts @@ -42,6 +42,10 @@ describe('embeddable state transfer', () => { const destinationApp = 'superUltraVisualize'; const originatingApp = 'superUltraTestDashboard'; + const testAppId = 'testApp'; + + const buildKey = (appId: string, key: string) => `${appId}-${key}`; + beforeEach(() => { currentAppId$ = new Subject(); currentAppId$.next(originatingApp); @@ -82,7 +86,9 @@ describe('embeddable state transfer', () => { it('can send an outgoing editor state', async () => { await stateTransfer.navigateToEditor(destinationApp, { state: { originatingApp } }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superUltraTestDashboard' }, + [buildKey(destinationApp, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superUltraTestDashboard', + }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { path: undefined, @@ -98,7 +104,9 @@ describe('embeddable state transfer', () => { }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { kibanaIsNowForSports: 'extremeSportsKibana', - [EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superUltraTestDashboard' }, + [buildKey(destinationApp, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superUltraTestDashboard', + }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { path: undefined, @@ -117,7 +125,10 @@ describe('embeddable state transfer', () => { state: { type: 'coolestType', input: { savedObjectId: '150' } }, }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'coolestType', input: { savedObjectId: '150' } }, + [buildKey(destinationApp, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'coolestType', + input: { savedObjectId: '150' }, + }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { path: undefined, @@ -133,7 +144,10 @@ describe('embeddable state transfer', () => { }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { kibanaIsNowForSports: 'extremeSportsKibana', - [EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'coolestType', input: { savedObjectId: '150' } }, + [buildKey(destinationApp, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'coolestType', + input: { savedObjectId: '150' }, + }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { path: undefined, @@ -151,42 +165,92 @@ describe('embeddable state transfer', () => { it('can fetch an incoming editor state', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superUltraTestDashboard' }, + [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superUltraTestDashboard', + }, + }); + const fetchedState = stateTransfer.getIncomingEditorState(testAppId); + expect(fetchedState).toEqual({ originatingApp: 'superUltraTestDashboard' }); + }); + + it('can fetch an incoming editor state and ignore state for other apps', async () => { + store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { + [buildKey('otherApp1', EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'whoops not me', + }, + [buildKey('otherApp2', EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'otherTestDashboard', + }, + [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superUltraTestDashboard', + }, }); - const fetchedState = stateTransfer.getIncomingEditorState(); + const fetchedState = stateTransfer.getIncomingEditorState(testAppId); expect(fetchedState).toEqual({ originatingApp: 'superUltraTestDashboard' }); + + const fetchedState2 = stateTransfer.getIncomingEditorState('otherApp2'); + expect(fetchedState2).toEqual({ originatingApp: 'otherTestDashboard' }); }); it('incoming editor state returns undefined when state is not in the right shape', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_EDITOR_STATE_KEY]: { helloSportsKibana: 'superUltraTestDashboard' }, + [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { + helloSportsKibana: 'superUltraTestDashboard', + }, }); - const fetchedState = stateTransfer.getIncomingEditorState(); + const fetchedState = stateTransfer.getIncomingEditorState(testAppId); expect(fetchedState).toBeUndefined(); }); it('can fetch an incoming embeddable package state', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'skisEmbeddable', input: { savedObjectId: '123' } }, + [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'skisEmbeddable', + input: { savedObjectId: '123' }, + }, }); - const fetchedState = stateTransfer.getIncomingEmbeddablePackage(); + const fetchedState = stateTransfer.getIncomingEmbeddablePackage(testAppId); expect(fetchedState).toEqual({ type: 'skisEmbeddable', input: { savedObjectId: '123' } }); }); + it('can fetch an incoming embeddable package state and ignore state for other apps', async () => { + store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { + [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'skisEmbeddable', + input: { savedObjectId: '123' }, + }, + [buildKey('testApp2', EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'crossCountryEmbeddable', + input: { savedObjectId: '456' }, + }, + }); + const fetchedState = stateTransfer.getIncomingEmbeddablePackage(testAppId); + expect(fetchedState).toEqual({ type: 'skisEmbeddable', input: { savedObjectId: '123' } }); + + const fetchedState2 = stateTransfer.getIncomingEmbeddablePackage('testApp2'); + expect(fetchedState2).toEqual({ + type: 'crossCountryEmbeddable', + input: { savedObjectId: '456' }, + }); + }); + it('embeddable package state returns undefined when state is not in the right shape', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_PACKAGE_STATE_KEY]: { kibanaIsFor: 'sports' }, + [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { kibanaIsFor: 'sports' }, }); - const fetchedState = stateTransfer.getIncomingEmbeddablePackage(); + const fetchedState = stateTransfer.getIncomingEmbeddablePackage(testAppId); expect(fetchedState).toBeUndefined(); }); it('removes embeddable package key when removeAfterFetch is true', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'coolestType', input: { savedObjectId: '150' } }, + [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'coolestType', + input: { savedObjectId: '150' }, + }, iSHouldStillbeHere: 'doing the sports thing', }); - stateTransfer.getIncomingEmbeddablePackage(true); + stateTransfer.getIncomingEmbeddablePackage(testAppId, true); expect(store.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)).toEqual({ iSHouldStillbeHere: 'doing the sports thing', }); @@ -194,10 +258,12 @@ describe('embeddable state transfer', () => { it('removes editor state key when removeAfterFetch is true', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superCoolFootballDashboard' }, + [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superCoolFootballDashboard', + }, iSHouldStillbeHere: 'doing the sports thing', }); - stateTransfer.getIncomingEditorState(true); + stateTransfer.getIncomingEditorState(testAppId, true); expect(store.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)).toEqual({ iSHouldStillbeHere: 'doing the sports thing', }); diff --git a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts index d3b1c1c76aadfe..8664a5aae7345f 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts @@ -50,13 +50,18 @@ export class EmbeddableStateTransfer { public getAppNameFromId = (appId: string): string | undefined => this.appList?.get(appId)?.title; /** - * Fetches an {@link EmbeddableEditorState | originating app} argument from the sessionStorage + * Fetches an {@link EmbeddableEditorState | editor state} from the sessionStorage for the provided app id * + * @param appId - The app to fetch incomingEditorState for * @param removeAfterFetch - Whether to remove the package state after fetch to prevent duplicates. */ - public getIncomingEditorState(removeAfterFetch?: boolean): EmbeddableEditorState | undefined { + public getIncomingEditorState( + appId: string, + removeAfterFetch?: boolean + ): EmbeddableEditorState | undefined { return this.getIncomingState( isEmbeddableEditorState, + appId, EMBEDDABLE_EDITOR_STATE_KEY, { keysToRemoveAfterFetch: removeAfterFetch ? [EMBEDDABLE_EDITOR_STATE_KEY] : undefined, @@ -64,24 +69,33 @@ export class EmbeddableStateTransfer { ); } - public clearEditorState() { + /** + * Clears the {@link EmbeddableEditorState | editor state} from the sessionStorage for the provided app id + * + * @param appId - The app to fetch incomingEditorState for + * @param removeAfterFetch - Whether to remove the package state after fetch to prevent duplicates. + */ + public clearEditorState(appId: string) { const currentState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY); if (currentState) { - delete currentState[EMBEDDABLE_EDITOR_STATE_KEY]; + delete currentState[this.buildKey(appId, EMBEDDABLE_EDITOR_STATE_KEY)]; this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, currentState); } } /** - * Fetches an {@link EmbeddablePackageState | embeddable package} argument from the sessionStorage + * Fetches an {@link EmbeddablePackageState | embeddable package} from the sessionStorage for the given AppId * + * @param appId - The app to fetch EmbeddablePackageState for * @param removeAfterFetch - Whether to remove the package state after fetch to prevent duplicates. */ public getIncomingEmbeddablePackage( + appId: string, removeAfterFetch?: boolean ): EmbeddablePackageState | undefined { return this.getIncomingState( isEmbeddablePackageState, + appId, EMBEDDABLE_PACKAGE_STATE_KEY, { keysToRemoveAfterFetch: removeAfterFetch ? [EMBEDDABLE_PACKAGE_STATE_KEY] : undefined, @@ -122,20 +136,27 @@ export class EmbeddableStateTransfer { }); } + private buildKey(appId: string, key: string) { + return `${appId}-${key}`; + } + private getIncomingState( guard: (state: unknown) => state is IncomingStateType, + appId: string, key: string, options?: { keysToRemoveAfterFetch?: string[]; } ): IncomingStateType | undefined { - const incomingState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)?.[key]; + const incomingState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)?.[ + this.buildKey(appId, key) + ]; const castState = !guard || guard(incomingState) ? (cloneDeep(incomingState) as IncomingStateType) : undefined; if (castState && options?.keysToRemoveAfterFetch) { const stateReplace = { ...this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY) }; options.keysToRemoveAfterFetch.forEach((keyToRemove: string) => { - delete stateReplace[keyToRemove]; + delete stateReplace[this.buildKey(appId, keyToRemove)]; }); this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, stateReplace); } @@ -150,9 +171,9 @@ export class EmbeddableStateTransfer { const stateObject = options?.appendToExistingState ? { ...this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY), - [key]: options.state, + [this.buildKey(appId, key)]: options.state, } - : { [key]: options?.state }; + : { [this.buildKey(appId, key)]: options?.state }; this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, stateObject); await this.navigateToApp(appId, { path: options?.path }); } diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 2f9b43121b45a4..3e7014d54958de 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -590,11 +590,10 @@ export class EmbeddableStateTransfer { // Warning: (ae-forgotten-export) The symbol "ApplicationStart" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "PublicAppInfo" needs to be exported by the entry point index.d.ts constructor(navigateToApp: ApplicationStart['navigateToApp'], currentAppId$: ApplicationStart['currentAppId$'], appList?: ReadonlyMap | undefined, customStorage?: Storage); - // (undocumented) - clearEditorState(): void; + clearEditorState(appId: string): void; getAppNameFromId: (appId: string) => string | undefined; - getIncomingEditorState(removeAfterFetch?: boolean): EmbeddableEditorState | undefined; - getIncomingEmbeddablePackage(removeAfterFetch?: boolean): EmbeddablePackageState | undefined; + getIncomingEditorState(appId: string, removeAfterFetch?: boolean): EmbeddableEditorState | undefined; + getIncomingEmbeddablePackage(appId: string, removeAfterFetch?: boolean): EmbeddablePackageState | undefined; // (undocumented) isTransferInProgress: boolean; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ApplicationStart" diff --git a/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx index 6ca6efaa897970..fa0e0bd5f48f08 100644 --- a/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx +++ b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx @@ -34,7 +34,7 @@ export const VisualizeByValueEditor = ({ onAppLeave }: VisualizeAppProps) => { useEffect(() => { const { originatingApp: value, embeddableId: embeddableIdValue, valueInput: valueInputValue } = - services.stateTransferService.getIncomingEditorState() || {}; + services.stateTransferService.getIncomingEditorState(VisualizeConstants.APP_ID) || {}; setOriginatingApp(value); setValueInput(valueInputValue); setEmbeddableId(embeddableIdValue); diff --git a/src/plugins/visualize/public/application/components/visualize_editor.tsx b/src/plugins/visualize/public/application/components/visualize_editor.tsx index 7465e7eaa90441..c6333e978183ff 100644 --- a/src/plugins/visualize/public/application/components/visualize_editor.tsx +++ b/src/plugins/visualize/public/application/components/visualize_editor.tsx @@ -22,6 +22,7 @@ import { import { VisualizeServices } from '../types'; import { VisualizeEditorCommon } from './visualize_editor_common'; import { VisualizeAppProps } from '../app'; +import { VisualizeConstants } from '../..'; export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { const { id: visualizationIdFromUrl } = useParams<{ id: string }>(); @@ -54,7 +55,8 @@ export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { useLinkedSearchUpdates(services, eventEmitter, appState, savedVisInstance); useEffect(() => { - const { originatingApp: value } = services.stateTransferService.getIncomingEditorState() || {}; + const { originatingApp: value } = + services.stateTransferService.getIncomingEditorState(VisualizeConstants.APP_ID) || {}; setOriginatingApp(value); }, [services]); diff --git a/src/plugins/visualize/public/application/components/visualize_listing.tsx b/src/plugins/visualize/public/application/components/visualize_listing.tsx index c772554344cb26..bc766d63db5a78 100644 --- a/src/plugins/visualize/public/application/components/visualize_listing.tsx +++ b/src/plugins/visualize/public/application/components/visualize_listing.tsx @@ -65,7 +65,7 @@ export const VisualizeListing = () => { useMount(() => { // Reset editor state if the visualize listing page is loaded. - stateTransferService.clearEditorState(); + stateTransferService.clearEditorState(VisualizeConstants.APP_ID); chrome.setBreadcrumbs([ { text: i18n.translate('visualize.visualizeListingBreadcrumbsTitle', { diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index 9ea42e8b565597..e8c3289d4ce411 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -142,7 +142,7 @@ export const getTopNavConfig = ( if (setOriginatingApp && originatingApp && newlyCreated) { setOriginatingApp(undefined); // remove editor state so the connection is still broken after reload - stateTransfer.clearEditorState(); + stateTransfer.clearEditorState(VisualizeConstants.APP_ID); } chrome.docTitle.change(savedVis.lastSavedTitle); chrome.setBreadcrumbs(getEditBreadcrumbs({}, savedVis.lastSavedTitle)); diff --git a/src/plugins/visualize/public/application/visualize_constants.ts b/src/plugins/visualize/public/application/visualize_constants.ts index 7dbf5be77b74d7..6e901882a9365d 100644 --- a/src/plugins/visualize/public/application/visualize_constants.ts +++ b/src/plugins/visualize/public/application/visualize_constants.ts @@ -16,4 +16,5 @@ export const VisualizeConstants = { CREATE_PATH: '/create', EDIT_PATH: '/edit', EDIT_BY_VALUE_PATH: '/edit_by_value', + APP_ID: 'visualize', }; diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 3d82e6c60a1b6e..4eb2d6fd2a731c 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -132,7 +132,7 @@ export class VisualizePlugin setUISettings(core.uiSettings); core.application.register({ - id: 'visualize', + id: VisualizeConstants.APP_ID, title: 'Visualize', order: 8000, euiIconType: 'logoKibana', @@ -147,7 +147,9 @@ export class VisualizePlugin // allows the urlTracker to only save URLs that are not linked to an originatingApp this.isLinkedToOriginatingApp = () => { return Boolean( - pluginsStart.embeddable.getStateTransfer().getIncomingEditorState()?.originatingApp + pluginsStart.embeddable + .getStateTransfer() + .getIncomingEditorState(VisualizeConstants.APP_ID)?.originatingApp ); }; diff --git a/x-pack/plugins/lens/common/constants.ts b/x-pack/plugins/lens/common/constants.ts index 202b80d3d84060..c3e556b1678890 100644 --- a/x-pack/plugins/lens/common/constants.ts +++ b/x-pack/plugins/lens/common/constants.ts @@ -6,6 +6,7 @@ */ export const PLUGIN_ID = 'lens'; +export const APP_ID = 'lens'; export const LENS_EMBEDDABLE_TYPE = 'lens'; export const DOC_TYPE = 'lens'; export const NOT_INTERNATIONALIZED_PRODUCT_NAME = 'Lens Visualizations'; diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 7e95479887dbd1..0d72a366fa4119 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -38,7 +38,7 @@ import { SavedQuery, syncQueryStateWithUrl, } from '../../../../../src/plugins/data/public'; -import { LENS_EMBEDDABLE_TYPE, getFullPath } from '../../common'; +import { LENS_EMBEDDABLE_TYPE, getFullPath, APP_ID } from '../../common'; import { LensAppProps, LensAppServices, LensAppState } from './types'; import { getLensTopNavConfig } from './lens_top_nav'; import { Document } from '../persistence'; @@ -498,7 +498,7 @@ export function App({ isLinkedToOriginatingApp: false, })); // remove editor state so the connection is still broken after reload - stateTransfer.clearEditorState(); + stateTransfer.clearEditorState(APP_ID); redirectTo(newInput.savedObjectId); return; diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 1ff31e5d4bf6bf..5869151485a526 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -23,7 +23,7 @@ import { App } from './app'; import { EditorFrameStart } from '../types'; import { addHelpMenuToAppChrome } from '../help_menu_util'; import { LensPluginStartDependencies } from '../plugin'; -import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE } from '../../common'; +import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE, APP_ID } from '../../common'; import { LensEmbeddableInput, LensByReferenceInput, @@ -57,7 +57,7 @@ export async function mountApp( const storage = new Storage(localStorage); const stateTransfer = embeddable?.getStateTransfer(); const historyLocationState = params.history.location.state as HistoryLocationState; - const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(); + const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(APP_ID); const lensServices: LensAppServices = { data, diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 05da76d9fd2076..c667ddea06b331 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -40,7 +40,7 @@ import { ACTION_VISUALIZE_FIELD, VISUALIZE_FIELD_TRIGGER, } from '../../../../src/plugins/ui_actions/public'; -import { getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; +import { APP_ID, getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; import { EditorFrameStart } from './types'; import { getLensAliasConfig } from './vis_type_alias'; import { visualizeFieldAction } from './trigger_actions/visualize_field_actions'; @@ -182,7 +182,7 @@ export class LensPlugin { }; core.application.register({ - id: 'lens', + id: APP_ID, title: NOT_INTERNATIONALIZED_PRODUCT_NAME, navLinkStatus: AppNavLinkStatus.hidden, mount: async (params: AppMountParameters) => { diff --git a/x-pack/plugins/maps/public/render_app.tsx b/x-pack/plugins/maps/public/render_app.tsx index ccd30126b67bd5..4d1dff9303b0c5 100644 --- a/x-pack/plugins/maps/public/render_app.tsx +++ b/x-pack/plugins/maps/public/render_app.tsx @@ -26,6 +26,7 @@ import { } from '../../../../src/plugins/kibana_utils/public'; import { ListPage, MapPage } from './routes'; import { MapByValueInput, MapByReferenceInput } from './embeddable/types'; +import { APP_ID } from '../common/constants'; export let goToSpecifiedPath: (path: string) => void; export let kbnUrlStateStorage: IKbnUrlStateStorage; @@ -80,7 +81,7 @@ export async function renderApp({ function renderMapApp(routeProps: RouteComponentProps<{ savedMapId?: string }>) { const { embeddableId, originatingApp, valueInput } = - stateTransfer.getIncomingEditorState() || {}; + stateTransfer.getIncomingEditorState(APP_ID) || {}; let mapEmbeddableInput; if (routeProps.match.params.savedMapId) { diff --git a/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx b/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx index 66b65eb8d0a9d7..feafb34f6a7152 100644 --- a/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx +++ b/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { Redirect } from 'react-router-dom'; import { getSavedObjectsClient, getToasts } from '../../kibana_services'; import { MapsListView } from './maps_list_view'; -import { MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; +import { APP_ID, MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; import { EmbeddableStateTransfer } from '../../../../../../src/plugins/embeddable/public'; export class LoadListAndRender extends React.Component<{ stateTransfer: EmbeddableStateTransfer }> { @@ -22,7 +22,7 @@ export class LoadListAndRender extends React.Component<{ stateTransfer: Embeddab componentDidMount() { this._isMounted = true; - this.props.stateTransfer.clearEditorState(); + this.props.stateTransfer.clearEditorState(APP_ID); this._loadMapsList(); } diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts index d38ff8b3e4da61..b6ee5274f690d8 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts @@ -9,7 +9,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { EmbeddableStateTransfer } from 'src/plugins/embeddable/public'; import { MapSavedObjectAttributes } from '../../../../common/map_saved_object_type'; -import { MAP_PATH, MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; +import { APP_ID, MAP_PATH, MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; import { createMapStore, MapStore, MapStoreState } from '../../../reducers/store'; import { getTimeFilters, @@ -364,7 +364,7 @@ export class SavedMap { this._originatingApp = undefined; // remove editor state so the connection is still broken after reload - this._getStateTransfer().clearEditorState(); + this._getStateTransfer().clearEditorState(APP_ID); getToasts().addSuccess({ title: i18n.translate('xpack.maps.topNav.saveSuccessMessage', { From cde3cbafe4f4506c7444b4c40a0a33027a4740e9 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 8 Feb 2021 15:53:01 -0500 Subject: [PATCH 41/51] fix summary alert details (#90657) --- .../factory/events/details/helpers.ts | 12 +- .../security_solution/timeline_details.ts | 158 +++++++++--------- 2 files changed, 82 insertions(+), 88 deletions(-) diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts index 4a6a1d61a92214..779454e9474ee7 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts @@ -51,16 +51,8 @@ export const getDataFromSourceHits = ( { category: fieldCategory, field, - values: Array.isArray(item) - ? item.map((value) => { - if (isObject(value)) { - return JSON.stringify(value); - } - - return value; - }) - : [item], - originalValue: item, + values: toStringArray(item), + originalValue: toStringArray(item), } as TimelineEventsDetailsItem, ]; } else if (isObject(item)) { diff --git a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts index 2705406009062d..39b343a3619457 100644 --- a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts +++ b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts @@ -19,97 +19,97 @@ const EXPECTED_DATA = [ category: 'base', field: '@timestamp', values: ['2019-02-10T02:39:44.107Z'], - originalValue: '2019-02-10T02:39:44.107Z', + originalValue: ['2019-02-10T02:39:44.107Z'], }, { category: '@version', field: '@version', values: ['1'], - originalValue: '1', + originalValue: ['1'], }, { category: 'agent', field: 'agent.ephemeral_id', values: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], - originalValue: '909cd6a1-527d-41a5-9585-a7fb5386f851', + originalValue: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], }, { category: 'agent', field: 'agent.hostname', values: ['raspberrypi'], - originalValue: 'raspberrypi', + originalValue: ['raspberrypi'], }, { category: 'agent', field: 'agent.id', values: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], - originalValue: '4d3ea604-27e5-4ec7-ab64-44f82285d776', + originalValue: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], }, { category: 'agent', field: 'agent.type', values: ['filebeat'], - originalValue: 'filebeat', + originalValue: ['filebeat'], }, { category: 'agent', field: 'agent.version', values: ['7.0.0'], - originalValue: '7.0.0', + originalValue: ['7.0.0'], }, { category: 'destination', field: 'destination.domain', values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], - originalValue: 's3-iad-2.cf.dash.row.aiv-cdn.net', + originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], }, { category: 'destination', field: 'destination.ip', values: ['10.100.7.196'], - originalValue: '10.100.7.196', + originalValue: ['10.100.7.196'], }, { category: 'destination', field: 'destination.port', - values: [40684], - originalValue: 40684, + values: ['40684'], + originalValue: ['40684'], }, { category: 'ecs', field: 'ecs.version', values: ['1.0.0-beta2'], - originalValue: '1.0.0-beta2', + originalValue: ['1.0.0-beta2'], }, { category: 'event', field: 'event.dataset', values: ['suricata.eve'], - originalValue: 'suricata.eve', + originalValue: ['suricata.eve'], }, { category: 'event', field: 'event.end', values: ['2019-02-10T02:39:44.107Z'], - originalValue: '2019-02-10T02:39:44.107Z', + originalValue: ['2019-02-10T02:39:44.107Z'], }, { category: 'event', field: 'event.kind', values: ['event'], - originalValue: 'event', + originalValue: ['event'], }, { category: 'event', field: 'event.module', values: ['suricata'], - originalValue: 'suricata', + originalValue: ['suricata'], }, { category: 'event', field: 'event.type', values: ['fileinfo'], - originalValue: 'fileinfo', + originalValue: ['fileinfo'], }, { category: 'file', @@ -117,260 +117,261 @@ const EXPECTED_DATA = [ values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - originalValue: + originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], }, { category: 'file', field: 'file.size', - values: [48277], - originalValue: 48277, + values: ['48277'], + originalValue: ['48277'], }, { category: 'fileset', field: 'fileset.name', values: ['eve'], - originalValue: 'eve', + originalValue: ['eve'], }, { category: 'flow', field: 'flow.locality', values: ['public'], - originalValue: 'public', + originalValue: ['public'], }, { category: 'host', field: 'host.architecture', values: ['armv7l'], - originalValue: 'armv7l', + originalValue: ['armv7l'], }, { category: 'host', field: 'host.hostname', values: ['raspberrypi'], - originalValue: 'raspberrypi', + originalValue: ['raspberrypi'], }, { category: 'host', field: 'host.id', values: ['b19a781f683541a7a25ee345133aa399'], - originalValue: 'b19a781f683541a7a25ee345133aa399', + originalValue: ['b19a781f683541a7a25ee345133aa399'], }, { category: 'host', field: 'host.name', values: ['raspberrypi'], - originalValue: 'raspberrypi', + originalValue: ['raspberrypi'], }, { category: 'host', field: 'host.os.codename', values: ['stretch'], - originalValue: 'stretch', + originalValue: ['stretch'], }, { category: 'host', field: 'host.os.family', values: [''], - originalValue: '', + originalValue: [''], }, { category: 'host', field: 'host.os.kernel', values: ['4.14.50-v7+'], - originalValue: '4.14.50-v7+', + originalValue: ['4.14.50-v7+'], }, { category: 'host', field: 'host.os.name', values: ['Raspbian GNU/Linux'], - originalValue: 'Raspbian GNU/Linux', + originalValue: ['Raspbian GNU/Linux'], }, { category: 'host', field: 'host.os.platform', values: ['raspbian'], - originalValue: 'raspbian', + originalValue: ['raspbian'], }, { category: 'host', field: 'host.os.version', values: ['9 (stretch)'], - originalValue: '9 (stretch)', + originalValue: ['9 (stretch)'], }, { category: 'http', field: 'http.request.method', values: ['get'], - originalValue: 'get', + originalValue: ['get'], }, { category: 'http', field: 'http.response.body.bytes', - values: [48277], - originalValue: 48277, + values: ['48277'], + originalValue: ['48277'], }, { category: 'http', field: 'http.response.status_code', - values: [206], - originalValue: 206, + values: ['206'], + originalValue: ['206'], }, { category: 'input', field: 'input.type', values: ['log'], - originalValue: 'log', + originalValue: ['log'], }, { category: 'base', field: 'labels.pipeline', values: ['filebeat-7.0.0-suricata-eve-pipeline'], - originalValue: 'filebeat-7.0.0-suricata-eve-pipeline', + originalValue: ['filebeat-7.0.0-suricata-eve-pipeline'], }, { category: 'log', field: 'log.file.path', values: ['/var/log/suricata/eve.json'], - originalValue: '/var/log/suricata/eve.json', + originalValue: ['/var/log/suricata/eve.json'], }, { category: 'log', field: 'log.offset', - values: [1856288115], - originalValue: 1856288115, + values: ['1856288115'], + originalValue: ['1856288115'], }, { category: 'network', field: 'network.name', values: ['iot'], - originalValue: 'iot', + originalValue: ['iot'], }, { category: 'network', field: 'network.protocol', values: ['http'], - originalValue: 'http', + originalValue: ['http'], }, { category: 'network', field: 'network.transport', values: ['tcp'], - originalValue: 'tcp', + originalValue: ['tcp'], }, { category: 'service', field: 'service.type', values: ['suricata'], - originalValue: 'suricata', + originalValue: ['suricata'], }, { category: 'source', field: 'source.as.num', - values: [16509], - originalValue: 16509, + values: ['16509'], + originalValue: ['16509'], }, { category: 'source', field: 'source.as.org', values: ['Amazon.com, Inc.'], - originalValue: 'Amazon.com, Inc.', + originalValue: ['Amazon.com, Inc.'], }, { category: 'source', field: 'source.domain', values: ['server-54-239-219-210.jfk51.r.cloudfront.net'], - originalValue: 'server-54-239-219-210.jfk51.r.cloudfront.net', + originalValue: ['server-54-239-219-210.jfk51.r.cloudfront.net'], }, { category: 'source', field: 'source.geo.city_name', values: ['Seattle'], - originalValue: 'Seattle', + originalValue: ['Seattle'], }, { category: 'source', field: 'source.geo.continent_name', values: ['North America'], - originalValue: 'North America', + originalValue: ['North America'], }, { category: 'source', field: 'source.geo.country_iso_code', values: ['US'], - originalValue: 'US', + originalValue: ['US'], }, { category: 'source', field: 'source.geo.location.lat', - values: [47.6103], - originalValue: 47.6103, + values: ['47.6103'], + originalValue: ['47.6103'], }, { category: 'source', field: 'source.geo.location.lon', - values: [-122.3341], - originalValue: -122.3341, + values: ['-122.3341'], + originalValue: ['-122.3341'], }, { category: 'source', field: 'source.geo.region_iso_code', values: ['US-WA'], - originalValue: 'US-WA', + originalValue: ['US-WA'], }, { category: 'source', field: 'source.geo.region_name', values: ['Washington'], - originalValue: 'Washington', + originalValue: ['Washington'], }, { category: 'source', field: 'source.ip', values: ['54.239.219.210'], - originalValue: '54.239.219.210', + originalValue: ['54.239.219.210'], }, { category: 'source', field: 'source.port', - values: [80], - originalValue: 80, + values: ['80'], + originalValue: ['80'], }, { category: 'suricata', field: 'suricata.eve.fileinfo.state', values: ['CLOSED'], - originalValue: 'CLOSED', + originalValue: ['CLOSED'], }, { category: 'suricata', field: 'suricata.eve.fileinfo.tx_id', - values: [301], - originalValue: 301, + values: ['301'], + originalValue: ['301'], }, { category: 'suricata', field: 'suricata.eve.flow_id', - values: [196625917175466], - originalValue: 196625917175466, + values: ['196625917175466'], + originalValue: ['196625917175466'], }, { category: 'suricata', field: 'suricata.eve.http.http_content_type', values: ['video/mp4'], - originalValue: 'video/mp4', + originalValue: ['video/mp4'], }, { category: 'suricata', field: 'suricata.eve.http.protocol', values: ['HTTP/1.1'], - originalValue: 'HTTP/1.1', + originalValue: ['HTTP/1.1'], }, { category: 'suricata', field: 'suricata.eve.in_iface', values: ['eth0'], - originalValue: 'eth0', + originalValue: ['eth0'], }, { category: 'base', @@ -382,7 +383,7 @@ const EXPECTED_DATA = [ category: 'url', field: 'url.domain', values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], - originalValue: 's3-iad-2.cf.dash.row.aiv-cdn.net', + originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], }, { category: 'url', @@ -390,8 +391,9 @@ const EXPECTED_DATA = [ values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - originalValue: + originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], }, { category: 'url', @@ -399,26 +401,27 @@ const EXPECTED_DATA = [ values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - originalValue: + originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], }, { category: '_index', field: '_index', values: ['filebeat-7.0.0-iot-2019.06'], - originalValue: 'filebeat-7.0.0-iot-2019.06', + originalValue: ['filebeat-7.0.0-iot-2019.06'], }, { category: '_id', field: '_id', values: ['QRhG1WgBqd-n62SwZYDT'], - originalValue: 'QRhG1WgBqd-n62SwZYDT', + originalValue: ['QRhG1WgBqd-n62SwZYDT'], }, { category: '_score', field: '_score', - values: [1], - originalValue: 1, + values: ['1'], + originalValue: ['1'], }, ]; @@ -452,7 +455,6 @@ export default function ({ getService }: FtrProviderContext) { eventId: ID, }) .expect(200); - expect(sortBy(detailsData, 'name')).to.eql(sortBy(EXPECTED_DATA, 'name')); }); From 180f309fab1dec168a0a7ae81ac497f46966b15d Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Mon, 8 Feb 2021 16:05:26 -0500 Subject: [PATCH 42/51] Update security solution codeowners (#89038) * move the code coverage owner line for security solution next to the other lines about security solution * replace @elastic/siem with @elastic/security-solution and remove the duplicate code coverage owner for security_solution * remove elastic/endpoint-app-team and cleanup directories that no longer exist, and reorder lines * added case_api_integration code owners Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2917cc52a6c6db..b6c0c6afdee0bf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -244,7 +244,6 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /x-pack/test/security_api_integration/ @elastic/kibana-security /x-pack/test/security_functional/ @elastic/kibana-security /x-pack/test/spaces_api_integration/ @elastic/kibana-security -#CC# /x-pack/plugins/security_solution/ @elastic/kibana-security #CC# /x-pack/plugins/security/ @elastic/kibana-security # Kibana Alerting Services @@ -312,25 +311,22 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib #CC# /x-pack/plugins/console_extensions/ @elastic/es-ui #CC# /x-pack/plugins/cross_cluster_replication/ @elastic/es-ui -# Endpoint -/x-pack/plugins/endpoint/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/endpoint_api_integration_no_ingest/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/security_solution_endpoint/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/functional/es_archives/endpoint/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/plugin_functional/test_suites/resolver/ @elastic/endpoint-app-team @elastic/siem -#CC# /x-pack/legacy/plugins/siem/ @elastic/siem -#CC# /x-pack/plugins/siem/ @elastic/siem -#CC# /x-pack/plugins/security_solution/ @elastic/siem - # Security Solution -/x-pack/plugins/security_solution/ @elastic/siem @elastic/endpoint-app-team -/x-pack/test/detection_engine_api_integration @elastic/siem @elastic/endpoint-app-team -/x-pack/test/lists_api_integration @elastic/siem @elastic/endpoint-app-team -/x-pack/test/api_integration/apis/security_solution @elastic/siem @elastic/endpoint-app-team -/x-pack/plugins/case @elastic/siem @elastic/endpoint-app-team -/x-pack/plugins/lists @elastic/siem @elastic/endpoint-app-team -#CC# /x-pack/plugins/security_solution/ @elastic/siem +/x-pack/test/endpoint_api_integration_no_ingest/ @elastic/security-solution +/x-pack/test/security_solution_endpoint/ @elastic/security-solution +/x-pack/test/functional/es_archives/endpoint/ @elastic/security-solution +/x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/security-solution +/x-pack/test/plugin_functional/test_suites/resolver/ @elastic/security-solution +/x-pack/plugins/security_solution/ @elastic/security-solution +/x-pack/test/detection_engine_api_integration @elastic/security-solution +/x-pack/test/lists_api_integration @elastic/security-solution +/x-pack/test/api_integration/apis/security_solution @elastic/security-solution +#CC# /x-pack/plugins/security_solution/ @elastic/security-solution + +# Security Solution sub teams +/x-pack/plugins/case @elastic/security-threat-hunting +/x-pack/test/case_api_integration @elastic/security-threat-hunting +/x-pack/plugins/lists @elastic/security-detections-response # Security Intelligence And Analytics /x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics @@ -362,3 +358,4 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Reporting #CC# /x-pack/plugins/reporting/ @elastic/kibana-reporting-services + From 3fa76956ac3e963cd6d89f2f390b47e47d3f65b0 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Mon, 8 Feb 2021 16:08:23 -0500 Subject: [PATCH 43/51] [CI] Automated backports via GitHub Actions - initial MVP (#90669) --- .github/CODEOWNERS | 1 + .github/workflows/backport.yml | 46 ++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 .github/workflows/backport.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b6c0c6afdee0bf..ec07a6a03d2c8f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -149,6 +149,7 @@ /src/cli/keystore/ @elastic/kibana-operations /src/legacy/server/warnings/ @elastic/kibana-operations /.ci/es-snapshots/ @elastic/kibana-operations +/.github/workflows/ @elastic/kibana-operations /vars/ @elastic/kibana-operations /.bazelignore @elastic/kibana-operations /.bazeliskversion @elastic/kibana-operations diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 00000000000000..f64b9e95fbaabc --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,46 @@ +on: + pull_request_target: + branches: + - master + types: + - labeled + - closed + +jobs: + backport: + name: Backport PR + if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'auto-backport') + runs-on: ubuntu-latest + + steps: + - name: 'Get backport config' + run: | + curl 'https://raw.githubusercontent.com/elastic/kibana/master/.backportrc.json' > .backportrc.json + + - name: Use Node.js 14.x + uses: actions/setup-node@v1 + with: + node-version: 14.x + + - name: Install backport CLI + run: npm install -g backport@5.6.4 + + - name: Backport PR + run: | + git config --global user.name "kibanamachine" + git config --global user.email "42973632+kibanamachine@users.noreply.github.com" + backport --fork true --username kibanamachine --accessToken "${{ secrets.KIBANAMACHINE_TOKEN }}" --ci --pr "$PR_NUMBER" --labels backport --assignee "$PR_OWNER" | tee 'output.log' + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_OWNER: ${{ github.event.pull_request.user.login }} + + - name: Report backport status + run: | + COMMENT="Backport result + \`\`\` + $(cat output.log) + \`\`\`" + + GITHUB_TOKEN="${{ secrets.KIBANAMACHINE_TOKEN }}" gh api -X POST repos/elastic/kibana/issues/$PR_NUMBER/comments -F body="$COMMENT" + env: + PR_NUMBER: ${{ github.event.pull_request.number }} From 7a2b7550c962c5b6084f8f84e2dc9a2b5f3db0a2 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Mon, 8 Feb 2021 15:10:23 -0600 Subject: [PATCH 44/51] [DOCS] Fixes Dashboard formatting (#90485) * [DOCS] Fixes Dashboard formatting * Fixes the semi-structured search example * Update docs/user/dashboard/dashboard.asciidoc Co-authored-by: Wylie Conlon Co-authored-by: Wylie Conlon --- docs/user/dashboard/dashboard.asciidoc | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/user/dashboard/dashboard.asciidoc b/docs/user/dashboard/dashboard.asciidoc index 8b3eddc008500d..3c86c37f1fd301 100644 --- a/docs/user/dashboard/dashboard.asciidoc +++ b/docs/user/dashboard/dashboard.asciidoc @@ -133,15 +133,17 @@ image::dashboard/images/dashboard-filters.png[Labeled interface with semi-struct Semi-structured search:: Combine free text search with field-based search using the <>. Type a search term to match across all fields, or begin typing a field name to - get prompted with field names and operators you can use to build a structured query. - + + get prompted with field names and operators you can use to build a structured query. For example, in the sample web logs data, this query displays data only for the US: - . Enter `g`, and then select *geo.source*. - . Select *equals some value* and *US*, and then click *Update*. + . Enter `g`, then select *geo.source*. + . Select *equals some value* and *US*, then click *Update*. . For a more complex search, try: - `geo.src : "US" and url.keyword : "https://www.elastic.co/downloads/beats/metricbeat"` +[source,text] +------------------- +geo.src : "US" and url.keyword : "https://www.elastic.co/downloads/beats/metricbeat" +------------------- Time filter:: Dashboards have a global time filter that restricts the data that displays, but individual panels can @@ -152,21 +154,18 @@ Time filter:: . Open the panel menu, then select *More > Customize time range*. . On the *Customize panel time range* window, specify the new time range, then click *Add to panel*. - + [role="screenshot"] image:images/time_range_per_panel.gif[Time range per dashboard panel] Additional filters with AND:: - You can add filters to a dashboard, or pin filters to multiple places in {kib}. To add filters, using a basic editor or an advanced JSON editor for the {es} {ref}/query-dsl.html[query DSL]. - + Add filters to a dashboard, or pin filters to multiple places in {kib}. To add filters, using a basic editor or an advanced JSON editor for the {es} {ref}/query-dsl.html[query DSL]. When you use more than one index pattern on a dashboard, the filter editor allows you to filter only one dashboard. - To dynamically add filters, click a series on a dashboard. For example, to filter the dashboard to display only ios data: - . Click *Add filter*. . Set *Field* to *machine.os*, *Operator* to *is*, and *Value* to *ios*. . *Save* the filter. - . To remove the filter, click *x* next to the filter. + . To remove the filter, click *x*. [float] [[clone-panels]] From 46feb7659279b98d07ae7c7a259cd666605ba6ae Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 8 Feb 2021 23:42:07 +0200 Subject: [PATCH 45/51] [Alerts] Jira: Disallow labels with spaces (#90548) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/actions/README.md | 2 +- .../builtin_action_types/jira/schema.ts | 10 ++++++- .../builtin_action_types/jira/jira.test.tsx | 19 ++++++++++++- .../builtin_action_types/jira/jira.tsx | 7 +++++ .../builtin_action_types/jira/jira_params.tsx | 8 ++++++ .../builtin_action_types/jira/translations.ts | 7 +++++ .../actions/builtin_action_types/jira.ts | 28 +++++++++++++++++++ 7 files changed, 78 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 1eb94af4dddf8c..1d50bc7e058070 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -657,7 +657,7 @@ The following table describes the properties of the `incident` object. | externalId | The id of the issue in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | | issueType | The id of the issue type in Jira. | string _(optional)_ | | priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ | -| labels | An array of labels. | string[] _(optional)_ | +| labels | An array of labels. Labels cannot contain spaces. | string[] _(optional)_ | | parent | The parent issue id or key. Only for `Sub-task` issue types. | string _(optional)_ | #### `subActionParams (getIncident)` diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts index 552053bdd76516..a81dfaeef8175a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -40,7 +40,15 @@ export const ExecutorSubActionPushParamsSchema = schema.object({ externalId: schema.nullable(schema.string()), issueType: schema.nullable(schema.string()), priority: schema.nullable(schema.string()), - labels: schema.nullable(schema.arrayOf(schema.string())), + labels: schema.nullable( + schema.arrayOf( + schema.string({ + validate: (label) => + // Matches any space, tab or newline character. + label.match(/\s/g) ? `The label ${label} cannot contain spaces` : undefined, + }) + ) + ), parent: schema.nullable(schema.string()), }), comments: schema.nullable( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx index 2d47740a581b80..ea1bcf82c314c3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx @@ -96,7 +96,7 @@ describe('jira action params validation', () => { }; expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { 'subActionParams.incident.summary': [] }, + errors: { 'subActionParams.incident.summary': [], 'subActionParams.incident.labels': [] }, }); }); @@ -108,6 +108,23 @@ describe('jira action params validation', () => { expect(actionTypeModel.validateParams(actionParams)).toEqual({ errors: { 'subActionParams.incident.summary': ['Summary is required.'], + 'subActionParams.incident.labels': [], + }, + }); + }); + + test('params validation fails when labels contain spaces', () => { + const actionParams = { + subActionParams: { + incident: { summary: 'some title', labels: ['label with spaces'] }, + comments: [], + }, + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + 'subActionParams.incident.summary': [], + 'subActionParams.incident.labels': ['Labels cannot contain spaces.'], }, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx index 5cb8a76d09bee6..26b37278003c35 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx @@ -72,6 +72,7 @@ export function getActionType(): ActionTypeModel => { const errors = { 'subActionParams.incident.summary': new Array(), + 'subActionParams.incident.labels': new Array(), }; const validationResult = { errors, @@ -83,6 +84,12 @@ export function getActionType(): ActionTypeModel label.match(/\s/g))) + errors['subActionParams.incident.labels'].push(i18n.LABELS_WHITE_SPACES); + } return validationResult; }, actionParamsFields: lazy(() => import('./jira_params')), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index 75930482797a29..cb2d637972cb82 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -184,6 +184,11 @@ const JiraParamsFields: React.FunctionComponent 0 && + incident.labels !== undefined; + return ( <> @@ -304,6 +309,8 @@ const JiraParamsFields: React.FunctionComponent
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts index 3c8bda7792f0a1..fe7ea61e681931 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts @@ -199,3 +199,10 @@ export const SEARCH_ISSUES_LOADING = i18n.translate( defaultMessage: 'Loading...', } ); + +export const LABELS_WHITE_SPACES = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.labelsSpacesErrorMessage', + { + defaultMessage: 'Labels cannot contain spaces.', + } +); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index 6cc5e2eaefb94a..8bd0b8a790d402 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -375,6 +375,34 @@ export default function jiraTest({ getService }: FtrProviderContext) { }); }); }); + + it('should handle failing with a simulated success when labels containing a space', async () => { + await supertest + .post(`/api/actions/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockJira.params, + subActionParams: { + incident: { + ...mockJira.params.subActionParams.incident, + issueType: '10006', + labels: ['label with spaces'], + }, + comments: [], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.incident.labels]: types that failed validation:\n - [subActionParams.incident.labels.0.0]: The label label with spaces cannot contain spaces\n - [subActionParams.incident.labels.1]: expected value to equal [null]\n- [4.subAction]: expected value to equal [issueTypes]\n- [5.subAction]: expected value to equal [fieldsByIssueType]\n- [6.subAction]: expected value to equal [issues]\n- [7.subAction]: expected value to equal [issue]', + }); + }); + }); }); describe('Execution', () => { From b39ad86b5d59fef96366affbdfbcc6758545cba9 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Mon, 8 Feb 2021 14:23:10 -0800 Subject: [PATCH 46/51] Use default ES distribution for functional tests (#88737) Signed-off-by: Tyler Smalley Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .ci/packer_cache_for_branch.sh | 1 - .../src/functional_test_runner/lib/config/schema.ts | 2 +- .../kbn-test/src/legacy_es/legacy_es_test_cluster.js | 2 +- .../migrationsv2/integration_tests/migration.test.ts | 2 +- .../migration_7.7.2_xpack_100k.test.ts | 2 +- src/core/test_helpers/kbn_server.ts | 4 ++-- src/dev/ci_setup/setup.sh | 2 -- test/api_integration/config.js | 5 ++++- test/common/config.js | 4 +--- test/common/services/deployment.ts | 12 +----------- test/examples/config.js | 5 ++++- test/functional/config.js | 6 ++++-- test/plugin_functional/config.ts | 5 ++++- test/server_integration/config.js | 5 ++++- test/server_integration/http/ssl/config.js | 5 ++++- test/server_integration/http/ssl_redirect/config.js | 5 ++++- test/server_integration/http/ssl_with_p12/config.js | 5 ++++- .../http/ssl_with_p12_intermediate/config.js | 5 ++++- 18 files changed, 44 insertions(+), 33 deletions(-) diff --git a/.ci/packer_cache_for_branch.sh b/.ci/packer_cache_for_branch.sh index bbdf5484faf659..ee220537de340a 100755 --- a/.ci/packer_cache_for_branch.sh +++ b/.ci/packer_cache_for_branch.sh @@ -26,7 +26,6 @@ source src/dev/ci_setup/setup.sh; # download es snapshots node scripts/es snapshot --download-only; -node scripts/es snapshot --license=oss --download-only; # download reporting browsers (cd "x-pack" && node ../node_modules/.bin/gulp downloadChromium); diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 46471a4e9dac7a..4fd28678d2653f 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -169,7 +169,7 @@ export const schema = Joi.object() esTestCluster: Joi.object() .keys({ - license: Joi.string().default('oss'), + license: Joi.string().default('basic'), from: Joi.string().default('snapshot'), serverArgs: Joi.array(), serverEnvVars: Joi.object(), diff --git a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js index c04564279a9719..43b6c90452b817 100644 --- a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js +++ b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js @@ -22,7 +22,7 @@ export function createLegacyEsTestCluster(options = {}) { const { port = esTestConfig.getPort(), password = 'changeme', - license = 'oss', + license = 'basic', log, basePath = resolve(KIBANA_ROOT, '.es'), esFrom = esTestConfig.getBuildFrom(), diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts index 2d3ab91697e42c..317bfe33b3a199 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts @@ -30,7 +30,7 @@ describe('migration v2', () => { adjustTimeout: (t: number) => jest.setTimeout(t), settings: { es: { - license: oss ? 'oss' : 'trial', + license: 'trial', dataArchive, }, }, diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts index bce01c93fe8868..16ba0c855867ce 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts @@ -32,7 +32,7 @@ describe.skip('migration from 7.7.2-xpack with 100k objects', () => { adjustTimeout: (t: number) => jest.setTimeout(600000), settings: { es: { - license: oss ? 'oss' : 'trial', + license: 'trial', dataArchive, }, }, diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts index 011ba67a055121..14f614643ac9f1 100644 --- a/src/core/test_helpers/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -185,7 +185,7 @@ export function createTestServers({ adjustTimeout: (timeout: number) => void; settings?: { es?: { - license: 'oss' | 'basic' | 'gold' | 'trial'; + license: 'basic' | 'gold' | 'trial'; [key: string]: any; }; kbn?: { @@ -208,7 +208,7 @@ export function createTestServers({ if (!adjustTimeout) { throw new Error('adjustTimeout is required in order to avoid flaky tests'); } - const license = get(settings, 'es.license', 'oss'); + const license = get(settings, 'es.license', 'basic'); const usersToBeAdded = get(settings, 'users', []); if (usersToBeAdded.length > 0) { if (license !== 'trial') { diff --git a/src/dev/ci_setup/setup.sh b/src/dev/ci_setup/setup.sh index 0b24f0b22b81a9..db7110d2d0875d 100755 --- a/src/dev/ci_setup/setup.sh +++ b/src/dev/ci_setup/setup.sh @@ -32,8 +32,6 @@ yarn kbn bootstrap ### echo " -- downloading es snapshot" node scripts/es snapshot --download-only; -node scripts/es snapshot --license=oss --download-only; - ### ### verify no git modifications diff --git a/test/api_integration/config.js b/test/api_integration/config.js index d688c31dc47e78..bd8f10606a45ae 100644 --- a/test/api_integration/config.js +++ b/test/api_integration/config.js @@ -19,7 +19,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'API Integration Tests', }, - esTestCluster: commonConfig.get('esTestCluster'), + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...functionalConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/common/config.js b/test/common/config.js index 9d108f05fd1fca..46cd07b2ec3704 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -21,9 +21,7 @@ export default function () { servers, esTestCluster: { - license: 'oss', - from: 'snapshot', - serverArgs: [], + serverArgs: ['xpack.security.enabled=false'], }, kbnTestServer: { diff --git a/test/common/services/deployment.ts b/test/common/services/deployment.ts index a19118bb3065ab..510124ce3d1b76 100644 --- a/test/common/services/deployment.ts +++ b/test/common/services/deployment.ts @@ -35,17 +35,7 @@ export function DeploymentProvider({ getService }: FtrProviderContext) { * Useful for functional testing in cloud environment */ async isOss() { - const baseUrl = this.getEsHostPort(); - const username = config.get('servers.elasticsearch.username'); - const password = config.get('servers.elasticsearch.password'); - const response = await fetch(baseUrl + '/_xpack', { - method: 'get', - headers: { - 'Content-Type': 'application/json', - Authorization: 'Basic ' + Buffer.from(username + ':' + password).toString('base64'), - }, - }); - return response.status !== 200; + return config.get('kbnTestServer.serverArgs').indexOf('--oss') > -1; }, async isCloud(): Promise { diff --git a/test/examples/config.js b/test/examples/config.js index fd1ad671cf4bf2..0ba7af0bfceb75 100644 --- a/test/examples/config.js +++ b/test/examples/config.js @@ -34,7 +34,10 @@ export default async function ({ readConfigFile }) { }, pageObjects: functionalConfig.get('pageObjects'), servers: functionalConfig.get('servers'), - esTestCluster: functionalConfig.get('esTestCluster'), + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, apps: functionalConfig.get('apps'), esArchiver: { directory: path.resolve(__dirname, '../es_archives'), diff --git a/test/functional/config.js b/test/functional/config.js index c15cfffbdb5763..05d6cf9dd6b68a 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -32,8 +32,10 @@ export default async function ({ readConfigFile }) { servers: commonConfig.get('servers'), - esTestCluster: commonConfig.get('esTestCluster'), - + esTestCluster: { + ...commonConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...commonConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/plugin_functional/config.ts b/test/plugin_functional/config.ts index f28e219884bde7..bd5ef814ae6c0a 100644 --- a/test/plugin_functional/config.ts +++ b/test/plugin_functional/config.ts @@ -36,7 +36,10 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { }, pageObjects: functionalConfig.get('pageObjects'), servers: functionalConfig.get('servers'), - esTestCluster: functionalConfig.get('esTestCluster'), + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, apps: functionalConfig.get('apps'), esArchiver: { directory: path.resolve(__dirname, '../es_archives'), diff --git a/test/server_integration/config.js b/test/server_integration/config.js index 7171a9b33bfd81..0ebb5c48033b80 100644 --- a/test/server_integration/config.js +++ b/test/server_integration/config.js @@ -27,7 +27,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Integration Tests', }, - esTestCluster: commonConfig.get('esTestCluster'), + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...functionalConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/server_integration/http/ssl/config.js b/test/server_integration/http/ssl/config.js index b305728b64de23..14381de6667fdc 100644 --- a/test/server_integration/http/ssl/config.js +++ b/test/server_integration/http/ssl/config.js @@ -33,7 +33,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Http SSL Integration Tests', }, - esTestCluster: httpConfig.get('esTestCluster'), + esTestCluster: { + ...httpConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...httpConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/server_integration/http/ssl_redirect/config.js b/test/server_integration/http/ssl_redirect/config.js index 0c3e8ce78237a4..d19883bcfe241f 100644 --- a/test/server_integration/http/ssl_redirect/config.js +++ b/test/server_integration/http/ssl_redirect/config.js @@ -44,7 +44,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Http SSL Integration Tests', }, - esTestCluster: httpConfig.get('esTestCluster'), + esTestCluster: { + ...httpConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...httpConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/server_integration/http/ssl_with_p12/config.js b/test/server_integration/http/ssl_with_p12/config.js index 75a33226aa669d..c4621500e927d9 100644 --- a/test/server_integration/http/ssl_with_p12/config.js +++ b/test/server_integration/http/ssl_with_p12/config.js @@ -33,7 +33,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Http SSL Integration Tests', }, - esTestCluster: httpConfig.get('esTestCluster'), + esTestCluster: { + ...httpConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...httpConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/server_integration/http/ssl_with_p12_intermediate/config.js b/test/server_integration/http/ssl_with_p12_intermediate/config.js index a120ea0b3a556a..7f32bad6483514 100644 --- a/test/server_integration/http/ssl_with_p12_intermediate/config.js +++ b/test/server_integration/http/ssl_with_p12_intermediate/config.js @@ -33,7 +33,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Http SSL Integration Tests', }, - esTestCluster: httpConfig.get('esTestCluster'), + esTestCluster: { + ...httpConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...httpConfig.get('kbnTestServer'), serverArgs: [ From 15a4c285b860904b6546519ba3905b85a260554a Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 8 Feb 2021 22:24:14 +0000 Subject: [PATCH 47/51] chore(NA): move bazel workspace status from bash script into nodejs executable (#90560) * chore(NA): move bazel workspace status into nodejs executable * chore(NA): removed unused console.log on error * chore(NA): ability to setup different name for origin remote on workspace status command * chore(NA): do not fail if cant collect repo url Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .bazelrc | 2 +- src/dev/bazel_workspace_status.js | 80 +++++++++++++++++++++++++++++++ src/dev/bazel_workspace_status.sh | 57 ---------------------- 3 files changed, 81 insertions(+), 58 deletions(-) create mode 100644 src/dev/bazel_workspace_status.js delete mode 100755 src/dev/bazel_workspace_status.sh diff --git a/.bazelrc b/.bazelrc index 158338ec5f093c..5fa6ef245fceae 100644 --- a/.bazelrc +++ b/.bazelrc @@ -11,7 +11,7 @@ import %workspace%/.bazelrc.common # BuildBuddy ## Metadata settings -build --workspace_status_command=$(pwd)/src/dev/bazel_workspace_status.sh +build --workspace_status_command="node ./src/dev/bazel_workspace_status.js" # Enable this in case you want to share your build info # build --build_metadata=VISIBILITY=PUBLIC build --build_metadata=TEST_GROUPS=//packages diff --git a/src/dev/bazel_workspace_status.js b/src/dev/bazel_workspace_status.js new file mode 100644 index 00000000000000..fe60f9176d243e --- /dev/null +++ b/src/dev/bazel_workspace_status.js @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// Inspired on https://github.com/buildbuddy-io/buildbuddy/blob/master/workspace_status.sh +// This script will be run bazel when building process starts to +// generate key-value information that represents the status of the +// workspace. The output should be like +// +// KEY1 VALUE1 +// KEY2 VALUE2 +// +// If the script exits with non-zero code, it's considered as a failure +// and the output will be discarded. + +(async () => { + const execa = require('execa'); + const os = require('os'); + + async function runCmd(cmd, args) { + try { + return await execa(cmd, args); + } catch (e) { + return { exitCode: 1 }; + } + } + + // Git repo + const kbnGitOriginName = process.env.KBN_GIT_ORIGIN_NAME || 'origin'; + const repoUrlCmdResult = await runCmd('git', [ + 'config', + '--get', + `remote.${kbnGitOriginName}.url`, + ]); + if (repoUrlCmdResult.exitCode === 0) { + // Only output REPO_URL when found it + console.log(`REPO_URL ${repoUrlCmdResult.stdout}`); + } + + // Commit SHA + const commitSHACmdResult = await runCmd('git', ['rev-parse', 'HEAD']); + if (commitSHACmdResult.exitCode !== 0) { + process.exit(1); + } + console.log(`COMMIT_SHA ${commitSHACmdResult.stdout}`); + + // Git branch + const gitBranchCmdResult = await runCmd('git', ['rev-parse', '--abbrev-ref', 'HEAD']); + if (gitBranchCmdResult.exitCode !== 0) { + process.exit(1); + } + console.log(`GIT_BRANCH ${gitBranchCmdResult.stdout}`); + + // Tree status + const treeStatusCmdResult = await runCmd('git', ['diff-index', '--quiet', 'HEAD', '--']); + const treeStatusVarStr = 'GIT_TREE_STATUS'; + if (treeStatusCmdResult.exitCode === 0) { + console.log(`${treeStatusVarStr} Clean`); + } else { + console.log(`${treeStatusVarStr} Modified`); + } + + // Host + if (process.env.CI) { + const hostCmdResult = await runCmd('hostname'); + const hostStr = hostCmdResult.stdout.split('-').slice(0, -1).join('-'); + const coresStr = os.cpus().filter((cpu, index) => { + return !cpu.model.includes('Intel') || index % 2 === 1; + }).length; + + if (hostCmdResult.exitCode !== 0) { + process.exit(1); + } + console.log(`HOST ${hostStr}-${coresStr}`); + } +})(); diff --git a/src/dev/bazel_workspace_status.sh b/src/dev/bazel_workspace_status.sh deleted file mode 100755 index efaca4bb98849b..00000000000000 --- a/src/dev/bazel_workspace_status.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash - -# Inspired on https://github.com/buildbuddy-io/buildbuddy/blob/master/workspace_status.sh -# This script will be run bazel when building process starts to -# generate key-value information that represents the status of the -# workspace. The output should be like -# -# KEY1 VALUE1 -# KEY2 VALUE2 -# -# If the script exits with non-zero code, it's considered as a failure -# and the output will be discarded. - -# Git repo -repo_url=$(git config --get remote.origin.url) -if [[ $? != 0 ]]; -then - exit 1 -fi -echo "REPO_URL ${repo_url}" - -# Commit SHA -commit_sha=$(git rev-parse HEAD) -if [[ $? != 0 ]]; -then - exit 1 -fi -echo "COMMIT_SHA ${commit_sha}" - -# Git branch -repo_url=$(git rev-parse --abbrev-ref HEAD) -if [[ $? != 0 ]]; -then - exit 1 -fi -echo "GIT_BRANCH ${repo_url}" - -# Tree status -git diff-index --quiet HEAD -- -if [[ $? == 0 ]]; -then - tree_status="Clean" -else - tree_status="Modified" -fi -echo "GIT_TREE_STATUS ${tree_status}" - -# Host -if [ "$CI" = "true" ]; then - host=$(hostname | sed 's|\(.*\)-.*|\1|') - cores=$(grep ^cpu\\scores /proc/cpuinfo | uniq | awk '{print $4}' ) - if [[ $? != 0 ]]; - then - exit 1 - fi - echo "HOST ${host}-${cores}" -fi From 54863889d496361239dea50360b68f390160e03c Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 8 Feb 2021 22:25:05 +0000 Subject: [PATCH 48/51] chore(NA): remove write permissions on Bazel remote cache for PRs (#90652) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/dev/ci_setup/setup_env.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 0b835d4b9fa947..2deafaaf35a94d 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -180,6 +180,15 @@ fi ### cp -f "$KIBANA_DIR/src/dev/ci_setup/.bazelrc-ci" "$HOME/.bazelrc"; +### +### remove write permissions on buildbuddy remote cache for prs +### +if [[ "$ghprbPullId" ]] ; then + echo "# Appended by $KIBANA_DIR/src/dev/ci_setup/setup.sh" >> "$HOME/.bazelrc" + echo "# Uploads logs & artifacts without writing to cache" >> "$HOME/.bazelrc" + echo "build --noremote_upload_local_results" >> "$HOME/.bazelrc" +fi + ### ### append auth token to buildbuddy into "$HOME/.bazelrc"; ### From 3ef3e9324fd9d77dd442b09e3355fb005c4b3caf Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 8 Feb 2021 14:46:57 -0800 Subject: [PATCH 49/51] [dev-utils/ship-ci-stats] fail when CI stats is down (#90678) Co-authored-by: spalger --- .../src/ci_stats_reporter/ci_stats_reporter.ts | 2 +- .../src/ci_stats_reporter/ship_ci_stats_cli.ts | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts index cb5175142c1606..93826cf3add80c 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts @@ -109,7 +109,7 @@ export class CiStatsReporter { }, }); - return; + return true; } catch (error) { if (!error?.request) { // not an axios error, must be a usage error that we should notify user about diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts index 244af7b6574183..1ee78518bb8018 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts @@ -10,7 +10,7 @@ import Path from 'path'; import Fs from 'fs'; import { CiStatsReporter } from './ci_stats_reporter'; -import { run, createFlagError } from '../run'; +import { run, createFlagError, createFailError } from '../run'; export function shipCiStatsCli() { run( @@ -23,12 +23,20 @@ export function shipCiStatsCli() { } const reporter = CiStatsReporter.fromEnv(log); + + if (!reporter.isEnabled()) { + throw createFailError('unable to initilize the CI Stats reporter'); + } + for (const path of metricPaths) { // resolve path from CLI relative to CWD const abs = Path.resolve(path); const json = Fs.readFileSync(abs, 'utf8'); - await reporter.metrics(JSON.parse(json)); - log.success('shipped metrics from', path); + if (await reporter.metrics(JSON.parse(json))) { + log.success('shipped metrics from', path); + } else { + throw createFailError('failed to ship metrics'); + } } }, { From 3cf00d2bb40c43471327501752ce099ce7f36a21 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Mon, 8 Feb 2021 15:21:06 -0800 Subject: [PATCH 50/51] [Time to Visualize] Adds functional tests for linking/unlinking panel from embeddable library (#89612) --- .github/CODEOWNERS | 1 + .../actions/add_to_library_action.tsx | 2 +- .../apps/dashboard/embeddable_library.ts | 111 ++++++++++++++++++ test/functional/apps/dashboard/index.ts | 1 + .../services/dashboard/panel_actions.ts | 25 ++++ x-pack/test/functional/apps/lens/dashboard.ts | 38 ++++++ .../maps/embeddable/embeddable_library.js | 80 +++++++++++++ .../functional/apps/maps/embeddable/index.js | 1 + 8 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 test/functional/apps/dashboard/embeddable_library.ts create mode 100644 x-pack/test/functional/apps/maps/embeddable/embeddable_library.js diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ec07a6a03d2c8f..34b449346ddf7e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -91,6 +91,7 @@ /src/plugins/dashboard/ @elastic/kibana-presentation /src/plugins/input_control_vis/ @elastic/kibana-presentation /src/plugins/vis_type_markdown/ @elastic/kibana-presentation +/test/functional/apps/dashboard/ @elastic/kibana-presentation /x-pack/plugins/canvas/ @elastic/kibana-presentation /x-pack/plugins/dashboard_enhanced/ @elastic/kibana-presentation /x-pack/test/functional/apps/canvas/ @elastic/kibana-presentation diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx index 5d384ed8ebd826..ef730e16bc5cf4 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx @@ -22,7 +22,7 @@ import { NotificationsStart } from '../../services/core'; import { dashboardAddToLibraryAction } from '../../dashboard_strings'; import { DashboardPanelState, DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '..'; -export const ACTION_ADD_TO_LIBRARY = 'addToFromLibrary'; +export const ACTION_ADD_TO_LIBRARY = 'saveToLibrary'; export interface AddToLibraryActionContext { embeddable: IEmbeddable; diff --git a/test/functional/apps/dashboard/embeddable_library.ts b/test/functional/apps/dashboard/embeddable_library.ts new file mode 100644 index 00000000000000..20fe9aeb1387a2 --- /dev/null +++ b/test/functional/apps/dashboard/embeddable_library.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'settings', 'common']); + const esArchiver = getService('esArchiver'); + const find = getService('find'); + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + const dashboardAddPanel = getService('dashboardAddPanel'); + const panelActions = getService('dashboardPanelActions'); + + describe('embeddable library', () => { + before(async () => { + await esArchiver.load('dashboard/current/kibana'); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.clickNewDashboard(); + }); + + it('unlink visualize panel from embeddable library', async () => { + // add heatmap panel from library + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('Rendering Test: heatmap'); + await find.clickByButtonText('Rendering Test: heatmap'); + await dashboardAddPanel.closeAddPanel(); + + const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:heatmap'); + await panelActions.unlinkFromLibary(originalPanel); + await testSubjects.existOrFail('unlinkPanelSuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:heatmap'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(false); + + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('Rendering Test: heatmap'); + await find.existsByLinkText('Rendering Test: heatmap'); + await dashboardAddPanel.closeAddPanel(); + }); + + it('save visualize panel to embeddable library', async () => { + const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:heatmap'); + await panelActions.saveToLibrary('Rendering Test: heatmap - copy', originalPanel); + await testSubjects.existOrFail('addPanelToLibrarySuccess'); + + const updatedPanel = await testSubjects.find( + 'embeddablePanelHeading-RenderingTest:heatmap-copy' + ); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(true); + }); + + it('unlink map panel from embeddable library', async () => { + // add map panel from library + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('Rendering Test: geo map'); + await find.clickByButtonText('Rendering Test: geo map'); + await dashboardAddPanel.closeAddPanel(); + + const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:geomap'); + await panelActions.unlinkFromLibary(originalPanel); + await testSubjects.existOrFail('unlinkPanelSuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:geomap'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(false); + + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('Rendering Test: geo map'); + await find.existsByLinkText('Rendering Test: geo map'); + await dashboardAddPanel.closeAddPanel(); + }); + + it('save map panel to embeddable library', async () => { + const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:geomap'); + await panelActions.saveToLibrary('Rendering Test: geo map - copy', originalPanel); + await testSubjects.existOrFail('addPanelToLibrarySuccess'); + + const updatedPanel = await testSubjects.find( + 'embeddablePanelHeading-RenderingTest:geomap-copy' + ); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(true); + }); + }); +} diff --git a/test/functional/apps/dashboard/index.ts b/test/functional/apps/dashboard/index.ts index 93325035398749..b71a89501fbf6d 100644 --- a/test/functional/apps/dashboard/index.ts +++ b/test/functional/apps/dashboard/index.ts @@ -81,6 +81,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { // The dashboard_snapshot test below requires the timestamped URL which breaks the view_edit test. // If we don't use the timestamp in the URL, the colors in the charts will be different. loadTestFile(require.resolve('./dashboard_snapshots')); + loadTestFile(require.resolve('./embeddable_library')); }); // Each of these tests call initTests themselves, the way it was originally written. The above tests only load diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts index 534d4cebd92f47..881e3ad4157a40 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -17,6 +17,8 @@ const TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-togglePanel'; const CUSTOMIZE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-ACTION_CUSTOMIZE_PANEL'; const OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ = 'embeddablePanelToggleMenuIcon'; const OPEN_INSPECTOR_TEST_SUBJ = 'embeddablePanelAction-openInspector'; +const LIBRARY_NOTIFICATION_TEST_SUBJ = 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION'; +const SAVE_TO_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-saveToLibrary'; export function DashboardPanelActionsProvider({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); @@ -170,6 +172,29 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }: Ft await testSubjects.click(OPEN_INSPECTOR_TEST_SUBJ); } + async unlinkFromLibary(parent?: WebElementWrapper) { + log.debug('unlinkFromLibrary'); + const libraryNotification = parent + ? await testSubjects.findDescendant(LIBRARY_NOTIFICATION_TEST_SUBJ, parent) + : await testSubjects.find(LIBRARY_NOTIFICATION_TEST_SUBJ); + await libraryNotification.click(); + await testSubjects.click('libraryNotificationUnlinkButton'); + } + + async saveToLibrary(newTitle: string, parent?: WebElementWrapper) { + log.debug('saveToLibrary'); + await this.openContextMenu(parent); + const exists = await testSubjects.exists(SAVE_TO_LIBRARY_TEST_SUBJ); + if (!exists) { + await this.clickContextMenuMoreItem(); + } + await testSubjects.click(SAVE_TO_LIBRARY_TEST_SUBJ); + await testSubjects.setValue('savedObjectTitle', newTitle, { + clearWithKeyboard: true, + }); + await testSubjects.click('confirmSaveSavedObjectButton'); + } + async expectExistsRemovePanelAction() { log.debug('expectExistsRemovePanelAction'); await this.expectExistsPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ); diff --git a/x-pack/test/functional/apps/lens/dashboard.ts b/x-pack/test/functional/apps/lens/dashboard.ts index 738e45c1cbcf1d..5cbd5dff45e1e0 100644 --- a/x-pack/test/functional/apps/lens/dashboard.ts +++ b/x-pack/test/functional/apps/lens/dashboard.ts @@ -156,5 +156,43 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await panelActions.clickContextMenuMoreItem(); await testSubjects.existOrFail(ACTION_TEST_SUBJ); }); + + it('unlink lens panel from embeddable library', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('lnsPieVis'); + await find.clickByButtonText('lnsPieVis'); + await dashboardAddPanel.closeAddPanel(); + + const originalPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis'); + await panelActions.unlinkFromLibary(originalPanel); + await testSubjects.existOrFail('unlinkPanelSuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(false); + }); + + it('save lens panel to embeddable library', async () => { + const originalPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis'); + await panelActions.saveToLibrary('lnsPieVis - copy', originalPanel); + await testSubjects.click('confirmSaveSavedObjectButton'); + await testSubjects.existOrFail('addPanelToLibrarySuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis-copy'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(true); + + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('lnsPieVis'); + await find.existsByLinkText('lnsPieVis'); + }); }); } diff --git a/x-pack/test/functional/apps/maps/embeddable/embeddable_library.js b/x-pack/test/functional/apps/maps/embeddable/embeddable_library.js new file mode 100644 index 00000000000000..40e73f0d8a7632 --- /dev/null +++ b/x-pack/test/functional/apps/maps/embeddable/embeddable_library.js @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +export default function ({ getPageObjects, getService }) { + const find = getService('find'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'maps', 'visualize']); + const kibanaServer = getService('kibanaServer'); + const security = getService('security'); + const dashboardAddPanel = getService('dashboardAddPanel'); + const dashboardPanelActions = getService('dashboardPanelActions'); + const dashboardVisualizations = getService('dashboardVisualizations'); + + describe('maps in embeddable library', () => { + before(async () => { + await security.testUser.setRoles( + [ + 'test_logstash_reader', + 'global_maps_all', + 'geoshape_data_reader', + 'global_dashboard_all', + 'meta_for_geoshape_data_reader', + ], + false + ); + await kibanaServer.uiSettings.replace({ + defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', + }); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickCreateNewLink(); + await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); + await PageObjects.visualize.clickMapsApp(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.maps.waitForLayersToLoad(); + await PageObjects.maps.clickSaveAndReturnButton(); + await PageObjects.dashboard.waitForRenderComplete(); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('save map panel to embeddable library', async () => { + await dashboardPanelActions.saveToLibrary('embeddable library map'); + await testSubjects.existOrFail('addPanelToLibrarySuccess'); + + const mapPanel = await testSubjects.find('embeddablePanelHeading-embeddablelibrarymap'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + mapPanel + ); + expect(libraryActionExists).to.be(true); + }); + + it('unlink map panel from embeddable library', async () => { + const originalPanel = await testSubjects.find('embeddablePanelHeading-embeddablelibrarymap'); + await dashboardPanelActions.unlinkFromLibary(originalPanel); + await testSubjects.existOrFail('unlinkPanelSuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-embeddablelibrarymap'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(false); + + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('embeddable library map'); + await find.existsByLinkText('embeddable library map'); + await dashboardAddPanel.closeAddPanel(); + }); + }); +} diff --git a/x-pack/test/functional/apps/maps/embeddable/index.js b/x-pack/test/functional/apps/maps/embeddable/index.js index 815de2e0813097..9fd4c9db703db9 100644 --- a/x-pack/test/functional/apps/maps/embeddable/index.js +++ b/x-pack/test/functional/apps/maps/embeddable/index.js @@ -9,6 +9,7 @@ export default function ({ loadTestFile }) { describe('embeddable', function () { loadTestFile(require.resolve('./save_and_return')); loadTestFile(require.resolve('./dashboard')); + loadTestFile(require.resolve('./embeddable_library')); loadTestFile(require.resolve('./embeddable_state')); loadTestFile(require.resolve('./tooltip_filter_actions')); }); From 87212e68f71dab0b827cecc78d8e60c9eb821298 Mon Sep 17 00:00:00 2001 From: John Dorlus Date: Mon, 8 Feb 2021 19:08:15 -0500 Subject: [PATCH 51/51] [Upgrade Assistant] Add A11y Tests (#90265) * Added overview a11y test. Added data test subjects to the tabs in the app. * Added data test subjects for all tabs and the detail panel on the indidual pages. Updated tests to wait for detail panel to be visible before taking snapshot. * Updated snapshot for upgrade assistant jest tests. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/components/tabs.tsx | 3 ++ .../__snapshots__/checkup_tab.test.tsx.snap | 12 +++-- .../components/tabs/checkup/checkup_tab.tsx | 6 ++- .../components/tabs/overview/index.tsx | 2 +- .../accessibility/apps/upgrade_assistant.ts | 44 +++++++++++++++++++ x-pack/test/accessibility/config.ts | 1 + 6 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 x-pack/test/accessibility/apps/upgrade_assistant.ts diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx index d77349e53b354f..fa6badb34635bb 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx @@ -190,6 +190,7 @@ export class UpgradeAssistantTabs extends React.Component { return [ { id: 'overview', + 'data-test-subj': 'upgradeAssistantOverviewTab', name: i18n.translate('xpack.upgradeAssistant.overviewTab.overviewTabTitle', { defaultMessage: 'Overview', }), @@ -197,6 +198,7 @@ export class UpgradeAssistantTabs extends React.Component { }, { id: 'cluster', + 'data-test-subj': 'upgradeAssistantClusterTab', name: i18n.translate('xpack.upgradeAssistant.checkupTab.clusterTabLabel', { defaultMessage: 'Cluster', }), @@ -213,6 +215,7 @@ export class UpgradeAssistantTabs extends React.Component { }, { id: 'indices', + 'data-test-subj': 'upgradeAssistantIndicesTab', name: i18n.translate('xpack.upgradeAssistant.checkupTab.indicesTabLabel', { defaultMessage: 'Indices', }), diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap index 5aa4a469e4f020..bac67bf722ea74 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap @@ -6,7 +6,9 @@ exports[`CheckupTab render with deprecations 1`] = ` -

+

-

+

-

+

= ({ <> -

+

= (props) <> - +

{ + before(async () => { + await PageObjects.upgradeAssistant.navigateToPage(); + }); + + it('Overview Tab', async () => { + await retry.waitFor('Upgrade Assistant overview tab to be visible', async () => { + return testSubjects.exists('upgradeAssistantOverviewTabDetail'); + }); + await a11y.testAppSnapshot(); + }); + + it('Cluster Tab', async () => { + await testSubjects.click('upgradeAssistantClusterTab'); + await retry.waitFor('Upgrade Assistant Cluster tab to be visible', async () => { + return testSubjects.exists('upgradeAssistantClusterTabDetail'); + }); + await a11y.testAppSnapshot(); + }); + + it('Indices Tab', async () => { + await testSubjects.click('upgradeAssistantIndicesTab'); + await retry.waitFor('Upgrade Assistant Cluster tab to be visible', async () => { + return testSubjects.exists('upgradeAssistantIndexTabDetail'); + }); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index 94a09e3f767f6a..24c46c1a1687e6 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -31,6 +31,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/index_lifecycle_management'), require.resolve('./apps/ml'), require.resolve('./apps/lens'), + require.resolve('./apps/upgrade_assistant'), ], pageObjects,