From 4b7fec4a98ed481d6665e76364d644bbac988af4 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 12 Dec 2019 11:24:41 -0500 Subject: [PATCH 01/36] [Watcher] Removed overwritten property (#49998) (#52891) --- .../public/np_ready/application/models/action/index_action.js | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/index_action.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/index_action.js index 7276ef59a3fc3..de84951cc3d27 100644 --- a/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/index_action.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/index_action.js @@ -35,7 +35,6 @@ export class IndexAction extends BaseAction { const result = super.upstreamJson; Object.assign(result, { - index: this.index, index: { index: this.index, } From 4dd2d040fc6e77bc47e37ee45d697dfb5169fbe0 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Thu, 12 Dec 2019 08:49:17 -0800 Subject: [PATCH 02/36] [DOCS] Adds example of assigning roles in Reporting (#52757) (#52911) * [DOCS] Adds example of assigning roles in Reporting * [DOCS] Updates reporting security doc with review comments * [DOCS] Incorporates review comments in reporting doc --- .../images/reporting-privileges-example.png | Bin 0 -> 96808 bytes docs/user/security/reporting.asciidoc | 63 +++++++++++++++--- 2 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 docs/user/security/images/reporting-privileges-example.png diff --git a/docs/user/security/images/reporting-privileges-example.png b/docs/user/security/images/reporting-privileges-example.png new file mode 100644 index 0000000000000000000000000000000000000000..d108fe6634fa2b75d183e2f196a79cd943409412 GIT binary patch literal 96808 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%0DH}s-wzV<6Xpz2pz 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 EAHA=YYybcN literal 0 HcmV?d00001 diff --git a/docs/user/security/reporting.asciidoc b/docs/user/security/reporting.asciidoc index aaba60ca4b3ca..86599be9af375 100644 --- a/docs/user/security/reporting.asciidoc +++ b/docs/user/security/reporting.asciidoc @@ -9,19 +9,60 @@ To use {reporting} with {security} enabled, you need to <>. If you are automatically generating reports with {ref}/xpack-alerting.html[{watcher}], you also need to configure {watcher} -to trust the {kib} server's certificate. For more information, see +to trust the {kib} server's certificate. +//// +For more information, see <>. +//// [[reporting-app-users]] -To enable users to generate reports, assign them the built-in `reporting_user` -role. Users will also need the appropriate <> to access the objects +To enable users to generate reports, you must assign them the built-in `reporting_user` +role. Users will also need the appropriate <> to access the objects to report on and the {es} indices. -* If you're using the `native` realm, you can assign roles through -**Management > Users** UI in Kibana or with the `user` API. For example, -the following request creates a `reporter` user that has the -`reporting_user` role and the `kibana_user` role: +[float] +[[reporting-roles-management-ui]] +=== If you are using the `native` realm + +You can assign roles through the +*Management* app in Kibana or with the <>. +This example shows how to use *Management* to create a user who has a custom role and the +`reporting_user` role. + +. Go to *Management > Roles*, and click *Create role*. + +. Give the new role a name, for example, `custom_reporting_user`. + +. Specify the indices and privileges. + +Access to data is an index-level privilege, so in *Create role*, +add a line for each index that contains the data for the report and give each +index `read` and `view_index_metadata` privileges. +For more information, see {ref}/security-privileges.html[Security privileges]. ++ +[role="screenshot"] +image::user/security/images/reporting-privileges-example.png["Reporting privileges"] + +. Add space privileges. ++ +Reporting users typically save searches, create +visualizations, and build dashboards. They require a space +that provides read and write privileges in +*Discover*, *Visualize*, and *Dashboard*. + +. Save your new role. + +. Create a user account with the proper roles. ++ +Go to *Management > Users*, add a new user, and assign the user the built-in +`reporting_user` role and your new custom role, `custom_reporting_user`. + +[float] +[[reporting-roles-user-api]] +==== With the user API +This example uses the {ref}/security-api-put-user.html[user API] to create a user who has the +`reporting_user` role and the `kibana_user` role: + [source, sh] --------------------------------------------------------------- POST /_security/user/reporter @@ -32,13 +73,17 @@ POST /_security/user/reporter } --------------------------------------------------------------- -* If you are using an LDAP or Active Directory realm, you can either assign +[float] +=== If you are using an external identity provider + +If you are using an external identity provider, such as +LDAP or Active Directory, you can either assign roles on a per user basis, or assign roles to groups of users. By default, role mappings are configured in {ref}/mapping-roles.html[`config/shield/role_mapping.yml`]. For example, the following snippet assigns the user named Bill Murray the `kibana_user` and `reporting_user` roles: -+ + [source,yaml] -------------------------------------------------------------------------------- kibana_user: From 31b814453a7932573f5b802e03e9942194a2c433 Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Thu, 12 Dec 2019 17:52:26 +0100 Subject: [PATCH 03/36] Add babel-plugin-styled-components to webpack config (#52862) (#52898) --- package.json | 2 +- packages/kbn-babel-preset/package.json | 1 + packages/kbn-babel-preset/webpack_preset.js | 8 +++++++- x-pack/package.json | 2 +- yarn.lock | 19 ++++++++++++++----- 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index ab14f5b4e26ac..4dd5922d3565f 100644 --- a/package.json +++ b/package.json @@ -351,7 +351,7 @@ "@types/semver": "^5.5.0", "@types/sinon": "^7.0.13", "@types/strip-ansi": "^3.0.0", - "@types/styled-components": "^4.4.0", + "@types/styled-components": "^4.4.1", "@types/supertest": "^2.0.5", "@types/supertest-as-promised": "^2.0.38", "@types/testing-library__react": "^9.1.2", diff --git a/packages/kbn-babel-preset/package.json b/packages/kbn-babel-preset/package.json index e554859928c0b..c8bb4a568c500 100644 --- a/packages/kbn-babel-preset/package.json +++ b/packages/kbn-babel-preset/package.json @@ -14,6 +14,7 @@ "@babel/preset-typescript": "^7.3.3", "babel-plugin-add-module-exports": "^1.0.2", "babel-plugin-filter-imports": "^3.0.0", + "babel-plugin-styled-components": "^1.10.6", "babel-plugin-transform-define": "^1.3.1", "babel-plugin-typescript-strip-namespaces": "^1.1.1" } diff --git a/packages/kbn-babel-preset/webpack_preset.js b/packages/kbn-babel-preset/webpack_preset.js index def848f4154bb..e6a8bd81b602e 100644 --- a/packages/kbn-babel-preset/webpack_preset.js +++ b/packages/kbn-babel-preset/webpack_preset.js @@ -33,6 +33,12 @@ module.exports = () => { plugins: [ require.resolve('@babel/plugin-transform-modules-commonjs'), require.resolve('@babel/plugin-syntax-dynamic-import'), - ] + [ + require.resolve('babel-plugin-styled-components'), + { + fileName: false, + }, + ], + ], }; }; diff --git a/x-pack/package.json b/x-pack/package.json index a5794d589d78f..870c5f741ab6d 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -98,7 +98,7 @@ "@types/reduce-reducers": "^0.3.0", "@types/redux-actions": "^2.2.1", "@types/sinon": "^7.0.13", - "@types/styled-components": "^4.4.0", + "@types/styled-components": "^4.4.1", "@types/supertest": "^2.0.5", "@types/tar-fs": "^1.16.1", "@types/tinycolor2": "^1.4.1", diff --git a/yarn.lock b/yarn.lock index 37ccf74319796..9d097368aeab7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3402,6 +3402,14 @@ resolved "https://registry.yarnpkg.com/@types/hoek/-/hoek-4.1.3.tgz#d1982d48fb0d2a0e5d7e9d91838264d8e428d337" integrity sha1-0ZgtSPsNKg5dfp2Rg4Jk2OQo0zc= +"@types/hoist-non-react-statics@*": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/indent-string@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/indent-string/-/indent-string-3.0.0.tgz#9ebb391ceda548926f5819ad16405349641b999f" @@ -3994,11 +4002,12 @@ dependencies: "@types/node" "*" -"@types/styled-components@^4.4.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-4.4.0.tgz#15a3d59533fd3a5bd013db4a7c4422ec542c59d2" - integrity sha512-QFl+w3hQJNHE64Or3PXMFpC3HAQDiuQLi5o9m1XPEwYWfgCZtAribO5ksjxnO8U0LG8Parh0ESCgVxo4VfxlHg== +"@types/styled-components@^4.4.1": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-4.4.1.tgz#bc40cf5ce0708032f4b148b04ab3c470d3e74026" + integrity sha512-cQXT4pkAkM0unk/s26UBrJx9RmJ2rNUn2aDTgzp1rtu+tTkScebE78jbxNWhlqkA43XF3d41CcDlyl9Ldotm2g== dependencies: + "@types/hoist-non-react-statics" "*" "@types/react" "*" "@types/react-native" "*" csstype "^2.2.0" @@ -6151,7 +6160,7 @@ babel-plugin-react-docgen@^3.0.0: resolved "https://registry.yarnpkg.com/babel-plugin-require-context-hook-babel7/-/babel-plugin-require-context-hook-babel7-1.0.0.tgz#1273d4cee7e343d0860966653759a45d727e815d" integrity sha512-kez0BAN/cQoyO1Yu1nre1bQSYZEF93Fg7VQiBHFfMWuaZTy7vJSTT4FY68FwHTYG53Nyt0A7vpSObSVxwweQeQ== -"babel-plugin-styled-components@>= 1": +"babel-plugin-styled-components@>= 1", babel-plugin-styled-components@^1.10.6: version "1.10.6" resolved "https://registry.yarnpkg.com/babel-plugin-styled-components/-/babel-plugin-styled-components-1.10.6.tgz#f8782953751115faf09a9f92431436912c34006b" integrity sha512-gyQj/Zf1kQti66100PhrCRjI5ldjaze9O0M3emXRPAN80Zsf8+e1thpTpaXJXVHXtaM4/+dJEgZHyS9Its+8SA== From b2478f874a3b2f77dd6fcc724b0a71fe9f413ec7 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Thu, 12 Dec 2019 17:58:40 +0100 Subject: [PATCH 04/36] Hide stderr git output during APM agent configuration (#52878) (#52890) --- config/apm.js | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/config/apm.js b/config/apm.js index 8efbbf87487e3..0cfcd759f163b 100644 --- a/config/apm.js +++ b/config/apm.js @@ -42,19 +42,22 @@ const { join } = require('path'); const { execSync } = require('child_process'); const merge = require('lodash.merge'); -module.exports = merge({ - active: false, - serverUrl: 'https://f1542b814f674090afd914960583265f.apm.us-central1.gcp.cloud.es.io:443', - // The secretToken below is intended to be hardcoded in this file even though - // it makes it public. This is not a security/privacy issue. Normally we'd - // instead disable the need for a secretToken in the APM Server config where - // the data is transmitted to, but due to how it's being hosted, it's easier, - // for now, to simply leave it in. - secretToken: 'R0Gjg46pE9K9wGestd', - globalLabels: {}, - centralConfig: false, - logUncaughtExceptions: true -}, devConfig()); +module.exports = merge( + { + active: false, + serverUrl: 'https://f1542b814f674090afd914960583265f.apm.us-central1.gcp.cloud.es.io:443', + // The secretToken below is intended to be hardcoded in this file even though + // it makes it public. This is not a security/privacy issue. Normally we'd + // instead disable the need for a secretToken in the APM Server config where + // the data is transmitted to, but due to how it's being hosted, it's easier, + // for now, to simply leave it in. + secretToken: 'R0Gjg46pE9K9wGestd', + globalLabels: {}, + centralConfig: false, + logUncaughtExceptions: true, + }, + devConfig() +); const rev = gitRev(); if (rev !== null) module.exports.globalLabels.git_rev = rev; @@ -66,7 +69,10 @@ try { function gitRev() { try { - return execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim(); + return execSync('git rev-parse --short HEAD', { + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); } catch (e) { return null; } From fdb94cc992fbb69035ee249c1cd065fd74e28691 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Thu, 12 Dec 2019 17:58:55 +0100 Subject: [PATCH 05/36] Ensure APM agent config file path respects CWD (#52880) (#52892) --- src/apm.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/apm.js b/src/apm.js index 04a70ee71c53e..5effefaccd029 100644 --- a/src/apm.js +++ b/src/apm.js @@ -25,15 +25,13 @@ module.exports = function (serviceName = name) { if (process.env.kbnWorkerType === 'optmzr') return; const conf = { - serviceName: `${serviceName}-${version.replace(/\./g, '_')}` + serviceName: `${serviceName}-${version.replace(/\./g, '_')}`, }; - if (configFileExists()) conf.configFile = 'config/apm.js'; + const configFile = join(__dirname, '..', 'config', 'apm.js'); + + if (existsSync(configFile)) conf.configFile = configFile; else conf.active = false; require('elastic-apm-node').start(conf); }; - -function configFileExists() { - return existsSync(join(__dirname, '..', 'config', 'apm.js')); -} From 944729c227465c53e96bd5bf7397759d91c81f9b Mon Sep 17 00:00:00 2001 From: Gil Raphaelli Date: Thu, 12 Dec 2019 12:02:15 -0500 Subject: [PATCH 06/36] upgrade APM migration script v1 support (#52824) * assume processor.event-less docs are from onboarding * try harder to set a parent.id in spans --- .../server/np_ready/lib/apm/index.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/apm/index.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/apm/index.ts index 153c8a54404b8..29798a7a20fc1 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/apm/index.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/apm/index.ts @@ -66,6 +66,12 @@ export const apmReindexScript = ` // add ecs version ctx._source.ecs = ['version': '1.1.0-dev']; + // set processor.event + if (ctx._source.processor == null) { + // onboarding docs had no processor pre-6.4 - https://github.com/elastic/kibana/issues/52655 + ctx._source.processor = ["event": "onboarding"]; + } + // beat -> observer def beat = ctx._source.remove("beat"); if (beat != null) { @@ -428,8 +434,14 @@ export const apmReindexScript = ` ctx._source.span.id = span_id.toString(); } def parent = ctx._source.span.remove("parent"); - if (parent != null && ctx._source.parent == null) { - ctx._source.parent = ["id": parent.toString()]; + def tr_id = ctx._source.transaction.get("id"); + if (ctx._source.parent == null) { + if (parent != null) { + ctx._source.parent = ["id": parent.toString()]; + } else if (tr_id != null) { + // 7.x UI requires a value for parent.id - https://github.com/elastic/kibana/issues/52763 + ctx._source.parent = ["id": tr_id]; + } } } From f052081c14da9ab567a7d99c5acca770f148c747 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Thu, 12 Dec 2019 11:44:44 -0600 Subject: [PATCH 07/36] [Logs UI] Refactor log entry data fetching to hooks (#51526) (#52582) * Get initialinitial log fetch working with v2 store * Replicate shouldLoadAroundPosition logic within hooks * Reload entries on filter change * Add scroll to load additional entries functionality * Cleanup types types and remove state/remote folder * Typescript cleanup * Remove extraneous console.log * Fix typecheck * Add action to load new entries manually * Typecheck fix * Move v2 store stuff into logs containers * Typecheck fix * More typecheck fix * Remove filterQuery from log highlights redux bridge * Rename LogEntriesDependencies to LogEntriesFetchParams * Fix endless reloading bug * Fix duplicate entry rendering * Make sourceId into a dynamic parameter * Fix bug in pagesAfterEnd not being reported causing endless reload * Fix bugs with live streaming --- .../plugins/infra/public/apps/start_app.tsx | 21 +- .../log_text_stream/loading_item_view.tsx | 2 +- .../scrollable_log_text_stream_view.tsx | 4 +- .../log_text_stream/vertical_scroll_panel.tsx | 6 +- .../logs/log_entries/gql_queries.ts | 64 +++++ .../containers/logs/log_entries/index.ts | 268 ++++++++++++++++++ .../containers/logs/log_entries/types.ts | 75 +++++ .../containers/logs/log_filter/index.ts | 26 ++ .../logs/log_highlights/log_highlights.tsx | 25 +- .../log_highlights/redux_bridge_setters.tsx | 6 - .../logs/log_highlights/redux_bridges.tsx | 13 - .../containers/logs/log_position/index.ts | 35 +++ .../containers/logs/with_stream_items.ts | 122 +++----- .../log_entries.gql_query.ts | 2 +- .../pages/logs/stream/page_logs_content.tsx | 12 +- .../pages/logs/stream/page_providers.tsx | 45 ++- .../plugins/infra/public/store/actions.ts | 1 - .../plugins/infra/public/store/epics.ts | 4 +- .../plugins/infra/public/store/local/epic.ts | 4 +- .../public/store/local/log_position/index.ts | 1 - .../store/local/log_position/reducer.ts | 35 +-- .../store/local/log_position/selectors.ts | 9 +- .../plugins/infra/public/store/reducer.ts | 4 - .../infra/public/store/remote/actions.ts | 7 - .../plugins/infra/public/store/remote/epic.ts | 9 - .../infra/public/store/remote/index.ts | 10 - .../store/remote/log_entries/actions.ts | 21 -- .../public/store/remote/log_entries/epic.ts | 198 ------------- .../public/store/remote/log_entries/index.ts | 13 - .../remote/log_entries/operations/load.ts | 35 --- .../log_entries/operations/load_more.ts | 72 ----- .../store/remote/log_entries/reducer.ts | 17 -- .../store/remote/log_entries/selectors.ts | 71 ----- .../public/store/remote/log_entries/state.ts | 16 -- .../infra/public/store/remote/reducer.ts | 20 -- .../infra/public/store/remote/selectors.ts | 14 - .../plugins/infra/public/store/selectors.ts | 38 --- .../plugins/infra/public/store/store.ts | 6 - .../infra/public/utils/redux_context.tsx | 16 ++ .../remote_state/remote_graphql_state.ts | 214 -------------- 40 files changed, 620 insertions(+), 941 deletions(-) create mode 100644 x-pack/legacy/plugins/infra/public/containers/logs/log_entries/gql_queries.ts create mode 100644 x-pack/legacy/plugins/infra/public/containers/logs/log_entries/index.ts create mode 100644 x-pack/legacy/plugins/infra/public/containers/logs/log_entries/types.ts create mode 100644 x-pack/legacy/plugins/infra/public/containers/logs/log_filter/index.ts create mode 100644 x-pack/legacy/plugins/infra/public/containers/logs/log_position/index.ts rename x-pack/legacy/plugins/infra/public/{store/remote/log_entries/operations => graphql}/log_entries.gql_query.ts (93%) delete mode 100644 x-pack/legacy/plugins/infra/public/store/remote/actions.ts delete mode 100644 x-pack/legacy/plugins/infra/public/store/remote/epic.ts delete mode 100644 x-pack/legacy/plugins/infra/public/store/remote/index.ts delete mode 100644 x-pack/legacy/plugins/infra/public/store/remote/log_entries/actions.ts delete mode 100644 x-pack/legacy/plugins/infra/public/store/remote/log_entries/epic.ts delete mode 100644 x-pack/legacy/plugins/infra/public/store/remote/log_entries/index.ts delete mode 100644 x-pack/legacy/plugins/infra/public/store/remote/log_entries/operations/load.ts delete mode 100644 x-pack/legacy/plugins/infra/public/store/remote/log_entries/operations/load_more.ts delete mode 100644 x-pack/legacy/plugins/infra/public/store/remote/log_entries/reducer.ts delete mode 100644 x-pack/legacy/plugins/infra/public/store/remote/log_entries/selectors.ts delete mode 100644 x-pack/legacy/plugins/infra/public/store/remote/log_entries/state.ts delete mode 100644 x-pack/legacy/plugins/infra/public/store/remote/reducer.ts delete mode 100644 x-pack/legacy/plugins/infra/public/store/remote/selectors.ts create mode 100644 x-pack/legacy/plugins/infra/public/utils/redux_context.tsx delete mode 100644 x-pack/legacy/plugins/infra/public/utils/remote_state/remote_graphql_state.ts diff --git a/x-pack/legacy/plugins/infra/public/apps/start_app.tsx b/x-pack/legacy/plugins/infra/public/apps/start_app.tsx index cd28114327b6d..41479cf6351ec 100644 --- a/x-pack/legacy/plugins/infra/public/apps/start_app.tsx +++ b/x-pack/legacy/plugins/infra/public/apps/start_app.tsx @@ -21,6 +21,7 @@ import { InfraFrontendLibs } from '../lib/lib'; import { PageRouter } from '../routes'; import { createStore } from '../store'; import { ApolloClientContext } from '../utils/apollo_context'; +import { ReduxStateContextProvider } from '../utils/redux_context'; import { HistoryContext } from '../utils/history_context'; import { useUiSetting$, @@ -46,15 +47,17 @@ export async function startApp(libs: InfraFrontendLibs) { - - - - - - - - - + + + + + + + + + + + diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/loading_item_view.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/loading_item_view.tsx index 549ca4c1ae047..4cefbea7225ec 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/loading_item_view.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/loading_item_view.tsx @@ -18,7 +18,7 @@ interface LogTextStreamLoadingItemViewProps { hasMore: boolean; isLoading: boolean; isStreaming: boolean; - lastStreamingUpdate: number | null; + lastStreamingUpdate: Date | null; onLoadMore?: () => void; } diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx index 674c3f59ce957..a5b85788fdea9 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx @@ -39,7 +39,7 @@ interface ScrollableLogTextStreamViewProps { hasMoreBeforeStart: boolean; hasMoreAfterEnd: boolean; isStreaming: boolean; - lastLoadedTime: number | null; + lastLoadedTime: Date | null; target: TimeKey | null; jumpToTarget: (target: TimeKey) => any; reportVisibleInterval: (params: { @@ -143,7 +143,7 @@ export class ScrollableLogTextStreamView extends React.PureComponent< const hasItems = items.length > 0; return ( - {isReloading && !hasItems ? ( + {isReloading && (!isStreaming || !hasItems) ? ( extends React.PureComponent< // Flag the scrollTop change that's about to happen as programmatic, as // opposed to being in direct response to user input this.nextScrollEventFromCenterTarget = true; - scrollRef.current.scrollTop = targetDimensions.top + targetOffset - scrollViewHeight / 2; - return true; + const currentScrollTop = scrollRef.current.scrollTop; + const newScrollTop = targetDimensions.top + targetOffset - scrollViewHeight / 2; + scrollRef.current.scrollTop = newScrollTop; + return currentScrollTop !== newScrollTop; } return false; }; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_entries/gql_queries.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_entries/gql_queries.ts new file mode 100644 index 0000000000000..83bae37c348d4 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_entries/gql_queries.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ApolloClient } from 'apollo-client'; +import { TimeKey } from '../../../../common/time'; +import { logEntriesQuery } from '../../../graphql/log_entries.gql_query'; +import { useApolloClient } from '../../../utils/apollo_context'; +import { LogEntriesResponse } from '.'; + +const LOAD_CHUNK_SIZE = 200; + +type LogEntriesGetter = ( + client: ApolloClient<{}>, + countBefore: number, + countAfter: number +) => (params: { + sourceId: string; + timeKey: TimeKey | null; + filterQuery: string | null; +}) => Promise; + +const getLogEntries: LogEntriesGetter = (client, countBefore, countAfter) => async ({ + sourceId, + timeKey, + filterQuery, +}) => { + if (!timeKey) throw new Error('TimeKey is null'); + const result = await client.query({ + query: logEntriesQuery, + variables: { + sourceId, + timeKey: { time: timeKey.time, tiebreaker: timeKey.tiebreaker }, + countBefore, + countAfter, + filterQuery, + }, + fetchPolicy: 'no-cache', + }); + // Workaround for Typescript. Since we're removing the GraphQL API in another PR or two + // 7.6 goes out I don't think it's worth the effort to actually make this + // typecheck pass + const { source } = result.data as any; + const { logEntriesAround } = source; + return { + entries: logEntriesAround.entries, + entriesStart: logEntriesAround.start, + entriesEnd: logEntriesAround.end, + hasMoreAfterEnd: logEntriesAround.hasMoreAfter, + hasMoreBeforeStart: logEntriesAround.hasMoreBefore, + lastLoadedTime: new Date(), + }; +}; + +export const useGraphQLQueries = () => { + const client = useApolloClient(); + if (!client) throw new Error('Unable to get Apollo Client from context'); + return { + getLogEntriesAround: getLogEntries(client, LOAD_CHUNK_SIZE, LOAD_CHUNK_SIZE), + getLogEntriesBefore: getLogEntries(client, LOAD_CHUNK_SIZE, 0), + getLogEntriesAfter: getLogEntries(client, 0, LOAD_CHUNK_SIZE), + }; +}; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_entries/index.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_entries/index.ts new file mode 100644 index 0000000000000..3020ad7eb5f84 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_entries/index.ts @@ -0,0 +1,268 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { useEffect, useState, useReducer, useCallback } from 'react'; +import createContainer from 'constate'; +import { pick, throttle } from 'lodash'; +import { useGraphQLQueries } from './gql_queries'; +import { TimeKey, timeKeyIsBetween } from '../../../../common/time'; +import { InfraLogEntry } from './types'; + +const DESIRED_BUFFER_PAGES = 2; + +enum Action { + FetchingNewEntries, + FetchingMoreEntries, + ReceiveNewEntries, + ReceiveEntriesBefore, + ReceiveEntriesAfter, + ErrorOnNewEntries, + ErrorOnMoreEntries, +} + +type ReceiveActions = + | Action.ReceiveNewEntries + | Action.ReceiveEntriesBefore + | Action.ReceiveEntriesAfter; + +interface ReceiveEntriesAction { + type: ReceiveActions; + payload: LogEntriesResponse; +} +interface FetchOrErrorAction { + type: Exclude; +} +type ActionObj = ReceiveEntriesAction | FetchOrErrorAction; + +type Dispatch = (action: ActionObj) => void; + +interface LogEntriesProps { + filterQuery: string | null; + timeKey: TimeKey | null; + pagesBeforeStart: number | null; + pagesAfterEnd: number | null; + sourceId: string; + isAutoReloading: boolean; +} + +type FetchEntriesParams = Omit; +type FetchMoreEntriesParams = Pick; + +export interface LogEntriesResponse { + entries: InfraLogEntry[]; + entriesStart: TimeKey | null; + entriesEnd: TimeKey | null; + hasMoreAfterEnd: boolean; + hasMoreBeforeStart: boolean; + lastLoadedTime: Date | null; +} + +export type LogEntriesStateParams = { + isReloading: boolean; + isLoadingMore: boolean; +} & LogEntriesResponse; + +export interface LogEntriesCallbacks { + fetchNewerEntries: () => Promise; +} +export const logEntriesInitialCallbacks = { + fetchNewerEntries: async () => {}, +}; + +export const logEntriesInitialState: LogEntriesStateParams = { + entries: [], + entriesStart: null, + entriesEnd: null, + hasMoreAfterEnd: false, + hasMoreBeforeStart: false, + isReloading: true, + isLoadingMore: false, + lastLoadedTime: null, +}; + +const cleanDuplicateItems = (entriesA: InfraLogEntry[], entriesB: InfraLogEntry[]) => { + const gids = new Set(entriesB.map(item => item.gid)); + return entriesA.filter(item => !gids.has(item.gid)); +}; + +const shouldFetchNewEntries = ({ + prevParams, + timeKey, + filterQuery, + entriesStart, + entriesEnd, +}: FetchEntriesParams & LogEntriesStateParams & { prevParams: FetchEntriesParams }) => { + if (!timeKey) return false; + const shouldLoadWithNewFilter = filterQuery !== prevParams.filterQuery; + const shouldLoadAroundNewPosition = + !entriesStart || !entriesEnd || !timeKeyIsBetween(entriesStart, entriesEnd, timeKey); + return shouldLoadWithNewFilter || shouldLoadAroundNewPosition; +}; + +enum ShouldFetchMoreEntries { + Before, + After, +} + +const shouldFetchMoreEntries = ( + { pagesAfterEnd, pagesBeforeStart }: FetchMoreEntriesParams, + { hasMoreBeforeStart, hasMoreAfterEnd }: LogEntriesStateParams +) => { + if (pagesBeforeStart === null || pagesAfterEnd === null) return false; + if (pagesBeforeStart < DESIRED_BUFFER_PAGES && hasMoreBeforeStart) + return ShouldFetchMoreEntries.Before; + if (pagesAfterEnd < DESIRED_BUFFER_PAGES && hasMoreAfterEnd) return ShouldFetchMoreEntries.After; + return false; +}; + +const useFetchEntriesEffect = ( + state: LogEntriesStateParams, + dispatch: Dispatch, + props: LogEntriesProps +) => { + const { getLogEntriesAround, getLogEntriesBefore, getLogEntriesAfter } = useGraphQLQueries(); + + const [prevParams, cachePrevParams] = useState(props); + const [startedStreaming, setStartedStreaming] = useState(false); + + const runFetchNewEntriesRequest = async () => { + dispatch({ type: Action.FetchingNewEntries }); + try { + const payload = await getLogEntriesAround(props); + dispatch({ type: Action.ReceiveNewEntries, payload }); + } catch (e) { + dispatch({ type: Action.ErrorOnNewEntries }); + } + }; + + const runFetchMoreEntriesRequest = async (direction: ShouldFetchMoreEntries) => { + dispatch({ type: Action.FetchingMoreEntries }); + const getEntriesBefore = direction === ShouldFetchMoreEntries.Before; + const timeKey = getEntriesBefore + ? state.entries[0].key + : state.entries[state.entries.length - 1].key; + const getMoreLogEntries = getEntriesBefore ? getLogEntriesBefore : getLogEntriesAfter; + try { + const payload = await getMoreLogEntries({ ...props, timeKey }); + dispatch({ + type: getEntriesBefore ? Action.ReceiveEntriesBefore : Action.ReceiveEntriesAfter, + payload, + }); + } catch (e) { + dispatch({ type: Action.ErrorOnMoreEntries }); + } + }; + + const fetchNewEntriesEffectDependencies = Object.values( + pick(props, ['sourceId', 'filterQuery', 'timeKey']) + ); + const fetchNewEntriesEffect = () => { + if (props.isAutoReloading) return; + if (shouldFetchNewEntries({ ...props, ...state, prevParams })) { + runFetchNewEntriesRequest(); + } + cachePrevParams(props); + }; + + const fetchMoreEntriesEffectDependencies = [ + ...Object.values(pick(props, ['pagesAfterEnd', 'pagesBeforeStart'])), + Object.values(pick(state, ['hasMoreBeforeStart', 'hasMoreAfterEnd'])), + ]; + const fetchMoreEntriesEffect = () => { + if (state.isLoadingMore || props.isAutoReloading) return; + const direction = shouldFetchMoreEntries(props, state); + switch (direction) { + case ShouldFetchMoreEntries.Before: + case ShouldFetchMoreEntries.After: + runFetchMoreEntriesRequest(direction); + break; + default: + break; + } + }; + + const fetchNewerEntries = useCallback( + throttle(() => runFetchMoreEntriesRequest(ShouldFetchMoreEntries.After), 500), + [props] + ); + + const streamEntriesEffectDependencies = [props.isAutoReloading, state.isLoadingMore]; + const streamEntriesEffect = () => { + (async () => { + if (props.isAutoReloading && !state.isLoadingMore) { + if (startedStreaming) { + await new Promise(res => setTimeout(res, 5000)); + } else { + setStartedStreaming(true); + } + fetchNewerEntries(); + } else if (!props.isAutoReloading) { + setStartedStreaming(false); + } + })(); + }; + + useEffect(fetchNewEntriesEffect, fetchNewEntriesEffectDependencies); + useEffect(fetchMoreEntriesEffect, fetchMoreEntriesEffectDependencies); + useEffect(streamEntriesEffect, streamEntriesEffectDependencies); + + return { fetchNewerEntries }; +}; + +export const useLogEntriesState: ( + props: LogEntriesProps +) => [LogEntriesStateParams, LogEntriesCallbacks] = props => { + const [state, dispatch] = useReducer(logEntriesStateReducer, logEntriesInitialState); + + const { fetchNewerEntries } = useFetchEntriesEffect(state, dispatch, props); + const callbacks = { fetchNewerEntries }; + + return [state, callbacks]; +}; + +const logEntriesStateReducer = (prevState: LogEntriesStateParams, action: ActionObj) => { + switch (action.type) { + case Action.ReceiveNewEntries: + return { ...prevState, ...action.payload, isReloading: false }; + case Action.ReceiveEntriesBefore: { + const prevEntries = cleanDuplicateItems(prevState.entries, action.payload.entries); + const newEntries = [...action.payload.entries, ...prevEntries]; + const { hasMoreBeforeStart, entriesStart, lastLoadedTime } = action.payload; + const update = { + entries: newEntries, + isLoadingMore: false, + hasMoreBeforeStart, + entriesStart, + lastLoadedTime, + }; + return { ...prevState, ...update }; + } + case Action.ReceiveEntriesAfter: { + const prevEntries = cleanDuplicateItems(prevState.entries, action.payload.entries); + const newEntries = [...prevEntries, ...action.payload.entries]; + const { hasMoreAfterEnd, entriesEnd, lastLoadedTime } = action.payload; + const update = { + entries: newEntries, + isLoadingMore: false, + hasMoreAfterEnd, + entriesEnd, + lastLoadedTime, + }; + return { ...prevState, ...update }; + } + case Action.FetchingNewEntries: + return { ...prevState, isReloading: true }; + case Action.FetchingMoreEntries: + return { ...prevState, isLoadingMore: true }; + case Action.ErrorOnNewEntries: + return { ...prevState, isReloading: false }; + case Action.ErrorOnMoreEntries: + return { ...prevState, isLoadingMore: false }; + default: + throw new Error(); + } +}; + +export const LogEntriesState = createContainer(useLogEntriesState); diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_entries/types.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_entries/types.ts new file mode 100644 index 0000000000000..75aea8c415eee --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_entries/types.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** A segment of the log entry message that was derived from a field */ +export interface InfraLogMessageFieldSegment { + /** The field the segment was derived from */ + field: string; + /** The segment's message */ + value: string; + /** A list of highlighted substrings of the value */ + highlights: string[]; +} +/** A segment of the log entry message that was derived from a string literal */ +export interface InfraLogMessageConstantSegment { + /** The segment's message */ + constant: string; +} + +export type InfraLogMessageSegment = InfraLogMessageFieldSegment | InfraLogMessageConstantSegment; + +/** A special built-in column that contains the log entry's timestamp */ +export interface InfraLogEntryTimestampColumn { + /** The id of the corresponding column configuration */ + columnId: string; + /** The timestamp */ + timestamp: number; +} +/** A special built-in column that contains the log entry's constructed message */ +export interface InfraLogEntryMessageColumn { + /** The id of the corresponding column configuration */ + columnId: string; + /** A list of the formatted log entry segments */ + message: InfraLogMessageSegment[]; +} + +/** A column that contains the value of a field of the log entry */ +export interface InfraLogEntryFieldColumn { + /** The id of the corresponding column configuration */ + columnId: string; + /** The field name of the column */ + field: string; + /** The value of the field in the log entry */ + value: string; + /** A list of highlighted substrings of the value */ + highlights: string[]; +} + +/** A column of a log entry */ +export type InfraLogEntryColumn = + | InfraLogEntryTimestampColumn + | InfraLogEntryMessageColumn + | InfraLogEntryFieldColumn; + +/** A representation of the log entry's position in the event stream */ +export interface InfraTimeKey { + /** The timestamp of the event that the log entry corresponds to */ + time: number; + /** The tiebreaker that disambiguates events with the same timestamp */ + tiebreaker: number; +} + +/** A log entry */ +export interface InfraLogEntry { + /** A unique representation of the log entry's position in the event stream */ + key: InfraTimeKey; + /** The log entry's id */ + gid: string; + /** The source id */ + source: string; + /** The columns used for rendering the log entry */ + columns: InfraLogEntryColumn[]; +} diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_filter/index.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_filter/index.ts new file mode 100644 index 0000000000000..a737d19a5923d --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_filter/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useContext } from 'react'; +import createContainer from 'constate'; +import { ReduxStateContext } from '../../../utils/redux_context'; +import { logFilterSelectors as logFilterReduxSelectors } from '../../../store/local/selectors'; + +export const useLogFilterState = () => { + const { local: state } = useContext(ReduxStateContext); + const filterQuery = logFilterReduxSelectors.selectLogFilterQueryAsJson(state); + return { filterQuery }; +}; + +export interface LogFilterStateParams { + filterQuery: string | null; +} + +export const logFilterInitialState = { + filterQuery: null, +}; + +export const LogFilterState = createContainer(useLogFilterState); diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_highlights.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_highlights.tsx index 39c088b1ceb5a..fa1ccb4efa4bb 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_highlights.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_highlights.tsx @@ -6,30 +6,30 @@ import createContainer from 'constate'; import { useState, useContext } from 'react'; - import { useLogEntryHighlights } from './log_entry_highlights'; import { useLogSummaryHighlights } from './log_summary_highlights'; import { useNextAndPrevious } from './next_and_previous'; import { useReduxBridgeSetters } from './redux_bridge_setters'; import { useLogSummaryBufferInterval } from '../log_summary'; import { LogViewConfiguration } from '../log_view_configuration'; +import { TimeKey } from '../../../../common/time'; export const useLogHighlightsState = ({ sourceId, sourceVersion, + entriesStart, + entriesEnd, + filterQuery, }: { sourceId: string; sourceVersion: string | undefined; + entriesStart: TimeKey | null; + entriesEnd: TimeKey | null; + filterQuery: string | null; }) => { const [highlightTerms, setHighlightTerms] = useState([]); - const { - startKey, - endKey, - filterQuery, visibleMidpoint, - setStartKey, - setEndKey, setFilterQuery, setVisibleMidpoint, jumpToTarget, @@ -50,7 +50,14 @@ export const useLogHighlightsState = ({ logEntryHighlights, logEntryHighlightsById, loadLogEntryHighlightsRequest, - } = useLogEntryHighlights(sourceId, sourceVersion, startKey, endKey, filterQuery, highlightTerms); + } = useLogEntryHighlights( + sourceId, + sourceVersion, + entriesStart, + entriesEnd, + filterQuery, + highlightTerms + ); const { logSummaryHighlights, loadLogSummaryHighlightsRequest } = useLogSummaryHighlights( sourceId, @@ -78,8 +85,6 @@ export const useLogHighlightsState = ({ return { highlightTerms, setHighlightTerms, - setStartKey, - setEndKey, setFilterQuery, logEntryHighlights, logEntryHighlightsById, diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/redux_bridge_setters.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/redux_bridge_setters.tsx index b3254f597dfcf..0e778f35188f0 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/redux_bridge_setters.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/redux_bridge_setters.tsx @@ -8,19 +8,13 @@ import { useState } from 'react'; import { TimeKey } from '../../../../common/time'; export const useReduxBridgeSetters = () => { - const [startKey, setStartKey] = useState(null); - const [endKey, setEndKey] = useState(null); const [filterQuery, setFilterQuery] = useState(null); const [visibleMidpoint, setVisibleMidpoint] = useState(null); const [jumpToTarget, setJumpToTarget] = useState<(target: TimeKey) => void>(() => undefined); return { - startKey, - endKey, filterQuery, visibleMidpoint, - setStartKey, - setEndKey, setFilterQuery, setVisibleMidpoint, jumpToTarget, diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/redux_bridges.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/redux_bridges.tsx index 220eaade12fa6..2b60c6edd97aa 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/redux_bridges.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/redux_bridges.tsx @@ -8,23 +8,11 @@ import React, { useEffect, useContext } from 'react'; import { TimeKey } from '../../../../common/time'; import { withLogFilter } from '../with_log_filter'; -import { withStreamItems } from '../with_stream_items'; import { withLogPosition } from '../with_log_position'; import { LogHighlightsState } from './log_highlights'; // Bridges Redux container state with Hooks state. Once state is moved fully from // Redux to Hooks this can be removed. -export const LogHighlightsStreamItemsBridge = withStreamItems( - ({ entriesStart, entriesEnd }: { entriesStart: TimeKey | null; entriesEnd: TimeKey | null }) => { - const { setStartKey, setEndKey } = useContext(LogHighlightsState.Context); - useEffect(() => { - setStartKey(entriesStart); - setEndKey(entriesEnd); - }, [entriesStart, entriesEnd]); - - return null; - } -); export const LogHighlightsPositionBridge = withLogPosition( ({ @@ -61,7 +49,6 @@ export const LogHighlightsFilterQueryBridge = withLogFilter( export const LogHighlightsBridge = ({ indexPattern }: { indexPattern: any }) => ( <> - diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_position/index.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_position/index.ts new file mode 100644 index 0000000000000..7cc8050aafd14 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_position/index.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useContext } from 'react'; +import createContainer from 'constate'; +import { ReduxStateContext } from '../../../utils/redux_context'; +import { logPositionSelectors as logPositionReduxSelectors } from '../../../store/local/selectors'; +import { TimeKey } from '../../../../common/time'; + +export const useLogPositionState = () => { + const { local: state } = useContext(ReduxStateContext); + const timeKey = logPositionReduxSelectors.selectVisibleMidpointOrTarget(state); + const pages = logPositionReduxSelectors.selectPagesBeforeAndAfter(state); + const isAutoReloading = logPositionReduxSelectors.selectIsAutoReloading(state); + return { timeKey, isAutoReloading, ...pages }; +}; + +export interface LogPositionStateParams { + timeKey: TimeKey | null; + pagesAfterEnd: number | null; + pagesBeforeStart: number | null; + isAutoReloading: boolean; +} + +export const logPositionInitialState = { + timeKey: null, + pagesAfterEnd: null, + pagesBeforeStart: null, + isAutoReloading: false, +}; + +export const LogPositionState = createContainer(useLogPositionState); diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/with_stream_items.ts b/x-pack/legacy/plugins/infra/public/containers/logs/with_stream_items.ts index 12117d88f8283..da468b4391e4e 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/with_stream_items.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/with_stream_items.ts @@ -4,85 +4,47 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useContext, useMemo } from 'react'; -import { connect } from 'react-redux'; - +import { useContext, useMemo } from 'react'; import { StreamItem, LogEntryStreamItem } from '../../components/logging/log_text_stream/item'; -import { logEntriesActions, logEntriesSelectors, logPositionSelectors, State } from '../../store'; import { LogEntry, LogEntryHighlight } from '../../utils/log_entry'; -import { PropsOfContainer, RendererFunction } from '../../utils/typed_react'; -import { bindPlainActionCreators } from '../../utils/typed_redux'; +import { RendererFunction } from '../../utils/typed_react'; // deep inporting to avoid a circular import problem import { LogHighlightsState } from './log_highlights/log_highlights'; +import { LogPositionState } from './log_position'; +import { LogEntriesState, LogEntriesStateParams, LogEntriesCallbacks } from './log_entries'; import { UniqueTimeKey } from '../../../common/time'; -export const withStreamItems = connect( - (state: State) => ({ - isAutoReloading: logPositionSelectors.selectIsAutoReloading(state), - isReloading: logEntriesSelectors.selectIsReloadingEntries(state), - isLoadingMore: logEntriesSelectors.selectIsLoadingMoreEntries(state), - wasAutoReloadJustAborted: logPositionSelectors.selectAutoReloadJustAborted(state), - hasMoreBeforeStart: logEntriesSelectors.selectHasMoreBeforeStart(state), - hasMoreAfterEnd: logEntriesSelectors.selectHasMoreAfterEnd(state), - lastLoadedTime: logEntriesSelectors.selectEntriesLastLoadedTime(state), - entries: logEntriesSelectors.selectEntries(state), - entriesStart: logEntriesSelectors.selectEntriesStart(state), - entriesEnd: logEntriesSelectors.selectEntriesEnd(state), - }), - bindPlainActionCreators({ - loadNewerEntries: logEntriesActions.loadNewerEntries, - reloadEntries: logEntriesActions.reloadEntries, - setSourceId: logEntriesActions.setSourceId, - }) -); - -type WithStreamItemsProps = PropsOfContainer; - -export const WithStreamItems = withStreamItems( - ({ - children, - initializeOnMount, - ...props - }: WithStreamItemsProps & { - children: RendererFunction< - WithStreamItemsProps & { +export const WithStreamItems: React.FunctionComponent<{ + children: RendererFunction< + LogEntriesStateParams & + LogEntriesCallbacks & { currentHighlightKey: UniqueTimeKey | null; items: StreamItem[]; } - >; - initializeOnMount: boolean; - }) => { - const { currentHighlightKey, logEntryHighlightsById } = useContext(LogHighlightsState.Context); - const items = useMemo( - () => - props.isReloading && !props.isAutoReloading && !props.wasAutoReloadJustAborted - ? [] - : props.entries.map(logEntry => - createLogEntryStreamItem(logEntry, logEntryHighlightsById[logEntry.gid] || []) - ), - - [ - props.isReloading, - props.isAutoReloading, - props.wasAutoReloadJustAborted, - props.entries, - logEntryHighlightsById, - ] - ); - - useEffect(() => { - if (initializeOnMount && !props.isReloading && !props.isLoadingMore) { - props.reloadEntries(); - } - }, []); - - return children({ - ...props, - currentHighlightKey, - items, - }); - } -); + >; +}> = ({ children }) => { + const [logEntries, logEntriesCallbacks] = useContext(LogEntriesState.Context); + const { isAutoReloading } = useContext(LogPositionState.Context); + const { currentHighlightKey, logEntryHighlightsById } = useContext(LogHighlightsState.Context); + + const items = useMemo( + () => + logEntries.isReloading && !isAutoReloading + ? [] + : logEntries.entries.map(logEntry => + createLogEntryStreamItem(logEntry, logEntryHighlightsById[logEntry.gid] || []) + ), + + [logEntries.entries, logEntryHighlightsById] + ); + + return children({ + ...logEntries, + ...logEntriesCallbacks, + items, + currentHighlightKey, + }); +}; const createLogEntryStreamItem = ( logEntry: LogEntry, @@ -92,23 +54,3 @@ const createLogEntryStreamItem = ( logEntry, highlights, }); - -/** - * This component serves as connection between the state and side-effects - * managed by redux and the state and effects managed by hooks. In particular, - * it forwards changes of the source id to redux via the action creator - * `setSourceId`. - * - * It will be mounted beneath the hierachy level where the redux store and the - * source state are initialized. Once the log entry state and loading - * side-effects have been migrated from redux to hooks it can be removed. - */ -export const ReduxSourceIdBridge = withStreamItems( - ({ setSourceId, sourceId }: { setSourceId: (sourceId: string) => void; sourceId: string }) => { - useEffect(() => { - setSourceId(sourceId); - }, [setSourceId, sourceId]); - - return null; - } -); diff --git a/x-pack/legacy/plugins/infra/public/store/remote/log_entries/operations/log_entries.gql_query.ts b/x-pack/legacy/plugins/infra/public/graphql/log_entries.gql_query.ts similarity index 93% rename from x-pack/legacy/plugins/infra/public/store/remote/log_entries/operations/log_entries.gql_query.ts rename to x-pack/legacy/plugins/infra/public/graphql/log_entries.gql_query.ts index 78893b83cd645..41ff3c293a713 100644 --- a/x-pack/legacy/plugins/infra/public/store/remote/log_entries/operations/log_entries.gql_query.ts +++ b/x-pack/legacy/plugins/infra/public/graphql/log_entries.gql_query.ts @@ -6,7 +6,7 @@ import gql from 'graphql-tag'; -import { sharedFragments } from '../../../../../common/graphql/shared'; +import { sharedFragments } from '../../common/graphql/shared'; export const logEntriesQuery = gql` query LogEntries( diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_logs_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_logs_content.tsx index beb5eb391d368..88212849d4594 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_logs_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_logs_content.tsx @@ -24,7 +24,7 @@ import { WithLogMinimapUrlState } from '../../../containers/logs/with_log_minima import { WithLogPositionUrlState } from '../../../containers/logs/with_log_position'; import { WithLogPosition } from '../../../containers/logs/with_log_position'; import { WithLogTextviewUrlState } from '../../../containers/logs/with_log_textview'; -import { ReduxSourceIdBridge, WithStreamItems } from '../../../containers/logs/with_stream_items'; +import { WithStreamItems } from '../../../containers/logs/with_stream_items'; import { Source } from '../../../containers/source'; import { LogsToolbar } from './page_toolbar'; @@ -44,10 +44,8 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { } = useContext(LogFlyoutState.Context); const { logSummaryHighlights } = useContext(LogHighlightsState.Context); const derivedIndexPattern = createDerivedIndexPattern('logs'); - return ( <> - @@ -87,7 +85,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { scrollUnlockLiveStreaming, isScrollLocked, }) => ( - + {({ currentHighlightKey, hasMoreAfterEnd, @@ -96,7 +94,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { isReloading, items, lastLoadedTime, - loadNewerEntries, + fetchNewerEntries, }) => ( { items={items} jumpToTarget={jumpToTargetPosition} lastLoadedTime={lastLoadedTime} - loadNewerItems={loadNewerEntries} + loadNewerItems={fetchNewerEntries} reportVisibleInterval={reportVisiblePositions} scale={textScale} target={targetPosition} @@ -140,7 +138,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { visibleMidpointTime, visibleTimeInterval, }) => ( - + {({ isReloading }) => ( { +const LogEntriesStateProvider: React.FC = ({ children }) => { + const { sourceId } = useContext(Source.Context); + const { timeKey, pagesBeforeStart, pagesAfterEnd, isAutoReloading } = useContext( + LogPositionState.Context + ); + const { filterQuery } = useContext(LogFilterState.Context); + const entriesProps = { + timeKey, + pagesBeforeStart, + pagesAfterEnd, + filterQuery, + sourceId, + isAutoReloading, + }; + return {children}; +}; + +const LogHighlightsStateProvider: React.FC = ({ children }) => { const { sourceId, version } = useContext(Source.Context); + const [{ entriesStart, entriesEnd }] = useContext(LogEntriesState.Context); + const { filterQuery } = useContext(LogFilterState.Context); + const highlightsProps = { + sourceId, + sourceVersion: version, + entriesStart, + entriesEnd, + filterQuery, + }; + return {children}; +}; +export const LogsPageProviders: React.FunctionComponent = ({ children }) => { return ( - - {children} - + + + + {children} + + + ); diff --git a/x-pack/legacy/plugins/infra/public/store/actions.ts b/x-pack/legacy/plugins/infra/public/store/actions.ts index 70855101518d6..e2be0d64b8f1e 100644 --- a/x-pack/legacy/plugins/infra/public/store/actions.ts +++ b/x-pack/legacy/plugins/infra/public/store/actions.ts @@ -11,4 +11,3 @@ export { waffleTimeActions, waffleOptionsActions, } from './local'; -export { logEntriesActions } from './remote'; diff --git a/x-pack/legacy/plugins/infra/public/store/epics.ts b/x-pack/legacy/plugins/infra/public/store/epics.ts index 4df8e1368ca01..b5e48a4ec6214 100644 --- a/x-pack/legacy/plugins/infra/public/store/epics.ts +++ b/x-pack/legacy/plugins/infra/public/store/epics.ts @@ -7,7 +7,5 @@ import { combineEpics } from 'redux-observable'; import { createLocalEpic } from './local'; -import { createRemoteEpic } from './remote'; -export const createRootEpic = () => - combineEpics(createLocalEpic(), createRemoteEpic()); +export const createRootEpic = () => combineEpics(createLocalEpic()); diff --git a/x-pack/legacy/plugins/infra/public/store/local/epic.ts b/x-pack/legacy/plugins/infra/public/store/local/epic.ts index 4cfac85f00b15..e1a051355576f 100644 --- a/x-pack/legacy/plugins/infra/public/store/local/epic.ts +++ b/x-pack/legacy/plugins/infra/public/store/local/epic.ts @@ -6,8 +6,6 @@ import { combineEpics } from 'redux-observable'; -import { createLogPositionEpic } from './log_position'; import { createWaffleTimeEpic } from './waffle_time'; -export const createLocalEpic = () => - combineEpics(createLogPositionEpic(), createWaffleTimeEpic()); +export const createLocalEpic = () => combineEpics(createWaffleTimeEpic()); diff --git a/x-pack/legacy/plugins/infra/public/store/local/log_position/index.ts b/x-pack/legacy/plugins/infra/public/store/local/log_position/index.ts index 17e06348d18b2..3edb289985d55 100644 --- a/x-pack/legacy/plugins/infra/public/store/local/log_position/index.ts +++ b/x-pack/legacy/plugins/infra/public/store/local/log_position/index.ts @@ -8,5 +8,4 @@ import * as logPositionActions from './actions'; import * as logPositionSelectors from './selectors'; export { logPositionActions, logPositionSelectors }; -export * from './epic'; export * from './reducer'; diff --git a/x-pack/legacy/plugins/infra/public/store/local/log_position/reducer.ts b/x-pack/legacy/plugins/infra/public/store/local/log_position/reducer.ts index 3b99e2d4f4379..2ca8be8e40d86 100644 --- a/x-pack/legacy/plugins/infra/public/store/local/log_position/reducer.ts +++ b/x-pack/legacy/plugins/infra/public/store/local/log_position/reducer.ts @@ -17,8 +17,6 @@ import { unlockAutoReloadScroll, } from './actions'; -import { loadEntriesActionCreators } from '../../remote/log_entries/operations/load'; - interface ManualTargetPositionUpdatePolicy { policy: 'manual'; } @@ -38,9 +36,10 @@ export interface LogPositionState { startKey: TimeKey | null; middleKey: TimeKey | null; endKey: TimeKey | null; + pagesAfterEnd: number; + pagesBeforeStart: number; }; controlsShouldDisplayTargetPosition: boolean; - autoReloadJustAborted: boolean; autoReloadScrollLock: boolean; } @@ -53,9 +52,10 @@ export const initialLogPositionState: LogPositionState = { endKey: null, middleKey: null, startKey: null, + pagesBeforeStart: Infinity, + pagesAfterEnd: Infinity, }, controlsShouldDisplayTargetPosition: false, - autoReloadJustAborted: false, autoReloadScrollLock: false, }; @@ -76,11 +76,16 @@ const targetPositionUpdatePolicyReducer = reducerWithInitialState( const visiblePositionReducer = reducerWithInitialState( initialLogPositionState.visiblePositions -).case(reportVisiblePositions, (state, { startKey, middleKey, endKey }) => ({ - endKey, - middleKey, - startKey, -})); +).case( + reportVisiblePositions, + (state, { startKey, middleKey, endKey, pagesBeforeStart, pagesAfterEnd }) => ({ + endKey, + middleKey, + startKey, + pagesBeforeStart, + pagesAfterEnd, + }) +); // Determines whether to use the target position or the visible midpoint when // displaying a timestamp or time range in the toolbar and log minimap. When the @@ -98,17 +103,6 @@ const controlsShouldDisplayTargetPositionReducer = reducerWithInitialState( return state; }); -// If auto reload is aborted before a pending request finishes, this flag will -// prevent the UI from displaying the Loading Entries screen -const autoReloadJustAbortedReducer = reducerWithInitialState( - initialLogPositionState.autoReloadJustAborted -) - .case(stopAutoReload, () => true) - .case(startAutoReload, () => false) - .case(loadEntriesActionCreators.resolveDone, () => false) - .case(loadEntriesActionCreators.resolveFailed, () => false) - .case(loadEntriesActionCreators.resolve, () => false); - const autoReloadScrollLockReducer = reducerWithInitialState( initialLogPositionState.autoReloadScrollLock ) @@ -122,6 +116,5 @@ export const logPositionReducer = combineReducers({ updatePolicy: targetPositionUpdatePolicyReducer, visiblePositions: visiblePositionReducer, controlsShouldDisplayTargetPosition: controlsShouldDisplayTargetPositionReducer, - autoReloadJustAborted: autoReloadJustAbortedReducer, autoReloadScrollLock: autoReloadScrollLockReducer, }); diff --git a/x-pack/legacy/plugins/infra/public/store/local/log_position/selectors.ts b/x-pack/legacy/plugins/infra/public/store/local/log_position/selectors.ts index 7a2fa86822c56..30fd4d3f77b5c 100644 --- a/x-pack/legacy/plugins/infra/public/store/local/log_position/selectors.ts +++ b/x-pack/legacy/plugins/infra/public/store/local/log_position/selectors.ts @@ -15,8 +15,6 @@ export const selectIsAutoReloading = (state: LogPositionState) => export const selectAutoReloadScrollLock = (state: LogPositionState) => state.autoReloadScrollLock; -export const selectAutoReloadJustAborted = (state: LogPositionState) => state.autoReloadJustAborted; - export const selectFirstVisiblePosition = (state: LogPositionState) => state.visiblePositions.startKey ? state.visiblePositions.startKey : null; @@ -26,6 +24,13 @@ export const selectMiddleVisiblePosition = (state: LogPositionState) => export const selectLastVisiblePosition = (state: LogPositionState) => state.visiblePositions.endKey ? state.visiblePositions.endKey : null; +export const selectPagesBeforeAndAfter = (state: LogPositionState) => + state.visiblePositions + ? { + pagesBeforeStart: state.visiblePositions.pagesBeforeStart, + pagesAfterEnd: state.visiblePositions.pagesAfterEnd, + } + : { pagesBeforeStart: null, pagesAfterEnd: null }; export const selectControlsShouldDisplayTargetPosition = (state: LogPositionState) => state.controlsShouldDisplayTargetPosition; diff --git a/x-pack/legacy/plugins/infra/public/store/reducer.ts b/x-pack/legacy/plugins/infra/public/store/reducer.ts index 65b225d019603..2536ddbee401b 100644 --- a/x-pack/legacy/plugins/infra/public/store/reducer.ts +++ b/x-pack/legacy/plugins/infra/public/store/reducer.ts @@ -7,19 +7,15 @@ import { combineReducers } from 'redux'; import { initialLocalState, localReducer, LocalState } from './local'; -import { initialRemoteState, remoteReducer, RemoteState } from './remote'; export interface State { local: LocalState; - remote: RemoteState; } export const initialState: State = { local: initialLocalState, - remote: initialRemoteState, }; export const reducer = combineReducers({ local: localReducer, - remote: remoteReducer, }); diff --git a/x-pack/legacy/plugins/infra/public/store/remote/actions.ts b/x-pack/legacy/plugins/infra/public/store/remote/actions.ts deleted file mode 100644 index b38890afefb41..0000000000000 --- a/x-pack/legacy/plugins/infra/public/store/remote/actions.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { logEntriesActions } from './log_entries'; diff --git a/x-pack/legacy/plugins/infra/public/store/remote/epic.ts b/x-pack/legacy/plugins/infra/public/store/remote/epic.ts deleted file mode 100644 index 3b3ff602731cc..0000000000000 --- a/x-pack/legacy/plugins/infra/public/store/remote/epic.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { createLogEntriesEpic } from './log_entries'; - -export const createRemoteEpic = () => createLogEntriesEpic(); diff --git a/x-pack/legacy/plugins/infra/public/store/remote/index.ts b/x-pack/legacy/plugins/infra/public/store/remote/index.ts deleted file mode 100644 index c2843320bfd0c..0000000000000 --- a/x-pack/legacy/plugins/infra/public/store/remote/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export * from './actions'; -export * from './epic'; -export * from './reducer'; -export * from './selectors'; diff --git a/x-pack/legacy/plugins/infra/public/store/remote/log_entries/actions.ts b/x-pack/legacy/plugins/infra/public/store/remote/log_entries/actions.ts deleted file mode 100644 index 02964bcb27e11..0000000000000 --- a/x-pack/legacy/plugins/infra/public/store/remote/log_entries/actions.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import actionCreatorFactory from 'typescript-fsa'; - -import { loadEntriesActionCreators } from './operations/load'; -import { loadMoreEntriesActionCreators } from './operations/load_more'; - -const actionCreator = actionCreatorFactory('x-pack/infra/remote/log_entries'); - -export const setSourceId = actionCreator('SET_SOURCE_ID'); - -export const loadEntries = loadEntriesActionCreators.resolve; -export const loadMoreEntries = loadMoreEntriesActionCreators.resolve; - -export const loadNewerEntries = actionCreator('LOAD_NEWER_LOG_ENTRIES'); - -export const reloadEntries = actionCreator('RELOAD_LOG_ENTRIES'); diff --git a/x-pack/legacy/plugins/infra/public/store/remote/log_entries/epic.ts b/x-pack/legacy/plugins/infra/public/store/remote/log_entries/epic.ts deleted file mode 100644 index 0894a31996042..0000000000000 --- a/x-pack/legacy/plugins/infra/public/store/remote/log_entries/epic.ts +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Action } from 'redux'; -import { combineEpics, Epic, EpicWithState } from 'redux-observable'; -import { merge } from 'rxjs'; -import { exhaustMap, filter, map, withLatestFrom } from 'rxjs/operators'; - -import { logFilterActions, logPositionActions } from '../..'; -import { pickTimeKey, TimeKey, timeKeyIsBetween } from '../../../../common/time'; -import { - loadEntries, - loadMoreEntries, - loadNewerEntries, - reloadEntries, - setSourceId, -} from './actions'; -import { loadEntriesEpic } from './operations/load'; -import { loadMoreEntriesEpic } from './operations/load_more'; - -const LOAD_CHUNK_SIZE = 200; -const DESIRED_BUFFER_PAGES = 2; - -interface ManageEntriesDependencies { - selectLogEntriesStart: (state: State) => TimeKey | null; - selectLogEntriesEnd: (state: State) => TimeKey | null; - selectHasMoreLogEntriesBeforeStart: (state: State) => boolean; - selectHasMoreLogEntriesAfterEnd: (state: State) => boolean; - selectIsAutoReloadingLogEntries: (state: State) => boolean; - selectIsLoadingLogEntries: (state: State) => boolean; - selectLogFilterQueryAsJson: (state: State) => string | null; - selectVisibleLogMidpointOrTarget: (state: State) => TimeKey | null; -} - -export const createLogEntriesEpic = () => - combineEpics( - createEntriesEffectsEpic(), - loadEntriesEpic as EpicWithState, - loadMoreEntriesEpic as EpicWithState - ); - -export const createEntriesEffectsEpic = (): Epic< - Action, - Action, - State, - ManageEntriesDependencies -> => ( - action$, - state$, - { - selectLogEntriesStart, - selectLogEntriesEnd, - selectHasMoreLogEntriesBeforeStart, - selectHasMoreLogEntriesAfterEnd, - selectIsAutoReloadingLogEntries, - selectIsLoadingLogEntries, - selectLogFilterQueryAsJson, - selectVisibleLogMidpointOrTarget, - } -) => { - const filterQuery$ = state$.pipe(map(selectLogFilterQueryAsJson)); - const visibleMidpointOrTarget$ = state$.pipe( - map(selectVisibleLogMidpointOrTarget), - filter(isNotNull), - map(pickTimeKey) - ); - - const sourceId$ = action$.pipe( - filter(setSourceId.match), - map(({ payload }) => payload) - ); - - const shouldLoadAroundNewPosition$ = action$.pipe( - filter(logPositionActions.jumpToTargetPosition.match), - withLatestFrom(state$), - filter(([{ payload }, state]) => { - const entriesStart = selectLogEntriesStart(state); - const entriesEnd = selectLogEntriesEnd(state); - - return entriesStart && entriesEnd - ? !timeKeyIsBetween(entriesStart, entriesEnd, payload) - : true; - }), - map(([{ payload }]) => pickTimeKey(payload)) - ); - - const shouldLoadWithNewFilter$ = action$.pipe( - filter(logFilterActions.applyLogFilterQuery.match), - withLatestFrom(filterQuery$, (filterQuery, filterQueryString) => filterQueryString) - ); - - const shouldReload$ = merge(action$.pipe(filter(reloadEntries.match)), sourceId$); - - const shouldLoadMoreBefore$ = action$.pipe( - filter(logPositionActions.reportVisiblePositions.match), - filter(({ payload: { pagesBeforeStart } }) => pagesBeforeStart < DESIRED_BUFFER_PAGES), - withLatestFrom(state$), - filter( - ([action, state]) => - !selectIsAutoReloadingLogEntries(state) && - !selectIsLoadingLogEntries(state) && - selectHasMoreLogEntriesBeforeStart(state) - ), - map(([action, state]) => selectLogEntriesStart(state)), - filter(isNotNull), - map(pickTimeKey) - ); - - const shouldLoadMoreAfter$ = merge( - action$.pipe( - filter(logPositionActions.reportVisiblePositions.match), - filter(({ payload: { pagesAfterEnd } }) => pagesAfterEnd < DESIRED_BUFFER_PAGES), - withLatestFrom(state$, (action, state) => state), - filter( - state => - !selectIsAutoReloadingLogEntries(state) && - !selectIsLoadingLogEntries(state) && - selectHasMoreLogEntriesAfterEnd(state) - ) - ), - action$.pipe( - filter(loadNewerEntries.match), - withLatestFrom(state$, (action, state) => state) - ) - ).pipe( - map(state => selectLogEntriesEnd(state)), - filter(isNotNull), - map(pickTimeKey) - ); - - return merge( - shouldLoadAroundNewPosition$.pipe( - withLatestFrom(filterQuery$, sourceId$), - exhaustMap(([timeKey, filterQuery, sourceId]) => [ - loadEntries({ - sourceId, - timeKey, - countBefore: LOAD_CHUNK_SIZE, - countAfter: LOAD_CHUNK_SIZE, - filterQuery, - }), - ]) - ), - shouldLoadWithNewFilter$.pipe( - withLatestFrom(visibleMidpointOrTarget$, sourceId$), - exhaustMap(([filterQuery, timeKey, sourceId]) => [ - loadEntries({ - sourceId, - timeKey, - countBefore: LOAD_CHUNK_SIZE, - countAfter: LOAD_CHUNK_SIZE, - filterQuery, - }), - ]) - ), - shouldReload$.pipe( - withLatestFrom(visibleMidpointOrTarget$, filterQuery$, sourceId$), - exhaustMap(([_, timeKey, filterQuery, sourceId]) => [ - loadEntries({ - sourceId, - timeKey, - countBefore: LOAD_CHUNK_SIZE, - countAfter: LOAD_CHUNK_SIZE, - filterQuery, - }), - ]) - ), - shouldLoadMoreAfter$.pipe( - withLatestFrom(filterQuery$, sourceId$), - exhaustMap(([timeKey, filterQuery, sourceId]) => [ - loadMoreEntries({ - sourceId, - timeKey, - countBefore: 0, - countAfter: LOAD_CHUNK_SIZE, - filterQuery, - }), - ]) - ), - shouldLoadMoreBefore$.pipe( - withLatestFrom(filterQuery$, sourceId$), - exhaustMap(([timeKey, filterQuery, sourceId]) => [ - loadMoreEntries({ - sourceId, - timeKey, - countBefore: LOAD_CHUNK_SIZE, - countAfter: 0, - filterQuery, - }), - ]) - ) - ); -}; - -const isNotNull = (value: T | null): value is T => value !== null; diff --git a/x-pack/legacy/plugins/infra/public/store/remote/log_entries/index.ts b/x-pack/legacy/plugins/infra/public/store/remote/log_entries/index.ts deleted file mode 100644 index 8e00425526935..0000000000000 --- a/x-pack/legacy/plugins/infra/public/store/remote/log_entries/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as logEntriesActions from './actions'; -import * as logEntriesSelectors from './selectors'; - -export { logEntriesActions, logEntriesSelectors }; -export * from './epic'; -export * from './reducer'; -export { initialLogEntriesState, LogEntriesState } from './state'; diff --git a/x-pack/legacy/plugins/infra/public/store/remote/log_entries/operations/load.ts b/x-pack/legacy/plugins/infra/public/store/remote/log_entries/operations/load.ts deleted file mode 100644 index ce3193e57ab09..0000000000000 --- a/x-pack/legacy/plugins/infra/public/store/remote/log_entries/operations/load.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { LogEntries as LogEntriesQuery } from '../../../../graphql/types'; -import { - createGraphqlOperationActionCreators, - createGraphqlOperationReducer, - createGraphqlQueryEpic, -} from '../../../../utils/remote_state/remote_graphql_state'; -import { initialLogEntriesState } from '../state'; -import { logEntriesQuery } from './log_entries.gql_query'; - -const operationKey = 'load'; - -export const loadEntriesActionCreators = createGraphqlOperationActionCreators< - LogEntriesQuery.Query, - LogEntriesQuery.Variables ->('log_entries', operationKey); - -export const loadEntriesReducer = createGraphqlOperationReducer( - operationKey, - initialLogEntriesState, - loadEntriesActionCreators, - (state, action) => action.payload.result.data.source.logEntriesAround, - () => ({ - entries: [], - hasMoreAfter: false, - hasMoreBefore: false, - }) -); - -export const loadEntriesEpic = createGraphqlQueryEpic(logEntriesQuery, loadEntriesActionCreators); diff --git a/x-pack/legacy/plugins/infra/public/store/remote/log_entries/operations/load_more.ts b/x-pack/legacy/plugins/infra/public/store/remote/log_entries/operations/load_more.ts deleted file mode 100644 index 7651b039083cf..0000000000000 --- a/x-pack/legacy/plugins/infra/public/store/remote/log_entries/operations/load_more.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { LogEntries as LogEntriesQuery } from '../../../../graphql/types'; -import { - getLogEntryIndexAfterTime, - getLogEntryIndexBeforeTime, - getLogEntryKey, -} from '../../../../utils/log_entry'; -import { - createGraphqlOperationActionCreators, - createGraphqlOperationReducer, - createGraphqlQueryEpic, -} from '../../../../utils/remote_state/remote_graphql_state'; -import { initialLogEntriesState } from '../state'; -import { logEntriesQuery } from './log_entries.gql_query'; - -const operationKey = 'load_more'; - -export const loadMoreEntriesActionCreators = createGraphqlOperationActionCreators< - LogEntriesQuery.Query, - LogEntriesQuery.Variables ->('log_entries', operationKey); - -export const loadMoreEntriesReducer = createGraphqlOperationReducer( - operationKey, - initialLogEntriesState, - loadMoreEntriesActionCreators, - (state, action) => { - const logEntriesAround = action.payload.result.data.source.logEntriesAround; - const newEntries = logEntriesAround.entries; - const oldEntries = state && state.entries ? state.entries : []; - const oldStart = state && state.start ? state.start : null; - const oldEnd = state && state.end ? state.end : null; - - if (newEntries.length <= 0) { - return state; - } - - if ((action.payload.params.countBefore || 0) > 0) { - const lastLogEntry = newEntries[newEntries.length - 1]; - const prependAtIndex = getLogEntryIndexAfterTime(oldEntries, getLogEntryKey(lastLogEntry)); - return { - start: logEntriesAround.start, - end: oldEnd, - hasMoreBefore: logEntriesAround.hasMoreBefore, - hasMoreAfter: state ? state.hasMoreAfter : logEntriesAround.hasMoreAfter, - entries: [...newEntries, ...oldEntries.slice(prependAtIndex)], - }; - } else if ((action.payload.params.countAfter || 0) > 0) { - const firstLogEntry = newEntries[0]; - const appendAtIndex = getLogEntryIndexBeforeTime(oldEntries, getLogEntryKey(firstLogEntry)); - return { - start: oldStart, - end: logEntriesAround.end, - hasMoreBefore: state ? state.hasMoreBefore : logEntriesAround.hasMoreBefore, - hasMoreAfter: logEntriesAround.hasMoreAfter, - entries: [...oldEntries.slice(0, appendAtIndex), ...newEntries], - }; - } else { - return state; - } - } -); - -export const loadMoreEntriesEpic = createGraphqlQueryEpic( - logEntriesQuery, - loadMoreEntriesActionCreators -); diff --git a/x-pack/legacy/plugins/infra/public/store/remote/log_entries/reducer.ts b/x-pack/legacy/plugins/infra/public/store/remote/log_entries/reducer.ts deleted file mode 100644 index c0d60c4d336de..0000000000000 --- a/x-pack/legacy/plugins/infra/public/store/remote/log_entries/reducer.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import reduceReducers from 'reduce-reducers'; -import { Reducer } from 'redux'; - -import { loadEntriesReducer } from './operations/load'; -import { loadMoreEntriesReducer } from './operations/load_more'; -import { LogEntriesState } from './state'; - -export const logEntriesReducer = reduceReducers( - loadEntriesReducer, - loadMoreEntriesReducer -) as Reducer; diff --git a/x-pack/legacy/plugins/infra/public/store/remote/log_entries/selectors.ts b/x-pack/legacy/plugins/infra/public/store/remote/log_entries/selectors.ts deleted file mode 100644 index 0306efc334a51..0000000000000 --- a/x-pack/legacy/plugins/infra/public/store/remote/log_entries/selectors.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { createSelector } from 'reselect'; - -import { createGraphqlStateSelectors } from '../../../utils/remote_state/remote_graphql_state'; -import { LogEntriesRemoteState } from './state'; - -const entriesGraphlStateSelectors = createGraphqlStateSelectors(); - -export const selectEntries = createSelector(entriesGraphlStateSelectors.selectData, data => - data ? data.entries : [] -); - -export const selectIsLoadingEntries = entriesGraphlStateSelectors.selectIsLoading; - -export const selectIsReloadingEntries = createSelector( - entriesGraphlStateSelectors.selectIsLoading, - entriesGraphlStateSelectors.selectLoadingProgressOperationInfo, - (isLoading, operationInfo) => - isLoading && operationInfo ? operationInfo.operationKey === 'load' : false -); - -export const selectIsLoadingMoreEntries = createSelector( - entriesGraphlStateSelectors.selectIsLoading, - entriesGraphlStateSelectors.selectLoadingProgressOperationInfo, - (isLoading, operationInfo) => - isLoading && operationInfo ? operationInfo.operationKey === 'load_more' : false -); - -export const selectEntriesStart = createSelector(entriesGraphlStateSelectors.selectData, data => - data && data.start ? data.start : null -); - -export const selectEntriesEnd = createSelector(entriesGraphlStateSelectors.selectData, data => - data && data.end ? data.end : null -); - -export const selectHasMoreBeforeStart = createSelector( - entriesGraphlStateSelectors.selectData, - data => (data ? data.hasMoreBefore : true) -); - -export const selectHasMoreAfterEnd = createSelector(entriesGraphlStateSelectors.selectData, data => - data ? data.hasMoreAfter : true -); - -export const selectEntriesLastLoadedTime = entriesGraphlStateSelectors.selectLoadingResultTime; - -export const selectEntriesStartLoadingState = entriesGraphlStateSelectors.selectLoadingState; - -export const selectEntriesEndLoadingState = entriesGraphlStateSelectors.selectLoadingState; - -export const selectFirstEntry = createSelector(selectEntries, entries => - entries.length > 0 ? entries[0] : null -); - -export const selectLastEntry = createSelector(selectEntries, entries => - entries.length > 0 ? entries[entries.length - 1] : null -); - -export const selectLoadedEntriesTimeInterval = createSelector( - entriesGraphlStateSelectors.selectData, - data => ({ - end: data && data.end ? data.end.time : null, - start: data && data.start ? data.start.time : null, - }) -); diff --git a/x-pack/legacy/plugins/infra/public/store/remote/log_entries/state.ts b/x-pack/legacy/plugins/infra/public/store/remote/log_entries/state.ts deleted file mode 100644 index 8dbccf6c2bdd3..0000000000000 --- a/x-pack/legacy/plugins/infra/public/store/remote/log_entries/state.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { LogEntries as LogEntriesQuery } from '../../../graphql/types'; -import { - createGraphqlInitialState, - GraphqlState, -} from '../../../utils/remote_state/remote_graphql_state'; - -export type LogEntriesRemoteState = LogEntriesQuery.LogEntriesAround; -export type LogEntriesState = GraphqlState; - -export const initialLogEntriesState = createGraphqlInitialState(); diff --git a/x-pack/legacy/plugins/infra/public/store/remote/reducer.ts b/x-pack/legacy/plugins/infra/public/store/remote/reducer.ts deleted file mode 100644 index 2ab0a9a47ae86..0000000000000 --- a/x-pack/legacy/plugins/infra/public/store/remote/reducer.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { combineReducers } from 'redux'; -import { initialLogEntriesState, logEntriesReducer, LogEntriesState } from './log_entries'; - -export interface RemoteState { - logEntries: LogEntriesState; -} - -export const initialRemoteState = { - logEntries: initialLogEntriesState, -}; - -export const remoteReducer = combineReducers({ - logEntries: logEntriesReducer, -}); diff --git a/x-pack/legacy/plugins/infra/public/store/remote/selectors.ts b/x-pack/legacy/plugins/infra/public/store/remote/selectors.ts deleted file mode 100644 index b1daa2bc8110d..0000000000000 --- a/x-pack/legacy/plugins/infra/public/store/remote/selectors.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { globalizeSelectors } from '../../utils/typed_redux'; -import { logEntriesSelectors as innerLogEntriesSelectors } from './log_entries'; -import { RemoteState } from './reducer'; - -export const logEntriesSelectors = globalizeSelectors( - (state: RemoteState) => state.logEntries, - innerLogEntriesSelectors -); diff --git a/x-pack/legacy/plugins/infra/public/store/selectors.ts b/x-pack/legacy/plugins/infra/public/store/selectors.ts index 79a442789d6dd..aecba1779d036 100644 --- a/x-pack/legacy/plugins/infra/public/store/selectors.ts +++ b/x-pack/legacy/plugins/infra/public/store/selectors.ts @@ -4,9 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createSelector } from 'reselect'; - -import { getLogEntryAtTime } from '../utils/log_entry'; import { globalizeSelectors } from '../utils/typed_redux'; import { logFilterSelectors as localLogFilterSelectors, @@ -16,8 +13,6 @@ import { waffleTimeSelectors as localWaffleTimeSelectors, } from './local'; import { State } from './reducer'; -import { logEntriesSelectors as remoteLogEntriesSelectors } from './remote'; - /** * local selectors */ @@ -29,36 +24,3 @@ export const logPositionSelectors = globalizeSelectors(selectLocal, localLogPosi export const waffleFilterSelectors = globalizeSelectors(selectLocal, localWaffleFilterSelectors); export const waffleTimeSelectors = globalizeSelectors(selectLocal, localWaffleTimeSelectors); export const waffleOptionsSelectors = globalizeSelectors(selectLocal, localWaffleOptionsSelectors); - -/** - * remote selectors - */ - -const selectRemote = (state: State) => state.remote; - -export const logEntriesSelectors = globalizeSelectors(selectRemote, remoteLogEntriesSelectors); - -/** - * shared selectors - */ - -export const sharedSelectors = { - selectFirstVisibleLogEntry: createSelector( - logEntriesSelectors.selectEntries, - logPositionSelectors.selectFirstVisiblePosition, - (entries, firstVisiblePosition) => - firstVisiblePosition ? getLogEntryAtTime(entries, firstVisiblePosition) : null - ), - selectMiddleVisibleLogEntry: createSelector( - logEntriesSelectors.selectEntries, - logPositionSelectors.selectMiddleVisiblePosition, - (entries, middleVisiblePosition) => - middleVisiblePosition ? getLogEntryAtTime(entries, middleVisiblePosition) : null - ), - selectLastVisibleLogEntry: createSelector( - logEntriesSelectors.selectEntries, - logPositionSelectors.selectLastVisiblePosition, - (entries, lastVisiblePosition) => - lastVisiblePosition ? getLogEntryAtTime(entries, lastVisiblePosition) : null - ), -}; diff --git a/x-pack/legacy/plugins/infra/public/store/store.ts b/x-pack/legacy/plugins/infra/public/store/store.ts index d699db6af042e..601db0f56a693 100644 --- a/x-pack/legacy/plugins/infra/public/store/store.ts +++ b/x-pack/legacy/plugins/infra/public/store/store.ts @@ -12,7 +12,6 @@ import { map } from 'rxjs/operators'; import { createRootEpic, initialState, - logEntriesSelectors, logFilterSelectors, logPositionSelectors, reducer, @@ -38,11 +37,6 @@ export function createStore({ apolloClient, observableApi }: StoreDependencies) const middlewareDependencies = { postToApi$: observableApi.pipe(map(({ post }) => post)), apolloClient$: apolloClient, - selectIsLoadingLogEntries: logEntriesSelectors.selectIsLoadingEntries, - selectLogEntriesEnd: logEntriesSelectors.selectEntriesEnd, - selectLogEntriesStart: logEntriesSelectors.selectEntriesStart, - selectHasMoreLogEntriesAfterEnd: logEntriesSelectors.selectHasMoreAfterEnd, - selectHasMoreLogEntriesBeforeStart: logEntriesSelectors.selectHasMoreBeforeStart, selectIsAutoReloadingLogEntries: logPositionSelectors.selectIsAutoReloading, selectIsAutoReloadingScrollLocked: logPositionSelectors.selectAutoReloadScrollLock, selectLogFilterQueryAsJson: logFilterSelectors.selectLogFilterQueryAsJson, diff --git a/x-pack/legacy/plugins/infra/public/utils/redux_context.tsx b/x-pack/legacy/plugins/infra/public/utils/redux_context.tsx new file mode 100644 index 0000000000000..3bd3d31c745a9 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/utils/redux_context.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import React, { createContext } from 'react'; +import { State, initialState } from '../store'; + +export const ReduxStateContext = createContext(initialState); + +const withRedux = connect((state: State) => state); +export const ReduxStateContextProvider = withRedux(({ children, ...state }) => { + return {children}; +}); diff --git a/x-pack/legacy/plugins/infra/public/utils/remote_state/remote_graphql_state.ts b/x-pack/legacy/plugins/infra/public/utils/remote_state/remote_graphql_state.ts deleted file mode 100644 index 0cbc94516617b..0000000000000 --- a/x-pack/legacy/plugins/infra/public/utils/remote_state/remote_graphql_state.ts +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ApolloError, ApolloQueryResult } from 'apollo-client'; -import { DocumentNode } from 'graphql'; -import { Action as ReduxAction } from 'redux'; -import { Epic } from 'redux-observable'; -import { from, Observable } from 'rxjs'; -import { catchError, filter, map, startWith, switchMap, withLatestFrom } from 'rxjs/operators'; -import { Action, ActionCreator, actionCreatorFactory, Failure, Success } from 'typescript-fsa'; -import { reducerWithInitialState } from 'typescript-fsa-reducers/dist'; - -import { createSelector } from 'reselect'; -import { InfraApolloClient } from '../../lib/lib'; -import { - isFailureLoadingResult, - isIdleLoadingProgress, - isRunningLoadingProgress, - isSuccessLoadingResult, - isUninitializedLoadingResult, - LoadingPolicy, - LoadingProgress, - LoadingResult, -} from '../loading_state'; - -export interface GraphqlState { - current: LoadingProgress>; - last: LoadingResult>; - data: State | undefined; -} - -interface OperationInfo { - operationKey: string; - variables: Variables; -} - -type ResolveDonePayload = Success>; -type ResolveFailedPayload = Failure; - -interface OperationActionCreators { - resolve: ActionCreator; - resolveStarted: ActionCreator; - resolveDone: ActionCreator>; - resolveFailed: ActionCreator>; -} - -export const createGraphqlInitialState = (initialData?: State): GraphqlState => ({ - current: { - progress: 'idle', - }, - last: { - result: 'uninitialized', - }, - data: initialData, -}); - -export const createGraphqlOperationActionCreators = ( - stateKey: string, - operationKey: string -): OperationActionCreators => { - const actionCreator = actionCreatorFactory(`x-pack/infra/remote/${stateKey}/${operationKey}`); - - const resolve = actionCreator('RESOLVE'); - const resolveEffect = actionCreator.async>('RESOLVE'); - - return { - resolve, - resolveStarted: resolveEffect.started, - resolveDone: resolveEffect.done, - resolveFailed: resolveEffect.failed, - }; -}; - -export const createGraphqlOperationReducer = ( - operationKey: string, - initialState: GraphqlState, - actionCreators: OperationActionCreators, - reduceSuccess: ( - state: State | undefined, - action: Action> - ) => State | undefined = state => state, - reduceFailure: ( - state: State | undefined, - action: Action> - ) => State | undefined = state => state -) => - reducerWithInitialState(initialState) - .caseWithAction(actionCreators.resolveStarted, (state, action) => ({ - ...state, - current: { - progress: 'running', - time: Date.now(), - parameters: { - operationKey, - variables: action.payload, - }, - }, - })) - .caseWithAction(actionCreators.resolveDone, (state, action) => ({ - ...state, - current: { - progress: 'idle', - }, - last: { - result: 'success', - parameters: { - operationKey, - variables: action.payload.params, - }, - time: Date.now(), - isExhausted: false, - }, - data: reduceSuccess(state.data, action), - })) - .caseWithAction(actionCreators.resolveFailed, (state, action) => ({ - ...state, - current: { - progress: 'idle', - }, - last: { - result: 'failure', - reason: `${action.payload}`, - time: Date.now(), - parameters: { - operationKey, - variables: action.payload.params, - }, - }, - data: reduceFailure(state.data, action), - })) - .build(); - -export const createGraphqlQueryEpic = ( - graphqlQuery: DocumentNode, - actionCreators: OperationActionCreators -): Epic< - ReduxAction, - ReduxAction, - any, - { - apolloClient$: Observable; - } -> => (action$, state$, { apolloClient$ }) => - action$.pipe( - filter(actionCreators.resolve.match), - withLatestFrom(apolloClient$), - switchMap(([{ payload: variables }, apolloClient]) => - from( - apolloClient.query({ - query: graphqlQuery, - variables, - fetchPolicy: 'no-cache', - }) - ).pipe( - map(result => actionCreators.resolveDone({ params: variables, result })), - catchError(error => [actionCreators.resolveFailed({ params: variables, error })]), - startWith(actionCreators.resolveStarted(variables)) - ) - ) - ); - -export const createGraphqlStateSelectors = ( - selectState: (parentState: any) => GraphqlState = parentState => parentState -) => { - const selectData = createSelector(selectState, state => state.data); - - const selectLoadingProgress = createSelector(selectState, state => state.current); - const selectLoadingProgressOperationInfo = createSelector(selectLoadingProgress, progress => - isRunningLoadingProgress(progress) ? progress.parameters : null - ); - const selectIsLoading = createSelector(selectLoadingProgress, isRunningLoadingProgress); - const selectIsIdle = createSelector(selectLoadingProgress, isIdleLoadingProgress); - - const selectLoadingResult = createSelector(selectState, state => state.last); - const selectLoadingResultOperationInfo = createSelector(selectLoadingResult, result => - !isUninitializedLoadingResult(result) ? result.parameters : null - ); - const selectLoadingResultTime = createSelector(selectLoadingResult, result => - !isUninitializedLoadingResult(result) ? result.time : null - ); - const selectIsUninitialized = createSelector(selectLoadingResult, isUninitializedLoadingResult); - const selectIsSuccess = createSelector(selectLoadingResult, isSuccessLoadingResult); - const selectIsFailure = createSelector(selectLoadingResult, isFailureLoadingResult); - - const selectLoadingState = createSelector( - selectLoadingProgress, - selectLoadingResult, - (loadingProgress, loadingResult) => ({ - current: loadingProgress, - last: loadingResult, - policy: { - policy: 'manual', - } as LoadingPolicy, - }) - ); - - return { - selectData, - selectIsFailure, - selectIsIdle, - selectIsLoading, - selectIsSuccess, - selectIsUninitialized, - selectLoadingProgress, - selectLoadingProgressOperationInfo, - selectLoadingResult, - selectLoadingResultOperationInfo, - selectLoadingResultTime, - selectLoadingState, - }; -}; From 7d5dd33a50f20c35b5046d487126ad353dc67ff6 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Thu, 12 Dec 2019 10:47:11 -0700 Subject: [PATCH 08/36] [Reporting/NP Migration] Remove server.expose of ExportTypeRegistry (#50973) (#52813) * [Reporting/NPMigration] typescriptify ExportTypeRegistry, remove from server.expose * Minor routes registration cleanup * move the ETR test file * Re-pack the route registration, reduce LOC changes * add EnqueueJobFn type * Fix usage collector test * remove a throw error used for development/debugging * fix imports error * Fix execute job tests * wip test fixes * test fixes for real * fix more tests * fix diffs * Add TODOs about the ExportTypesRegistry.register unwrap the factory functions. * really make headlessbrowserdriver required as an execute job factory option * fix tests * Use constants for license type keywords --- .../plugins/reporting/common/constants.ts | 6 + .../reporting/common/export_types_registry.js | 64 --------- .../common/lib/screenshots/index.ts | 14 +- .../reporting/export_types/csv/index.ts | 39 ++++++ .../export_types/csv/server/index.ts | 22 --- .../csv_from_savedobject/index.ts | 34 +++++ .../csv_from_savedobject/server/index.ts | 22 --- .../reporting/export_types/png/index.ts | 38 ++++++ .../png/server/execute_job/index.test.js | 6 +- .../png/server/execute_job/index.ts | 18 ++- .../export_types/png/server/index.ts | 23 ---- .../png/server/lib/generate_png.ts | 9 +- .../export_types/printable_pdf/index.ts | 38 ++++++ .../server/execute_job/index.test.js | 6 +- .../printable_pdf/server/execute_job/index.ts | 18 ++- .../printable_pdf/server/index.ts | 23 ---- .../printable_pdf/server/lib/generate_pdf.ts | 9 +- x-pack/legacy/plugins/reporting/index.ts | 25 ++-- .../lib}/__tests__/export_types_registry.js | 0 .../reporting/server/lib/create_queue.ts | 19 ++- .../server/lib/create_worker.test.ts | 40 +++--- .../reporting/server/lib/create_worker.ts | 17 ++- .../reporting/server/lib/enqueue_job.ts | 21 ++- .../server/lib/export_types_registry.ts | 128 ++++++++++++++---- .../plugins/reporting/server/lib/index.ts | 5 +- .../generate_from_savedobject_immediate.ts | 12 +- .../reporting/server/routes/generation.ts | 83 ++++++++++++ .../plugins/reporting/server/routes/index.ts | 82 +++-------- .../reporting/server/routes/jobs.test.js | 26 ++-- .../plugins/reporting/server/routes/jobs.ts | 26 ++-- .../server/routes/lib/get_document_payload.ts | 8 +- .../server/routes/lib/job_response_handler.js | 4 +- ..._handler.js => get_export_type_handler.ts} | 25 ++-- .../server/usage/get_reporting_usage.ts | 13 +- .../usage/reporting_usage_collector.test.js | 45 ++++-- .../server/usage/reporting_usage_collector.ts | 18 ++- x-pack/legacy/plugins/reporting/types.d.ts | 63 +++++---- 37 files changed, 645 insertions(+), 404 deletions(-) delete mode 100644 x-pack/legacy/plugins/reporting/common/export_types_registry.js create mode 100644 x-pack/legacy/plugins/reporting/export_types/csv/index.ts delete mode 100644 x-pack/legacy/plugins/reporting/export_types/csv/server/index.ts delete mode 100644 x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/index.ts create mode 100644 x-pack/legacy/plugins/reporting/export_types/png/index.ts delete mode 100644 x-pack/legacy/plugins/reporting/export_types/png/server/index.ts create mode 100644 x-pack/legacy/plugins/reporting/export_types/printable_pdf/index.ts delete mode 100644 x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/index.ts rename x-pack/legacy/plugins/reporting/{common => server/lib}/__tests__/export_types_registry.js (100%) create mode 100644 x-pack/legacy/plugins/reporting/server/routes/generation.ts rename x-pack/legacy/plugins/reporting/server/usage/{get_export_type_handler.js => get_export_type_handler.ts} (61%) diff --git a/x-pack/legacy/plugins/reporting/common/constants.ts b/x-pack/legacy/plugins/reporting/common/constants.ts index 723320e74bfbd..03b2d51c7b396 100644 --- a/x-pack/legacy/plugins/reporting/common/constants.ts +++ b/x-pack/legacy/plugins/reporting/common/constants.ts @@ -50,3 +50,9 @@ export const PNG_JOB_TYPE = 'PNG'; export const CSV_JOB_TYPE = 'csv'; export const CSV_FROM_SAVEDOBJECT_JOB_TYPE = 'csv_from_savedobject'; export const USES_HEADLESS_JOB_TYPES = [PDF_JOB_TYPE, PNG_JOB_TYPE]; + +export const LICENSE_TYPE_TRIAL = 'trial'; +export const LICENSE_TYPE_BASIC = 'basic'; +export const LICENSE_TYPE_STANDARD = 'standard'; +export const LICENSE_TYPE_GOLD = 'gold'; +export const LICENSE_TYPE_PLATINUM = 'platinum'; diff --git a/x-pack/legacy/plugins/reporting/common/export_types_registry.js b/x-pack/legacy/plugins/reporting/common/export_types_registry.js deleted file mode 100644 index 39abd8911e751..0000000000000 --- a/x-pack/legacy/plugins/reporting/common/export_types_registry.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isString } from 'lodash'; - -export class ExportTypesRegistry { - - constructor() { - this._map = new Map(); - } - - register(item) { - if (!isString(item.id)) { - throw new Error(`'item' must have a String 'id' property `); - } - - if (this._map.has(item.id)) { - throw new Error(`'item' with id ${item.id} has already been registered`); - } - - this._map.set(item.id, item); - } - - getAll() { - return this._map.values(); - } - - getSize() { - return this._map.size; - } - - getById(id) { - if (!this._map.has(id)) { - throw new Error(`Unknown id ${id}`); - } - - return this._map.get(id); - } - - get(callback) { - let result; - for (const value of this._map.values()) { - if (!callback(value)) { - continue; - } - - if (result) { - throw new Error('Found multiple items matching predicate.'); - } - - result = value; - } - - if (!result) { - throw new Error('Found no items matching predicate'); - } - - return result; - } - -} diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts index 2b4411584d752..152ef32e331b9 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts @@ -6,8 +6,12 @@ import * as Rx from 'rxjs'; import { first, mergeMap } from 'rxjs/operators'; -import { ServerFacade, CaptureConfig } from '../../../../types'; -import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; +import { + ServerFacade, + CaptureConfig, + HeadlessChromiumDriverFactory, + HeadlessChromiumDriver as HeadlessBrowser, +} from '../../../../types'; import { ElementsPositionAndAttribute, ScreenshotResults, @@ -26,10 +30,12 @@ import { getElementPositionAndAttributes } from './get_element_position_data'; import { getScreenshots } from './get_screenshots'; import { skipTelemetry } from './skip_telemetry'; -export function screenshotsObservableFactory(server: ServerFacade) { +export function screenshotsObservableFactory( + server: ServerFacade, + browserDriverFactory: HeadlessChromiumDriverFactory +) { const config = server.config(); const captureConfig: CaptureConfig = config.get('xpack.reporting.capture'); - const { browserDriverFactory } = server.plugins.reporting!; return function screenshotsObservable({ logger, diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/index.ts b/x-pack/legacy/plugins/reporting/export_types/csv/index.ts new file mode 100644 index 0000000000000..4f8aeb2be0c99 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/csv/index.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + CSV_JOB_TYPE as jobType, + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_BASIC, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, +} from '../../common/constants'; +import { ExportTypeDefinition, ESQueueCreateJobFn, ESQueueWorkerExecuteFn } from '../../types'; +import { metadata } from './metadata'; +import { createJobFactory } from './server/create_job'; +import { executeJobFactory } from './server/execute_job'; +import { JobParamsDiscoverCsv, JobDocPayloadDiscoverCsv } from './types'; + +export const getExportType = (): ExportTypeDefinition< + JobParamsDiscoverCsv, + ESQueueCreateJobFn, + JobDocPayloadDiscoverCsv, + ESQueueWorkerExecuteFn +> => ({ + ...metadata, + jobType, + jobContentExtension: 'csv', + createJobFactory, + executeJobFactory, + validLicenses: [ + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_BASIC, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + ], +}); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/index.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/index.ts deleted file mode 100644 index d752cdcd9779d..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ExportTypesRegistry } from '../../../types'; -import { createJobFactory } from './create_job'; -import { executeJobFactory } from './execute_job'; -import { metadata } from '../metadata'; -import { CSV_JOB_TYPE as jobType } from '../../../common/constants'; - -export function register(registry: ExportTypesRegistry) { - registry.register({ - ...metadata, - jobType, - jobContentExtension: 'csv', - createJobFactory, - executeJobFactory, - validLicenses: ['trial', 'basic', 'standard', 'gold', 'platinum'], - }); -} diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/index.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/index.ts index 68ad4a4b49155..4876cea0b1b28 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/index.ts @@ -4,9 +4,43 @@ * you may not use this file except in compliance with the Elastic License. */ +import { + CSV_FROM_SAVEDOBJECT_JOB_TYPE, + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_BASIC, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, +} from '../../common/constants'; +import { ExportTypeDefinition, ImmediateCreateJobFn, ImmediateExecuteFn } from '../../types'; +import { createJobFactory } from './server/create_job'; +import { executeJobFactory } from './server/execute_job'; +import { metadata } from './metadata'; +import { JobParamsPanelCsv } from './types'; + /* * These functions are exported to share with the API route handler that * generates csv from saved object immediately on request. */ export { executeJobFactory } from './server/execute_job'; export { createJobFactory } from './server/create_job'; + +export const getExportType = (): ExportTypeDefinition< + JobParamsPanelCsv, + ImmediateCreateJobFn, + JobParamsPanelCsv, + ImmediateExecuteFn +> => ({ + ...metadata, + jobType: CSV_FROM_SAVEDOBJECT_JOB_TYPE, + jobContentExtension: 'csv', + createJobFactory, + executeJobFactory, + validLicenses: [ + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_BASIC, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + ], +}); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/index.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/index.ts deleted file mode 100644 index b614fd3c681b3..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; -import { ExportTypesRegistry } from '../../../types'; -import { metadata } from '../metadata'; -import { createJobFactory } from './create_job'; -import { executeJobFactory } from './execute_job'; - -export function register(registry: ExportTypesRegistry) { - registry.register({ - ...metadata, - jobType: CSV_FROM_SAVEDOBJECT_JOB_TYPE, - jobContentExtension: 'csv', - createJobFactory, - executeJobFactory, - validLicenses: ['trial', 'basic', 'standard', 'gold', 'platinum'], - }); -} diff --git a/x-pack/legacy/plugins/reporting/export_types/png/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/index.ts new file mode 100644 index 0000000000000..bc00bc428f306 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/png/index.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + PNG_JOB_TYPE as jobType, + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, +} from '../../common/constants'; +import { ExportTypeDefinition, ESQueueCreateJobFn, ESQueueWorkerExecuteFn } from '../../types'; +import { createJobFactory } from './server/create_job'; +import { executeJobFactory } from './server/execute_job'; +import { metadata } from './metadata'; +import { JobParamsPNG, JobDocPayloadPNG } from './types'; + +export const getExportType = (): ExportTypeDefinition< + JobParamsPNG, + ESQueueCreateJobFn, + JobDocPayloadPNG, + ESQueueWorkerExecuteFn +> => ({ + ...metadata, + jobType, + jobContentEncoding: 'base64', + jobContentExtension: 'PNG', + createJobFactory, + executeJobFactory, + validLicenses: [ + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + ], +}); diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js index 867d537017f41..267c606449c3a 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js @@ -68,7 +68,7 @@ test(`passes browserTimezone to generatePng`, async () => { const generatePngObservable = generatePngObservableFactory(); generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); - const executeJob = executeJobFactory(mockServer); + const executeJob = executeJobFactory(mockServer, { browserDriverFactory: {} }); const browserTimezone = 'UTC'; await executeJob('pngJobId', { relativeUrl: '/app/kibana#/something', browserTimezone, headers: encryptedHeaders }, cancellationToken); @@ -76,7 +76,7 @@ test(`passes browserTimezone to generatePng`, async () => { }); test(`returns content_type of application/png`, async () => { - const executeJob = executeJobFactory(mockServer); + const executeJob = executeJobFactory(mockServer, { browserDriverFactory: {} }); const encryptedHeaders = await encryptHeaders({}); const generatePngObservable = generatePngObservableFactory(); @@ -93,7 +93,7 @@ test(`returns content of generatePng getBuffer base64 encoded`, async () => { const generatePngObservable = generatePngObservableFactory(); generatePngObservable.mockReturnValue(Rx.of(Buffer.from(testContent))); - const executeJob = executeJobFactory(mockServer); + const executeJob = executeJobFactory(mockServer, { browserDriverFactory: {} }); const encryptedHeaders = await encryptHeaders({}); const { content } = await executeJob('pngJobId', { relativeUrl: '/app/kibana#/something', timeRange: {}, headers: encryptedHeaders }, cancellationToken); diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts index 6678a83079d31..b289ae45dde67 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts @@ -7,7 +7,12 @@ import * as Rx from 'rxjs'; import { mergeMap, catchError, map, takeUntil } from 'rxjs/operators'; import { PLUGIN_ID, PNG_JOB_TYPE } from '../../../../common/constants'; -import { ServerFacade, ExecuteJobFactory, ESQueueWorkerExecuteFn } from '../../../../types'; +import { + ServerFacade, + ExecuteJobFactory, + ESQueueWorkerExecuteFn, + HeadlessChromiumDriverFactory, +} from '../../../../types'; import { LevelLogger } from '../../../../server/lib'; import { decryptJobHeaders, @@ -18,10 +23,13 @@ import { import { JobDocPayloadPNG } from '../../types'; import { generatePngObservableFactory } from '../lib/generate_png'; -export const executeJobFactory: ExecuteJobFactory> = function executeJobFactoryFn(server: ServerFacade) { - const generatePngObservable = generatePngObservableFactory(server); +type QueuedPngExecutorFactory = ExecuteJobFactory>; + +export const executeJobFactory: QueuedPngExecutorFactory = function executeJobFactoryFn( + server: ServerFacade, + { browserDriverFactory }: { browserDriverFactory: HeadlessChromiumDriverFactory } +) { + const generatePngObservable = generatePngObservableFactory(server, browserDriverFactory); const logger = LevelLogger.createForServer(server, [PLUGIN_ID, PNG_JOB_TYPE, 'execute']); return function executeJob( diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/index.ts deleted file mode 100644 index a569719f02324..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ExportTypesRegistry } from '../../../types'; -import { createJobFactory } from './create_job'; -import { executeJobFactory } from './execute_job'; -import { metadata } from '../metadata'; -import { PNG_JOB_TYPE as jobType } from '../../../common/constants'; - -export function register(registry: ExportTypesRegistry) { - registry.register({ - ...metadata, - jobType, - jobContentEncoding: 'base64', - jobContentExtension: 'PNG', - createJobFactory, - executeJobFactory, - validLicenses: ['trial', 'standard', 'gold', 'platinum'], - }); -} diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts index 90aeea25db858..e2b1474515786 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts @@ -7,13 +7,16 @@ import * as Rx from 'rxjs'; import { map } from 'rxjs/operators'; import { LevelLogger } from '../../../../server/lib'; -import { ServerFacade, ConditionalHeaders } from '../../../../types'; +import { ServerFacade, HeadlessChromiumDriverFactory, ConditionalHeaders } from '../../../../types'; import { screenshotsObservableFactory } from '../../../common/lib/screenshots'; import { PreserveLayout } from '../../../common/layouts/preserve_layout'; import { LayoutParams } from '../../../common/layouts/layout'; -export function generatePngObservableFactory(server: ServerFacade) { - const screenshotsObservable = screenshotsObservableFactory(server); +export function generatePngObservableFactory( + server: ServerFacade, + browserDriverFactory: HeadlessChromiumDriverFactory +) { + const screenshotsObservable = screenshotsObservableFactory(server, browserDriverFactory); return function generatePngObservable( logger: LevelLogger, diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/index.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/index.ts new file mode 100644 index 0000000000000..99880c1237a7a --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/index.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + PDF_JOB_TYPE as jobType, + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, +} from '../../common/constants'; +import { ExportTypeDefinition, ESQueueCreateJobFn, ESQueueWorkerExecuteFn } from '../../types'; +import { createJobFactory } from './server/create_job'; +import { executeJobFactory } from './server/execute_job'; +import { metadata } from './metadata'; +import { JobParamsPDF, JobDocPayloadPDF } from './types'; + +export const getExportType = (): ExportTypeDefinition< + JobParamsPDF, + ESQueueCreateJobFn, + JobDocPayloadPDF, + ESQueueWorkerExecuteFn +> => ({ + ...metadata, + jobType, + jobContentEncoding: 'base64', + jobContentExtension: 'pdf', + createJobFactory, + executeJobFactory, + validLicenses: [ + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + ], +}); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js index 8084c077ed23f..6a5c47829fd19 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js @@ -67,7 +67,7 @@ test(`passes browserTimezone to generatePdf`, async () => { const generatePdfObservable = generatePdfObservableFactory(); generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - const executeJob = executeJobFactory(mockServer); + const executeJob = executeJobFactory(mockServer, { browserDriverFactory: {} }); const browserTimezone = 'UTC'; await executeJob('pdfJobId', { objects: [], browserTimezone, headers: encryptedHeaders }, cancellationToken); @@ -84,7 +84,7 @@ test(`passes browserTimezone to generatePdf`, async () => { }); test(`returns content_type of application/pdf`, async () => { - const executeJob = executeJobFactory(mockServer); + const executeJob = executeJobFactory(mockServer, { browserDriverFactory: {} }); const encryptedHeaders = await encryptHeaders({}); const generatePdfObservable = generatePdfObservableFactory(); @@ -104,7 +104,7 @@ test(`returns content of generatePdf getBuffer base64 encoded`, async () => { const generatePdfObservable = generatePdfObservableFactory(); generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(testContent))); - const executeJob = executeJobFactory(mockServer); + const executeJob = executeJobFactory(mockServer, { browserDriverFactory: {} }); const encryptedHeaders = await encryptHeaders({}); const { content } = await executeJob('pdfJobId', { objects: [], timeRange: {}, headers: encryptedHeaders }, cancellationToken); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts index 543d5b587906d..e2b3183464cf2 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts @@ -6,7 +6,12 @@ import * as Rx from 'rxjs'; import { mergeMap, catchError, map, takeUntil } from 'rxjs/operators'; -import { ExecuteJobFactory, ESQueueWorkerExecuteFn, ServerFacade } from '../../../../types'; +import { + ServerFacade, + ExecuteJobFactory, + ESQueueWorkerExecuteFn, + HeadlessChromiumDriverFactory, +} from '../../../../types'; import { JobDocPayloadPDF } from '../../types'; import { PLUGIN_ID, PDF_JOB_TYPE } from '../../../../common/constants'; import { LevelLogger } from '../../../../server/lib'; @@ -19,10 +24,13 @@ import { getCustomLogo, } from '../../../common/execute_job/'; -export const executeJobFactory: ExecuteJobFactory> = function executeJobFactoryFn(server: ServerFacade) { - const generatePdfObservable = generatePdfObservableFactory(server); +type QueuedPdfExecutorFactory = ExecuteJobFactory>; + +export const executeJobFactory: QueuedPdfExecutorFactory = function executeJobFactoryFn( + server: ServerFacade, + { browserDriverFactory }: { browserDriverFactory: HeadlessChromiumDriverFactory } +) { + const generatePdfObservable = generatePdfObservableFactory(server, browserDriverFactory); const logger = LevelLogger.createForServer(server, [PLUGIN_ID, PDF_JOB_TYPE, 'execute']); return function executeJob( diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/index.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/index.ts deleted file mode 100644 index df798a7af23ec..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ExportTypesRegistry } from '../../../types'; -import { createJobFactory } from './create_job'; -import { executeJobFactory } from './execute_job'; -import { metadata } from '../metadata'; -import { PDF_JOB_TYPE as jobType } from '../../../common/constants'; - -export function register(registry: ExportTypesRegistry) { - registry.register({ - ...metadata, - jobType, - jobContentEncoding: 'base64', - jobContentExtension: 'pdf', - createJobFactory, - executeJobFactory, - validLicenses: ['trial', 'standard', 'gold', 'platinum'], - }); -} diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts index 1e0245ebd513f..898a13a2dfe80 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts @@ -8,7 +8,7 @@ import * as Rx from 'rxjs'; import { toArray, mergeMap } from 'rxjs/operators'; import { groupBy } from 'lodash'; import { LevelLogger } from '../../../../server/lib'; -import { ServerFacade, ConditionalHeaders } from '../../../../types'; +import { ServerFacade, HeadlessChromiumDriverFactory, ConditionalHeaders } from '../../../../types'; // @ts-ignore untyped module import { pdf } from './pdf'; import { screenshotsObservableFactory } from '../../../common/lib/screenshots'; @@ -26,8 +26,11 @@ const getTimeRange = (urlScreenshots: ScreenshotResults[]) => { return null; }; -export function generatePdfObservableFactory(server: ServerFacade) { - const screenshotsObservable = screenshotsObservableFactory(server); +export function generatePdfObservableFactory( + server: ServerFacade, + browserDriverFactory: HeadlessChromiumDriverFactory +) { + const screenshotsObservable = screenshotsObservableFactory(server, browserDriverFactory); const captureConcurrency = 1; return function generatePdfObservable( diff --git a/x-pack/legacy/plugins/reporting/index.ts b/x-pack/legacy/plugins/reporting/index.ts index 9add3accd262f..c0c9e458132f0 100644 --- a/x-pack/legacy/plugins/reporting/index.ts +++ b/x-pack/legacy/plugins/reporting/index.ts @@ -13,8 +13,7 @@ import { registerRoutes } from './server/routes'; import { LevelLogger, checkLicenseFactory, - createQueueFactory, - exportTypesRegistryFactory, + getExportTypesRegistry, runValidations, } from './server/lib'; import { config as reportingConfig } from './config'; @@ -74,20 +73,23 @@ export const reporting = (kibana: any) => { // TODO: Decouple Hapi: Build a server facade object based on the server to // pass through to the libs. Do not pass server directly async init(server: ServerFacade) { + const exportTypesRegistry = getExportTypesRegistry(); + let isCollectorReady = false; // Register a function with server to manage the collection of usage stats const { usageCollection } = server.newPlatform.setup.plugins; - registerReportingUsageCollector(usageCollection, server, () => isCollectorReady); + registerReportingUsageCollector( + usageCollection, + server, + () => isCollectorReady, + exportTypesRegistry + ); const logger = LevelLogger.createForServer(server, [PLUGIN_ID]); - const [exportTypesRegistry, browserFactory] = await Promise.all([ - exportTypesRegistryFactory(server), - createBrowserDriverFactory(server), - ]); - server.expose('exportTypesRegistry', exportTypesRegistry); + const browserDriverFactory = await createBrowserDriverFactory(server); logConfiguration(server, logger); - runValidations(server, logger, browserFactory); + runValidations(server, logger, browserDriverFactory); const { xpack_main: xpackMainPlugin } = server.plugins; mirrorPluginStatus(xpackMainPlugin, this); @@ -101,11 +103,8 @@ export const reporting = (kibana: any) => { // Post initialization of the above code, the collector is now ready to fetch its data isCollectorReady = true; - server.expose('browserDriverFactory', browserFactory); - server.expose('queue', createQueueFactory(server)); - // Reporting routes - registerRoutes(server, logger); + registerRoutes(server, exportTypesRegistry, browserDriverFactory, logger); }, deprecations({ unused }: any) { diff --git a/x-pack/legacy/plugins/reporting/common/__tests__/export_types_registry.js b/x-pack/legacy/plugins/reporting/server/lib/__tests__/export_types_registry.js similarity index 100% rename from x-pack/legacy/plugins/reporting/common/__tests__/export_types_registry.js rename to x-pack/legacy/plugins/reporting/server/lib/__tests__/export_types_registry.js diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts index 174c6d587e523..5cf760250ec0e 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts @@ -5,7 +5,12 @@ */ import { PLUGIN_ID } from '../../common/constants'; -import { ServerFacade, QueueConfig } from '../../types'; +import { + ServerFacade, + ExportTypesRegistry, + HeadlessChromiumDriverFactory, + QueueConfig, +} from '../../types'; // @ts-ignore import { Esqueue } from './esqueue'; import { createWorkerFactory } from './create_worker'; @@ -13,7 +18,15 @@ import { LevelLogger } from './level_logger'; // @ts-ignore import { createTaggedLogger } from './create_tagged_logger'; // TODO remove createTaggedLogger once esqueue is removed -export function createQueueFactory(server: ServerFacade): Esqueue { +interface CreateQueueFactoryOpts { + exportTypesRegistry: ExportTypesRegistry; + browserDriverFactory: HeadlessChromiumDriverFactory; +} + +export function createQueueFactory( + server: ServerFacade, + { exportTypesRegistry, browserDriverFactory }: CreateQueueFactoryOpts +): Esqueue { const queueConfig: QueueConfig = server.config().get('xpack.reporting.queue'); const index = server.config().get('xpack.reporting.index'); @@ -29,7 +42,7 @@ export function createQueueFactory(server: ServerFacade): Esqueue { if (queueConfig.pollEnabled) { // create workers to poll the index for idle jobs waiting to be claimed and executed - const createWorker = createWorkerFactory(server); + const createWorker = createWorkerFactory(server, { exportTypesRegistry, browserDriverFactory }); createWorker(queue); } else { const logger = LevelLogger.createForServer(server, [PLUGIN_ID, 'create_queue']); diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts b/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts index afad8f096a8bb..8f843752491ec 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts @@ -5,7 +5,8 @@ */ import * as sinon from 'sinon'; -import { ServerFacade } from '../../types'; +import { ServerFacade, HeadlessChromiumDriverFactory } from '../../types'; +import { ExportTypesRegistry } from './export_types_registry'; import { createWorkerFactory } from './create_worker'; // @ts-ignore import { Esqueue } from './esqueue'; @@ -22,16 +23,17 @@ configGetStub.withArgs('server.uuid').returns('g9ymiujthvy6v8yrh7567g6fwzgzftzfr const executeJobFactoryStub = sinon.stub(); -const getMockServer = ( - exportTypes: any[] = [{ executeJobFactory: executeJobFactoryStub }] -): ServerFacade => { +const getMockServer = (): ServerFacade => { return ({ log: sinon.stub(), - expose: sinon.stub(), config: () => ({ get: configGetStub }), - plugins: { reporting: { exportTypesRegistry: { getAll: () => exportTypes } } }, } as unknown) as ServerFacade; }; +const getMockExportTypesRegistry = ( + exportTypes: any[] = [{ executeJobFactory: executeJobFactoryStub }] +) => ({ + getAll: () => exportTypes, +}); describe('Create Worker', () => { let queue: Esqueue; @@ -44,7 +46,11 @@ describe('Create Worker', () => { }); test('Creates a single Esqueue worker for Reporting', async () => { - const createWorker = createWorkerFactory(getMockServer()); + const exportTypesRegistry = getMockExportTypesRegistry(); + const createWorker = createWorkerFactory(getMockServer(), { + exportTypesRegistry: exportTypesRegistry as ExportTypesRegistry, + browserDriverFactory: {} as HeadlessChromiumDriverFactory, + }); const registerWorkerSpy = sinon.spy(queue, 'registerWorker'); createWorker(queue); @@ -68,15 +74,17 @@ Object { }); test('Creates a single Esqueue worker for Reporting, even if there are multiple export types', async () => { - const createWorker = createWorkerFactory( - getMockServer([ - { executeJobFactory: executeJobFactoryStub }, - { executeJobFactory: executeJobFactoryStub }, - { executeJobFactory: executeJobFactoryStub }, - { executeJobFactory: executeJobFactoryStub }, - { executeJobFactory: executeJobFactoryStub }, - ]) - ); + const exportTypesRegistry = getMockExportTypesRegistry([ + { executeJobFactory: executeJobFactoryStub }, + { executeJobFactory: executeJobFactoryStub }, + { executeJobFactory: executeJobFactoryStub }, + { executeJobFactory: executeJobFactoryStub }, + { executeJobFactory: executeJobFactoryStub }, + ]); + const createWorker = createWorkerFactory(getMockServer(), { + exportTypesRegistry: exportTypesRegistry as ExportTypesRegistry, + browserDriverFactory: {} as HeadlessChromiumDriverFactory, + }); const registerWorkerSpy = sinon.spy(queue, 'registerWorker'); createWorker(queue); diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts b/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts index 01f59099a1d99..1326e411b6c5c 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts @@ -5,6 +5,7 @@ */ import { PLUGIN_ID } from '../../common/constants'; +import { ExportTypesRegistry, HeadlessChromiumDriverFactory } from '../../types'; import { CancellationToken } from '../../common/cancellation_token'; import { ESQueueInstance, @@ -21,14 +22,21 @@ import { import { events as esqueueEvents } from './esqueue'; import { LevelLogger } from './level_logger'; -export function createWorkerFactory(server: ServerFacade) { +interface CreateWorkerFactoryOpts { + exportTypesRegistry: ExportTypesRegistry; + browserDriverFactory: HeadlessChromiumDriverFactory; +} + +export function createWorkerFactory( + server: ServerFacade, + { exportTypesRegistry, browserDriverFactory }: CreateWorkerFactoryOpts +) { type JobDocPayloadType = JobDocPayload; const config = server.config(); const logger = LevelLogger.createForServer(server, [PLUGIN_ID, 'queue-worker']); const queueConfig: QueueConfig = config.get('xpack.reporting.queue'); const kibanaName: string = config.get('server.name'); const kibanaId: string = config.get('server.uuid'); - const { exportTypesRegistry } = server.plugins.reporting!; // Once more document types are added, this will need to be passed in return function createWorker(queue: ESQueueInstance) { @@ -41,8 +49,9 @@ export function createWorkerFactory(server: ServerFacade) { for (const exportType of exportTypesRegistry.getAll() as Array< ExportTypeDefinition >) { - const executeJobFactory = exportType.executeJobFactory(server); - jobExecutors.set(exportType.jobType, executeJobFactory); + // TODO: the executeJobFn should be unwrapped in the register method of the export types registry + const jobExecutor = exportType.executeJobFactory(server, { browserDriverFactory }); + jobExecutors.set(exportType.jobType, jobExecutor); } const workerFn = (jobSource: JobSource, ...workerRestArgs: any[]) => { diff --git a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts index a8cefa3fdc49b..2d044ab31a160 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts @@ -8,12 +8,14 @@ import { get } from 'lodash'; // @ts-ignore import { events as esqueueEvents } from './esqueue'; import { + EnqueueJobFn, ESQueueCreateJobFn, ImmediateCreateJobFn, Job, ServerFacade, RequestFacade, Logger, + ExportTypesRegistry, CaptureConfig, QueueConfig, ConditionalHeaders, @@ -26,13 +28,20 @@ interface ConfirmedJob { _primary_term: number; } -export function enqueueJobFactory(server: ServerFacade) { +interface EnqueueJobFactoryOpts { + exportTypesRegistry: ExportTypesRegistry; + esqueue: any; +} + +export function enqueueJobFactory( + server: ServerFacade, + { exportTypesRegistry, esqueue }: EnqueueJobFactoryOpts +): EnqueueJobFn { const config = server.config(); const captureConfig: CaptureConfig = config.get('xpack.reporting.capture'); const browserType = captureConfig.browser.type; const maxAttempts = captureConfig.maxAttempts; const queueConfig: QueueConfig = config.get('xpack.reporting.queue'); - const { exportTypesRegistry, queue: jobQueue } = server.plugins.reporting!; return async function enqueueJob( parentLogger: Logger, @@ -46,6 +55,12 @@ export function enqueueJobFactory(server: ServerFacade) { const logger = parentLogger.clone(['queue-job']); const exportType = exportTypesRegistry.getById(exportTypeId); + + if (exportType == null) { + throw new Error(`Export type ${exportTypeId} does not exist in the registry!`); + } + + // TODO: the createJobFn should be unwrapped in the register method of the export types registry const createJob = exportType.createJobFactory(server) as CreateJobFn; const payload = await createJob(jobParams, headers, request); @@ -57,7 +72,7 @@ export function enqueueJobFactory(server: ServerFacade) { }; return new Promise((resolve, reject) => { - const job = jobQueue.addJob(exportType.jobType, payload, options); + const job = esqueue.addJob(exportType.jobType, payload, options); job.on(esqueueEvents.EVENT_JOB_CREATED, (createdJob: ConfirmedJob) => { if (createdJob.id === job.id) { diff --git a/x-pack/legacy/plugins/reporting/server/lib/export_types_registry.ts b/x-pack/legacy/plugins/reporting/server/lib/export_types_registry.ts index af1457ab52fe2..d553cc07ae3ef 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/export_types_registry.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/export_types_registry.ts @@ -4,40 +4,110 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resolve as pathResolve } from 'path'; -import glob from 'glob'; -import { ServerFacade } from '../../types'; -import { PLUGIN_ID } from '../../common/constants'; -import { oncePerServer } from './once_per_server'; -import { LevelLogger } from './level_logger'; -// @ts-ignore untype module TODO -import { ExportTypesRegistry } from '../../common/export_types_registry'; - -function scan(pattern: string) { - return new Promise((resolve, reject) => { - glob(pattern, {}, (err, files) => { - if (err) { - return reject(err); +import memoizeOne from 'memoize-one'; +import { isString } from 'lodash'; +import { getExportType as getTypeCsv } from '../../export_types/csv'; +import { getExportType as getTypeCsvFromSavedObject } from '../../export_types/csv_from_savedobject'; +import { getExportType as getTypePng } from '../../export_types/png'; +import { getExportType as getTypePrintablePdf } from '../../export_types/printable_pdf'; +import { ExportTypeDefinition } from '../../types'; + +type GetCallbackFn = ( + item: ExportTypeDefinition +) => boolean; +// => ExportTypeDefinition + +export class ExportTypesRegistry { + private _map: Map> = new Map(); + + constructor() {} + + register( + item: ExportTypeDefinition + ): void { + if (!isString(item.id)) { + throw new Error(`'item' must have a String 'id' property `); + } + + if (this._map.has(item.id)) { + throw new Error(`'item' with id ${item.id} has already been registered`); + } + + // TODO: Unwrap the execute function from the item's executeJobFactory + // Move that work out of server/lib/create_worker to reduce dependence on ESQueue + this._map.set(item.id, item); + } + + getAll() { + return Array.from(this._map.values()); + } + + getSize() { + return this._map.size; + } + + getById( + id: string + ): ExportTypeDefinition { + if (!this._map.has(id)) { + throw new Error(`Unknown id ${id}`); + } + + return this._map.get(id) as ExportTypeDefinition< + JobParamsType, + CreateJobFnType, + JobPayloadType, + ExecuteJobFnType + >; + } + + get( + findType: GetCallbackFn + ): ExportTypeDefinition { + let result; + for (const value of this._map.values()) { + if (!findType(value)) { + continue; // try next value } + const foundResult: ExportTypeDefinition< + JobParamsType, + CreateJobFnType, + JobPayloadType, + ExecuteJobFnType + > = value; - resolve(files); - }); - }); -} + if (result) { + throw new Error('Found multiple items matching predicate.'); + } + + result = foundResult; + } -const pattern = pathResolve(__dirname, '../../export_types/*/server/index.[jt]s'); -async function exportTypesRegistryFn(server: ServerFacade) { - const logger = LevelLogger.createForServer(server, [PLUGIN_ID, 'exportTypes']); - const exportTypesRegistry = new ExportTypesRegistry(); - const files: string[] = (await scan(pattern)) as string[]; + if (!result) { + throw new Error('Found no items matching predicate'); + } + + return result; + } +} - files.forEach(file => { - logger.debug(`Found exportType at ${file}`); +function getExportTypesRegistryFn(): ExportTypesRegistry { + const registry = new ExportTypesRegistry(); - const { register } = require(file); // eslint-disable-line @typescript-eslint/no-var-requires - register(exportTypesRegistry); + /* this replaces the previously async method of registering export types, + * where this would run a directory scan and types would be registered via + * discovery */ + const getTypeFns: Array<() => ExportTypeDefinition> = [ + getTypeCsv, + getTypeCsvFromSavedObject, + getTypePng, + getTypePrintablePdf, + ]; + getTypeFns.forEach(getType => { + registry.register(getType()); }); - return exportTypesRegistry; + return registry; } -export const exportTypesRegistryFactory = oncePerServer(exportTypesRegistryFn); +// FIXME: is this the best way to return a singleton? +export const getExportTypesRegistry = memoizeOne(getExportTypesRegistryFn); diff --git a/x-pack/legacy/plugins/reporting/server/lib/index.ts b/x-pack/legacy/plugins/reporting/server/lib/index.ts index b11f7bd95d9ef..50d1a276b6b5d 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/index.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/index.ts @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export { exportTypesRegistryFactory } from './export_types_registry'; +export { getExportTypesRegistry } from './export_types_registry'; // @ts-ignore untyped module export { checkLicenseFactory } from './check_license'; export { LevelLogger } from './level_logger'; -export { createQueueFactory } from './create_queue'; export { cryptoFactory } from './crypto'; export { oncePerServer } from './once_per_server'; export { runValidations } from './validate'; +export { createQueueFactory } from './create_queue'; +export { enqueueJobFactory } from './enqueue_job'; diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index 8b0dd1a6e7c4f..bc96c27f64c10 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -10,6 +10,7 @@ import { ServerFacade, RequestFacade, ResponseFacade, + HeadlessChromiumDriverFactory, ReportingResponseToolkit, Logger, JobDocOutputExecuted, @@ -45,8 +46,17 @@ export function registerGenerateCsvFromSavedObjectImmediate( handler: async (request: RequestFacade, h: ReportingResponseToolkit) => { const logger = parentLogger.clone(['savedobject-csv']); const jobParams = getJobParamsFromRequest(request, { isImmediate: true }); + + /* TODO these functions should be made available in the export types registry: + * + * const { createJobFn, executeJobFn } = exportTypesRegistry.getById(CSV_FROM_SAVEDOBJECT_JOB_TYPE) + * + * Calling an execute job factory requires passing a browserDriverFactory option, so we should not call the factory from here + */ const createJobFn = createJobFactory(server); - const executeJobFn = executeJobFactory(server); + const executeJobFn = executeJobFactory(server, { + browserDriverFactory: {} as HeadlessChromiumDriverFactory, + }); const jobDocPayload: JobDocPayloadPanelCsv = await createJobFn( jobParams, request.headers, diff --git a/x-pack/legacy/plugins/reporting/server/routes/generation.ts b/x-pack/legacy/plugins/reporting/server/routes/generation.ts new file mode 100644 index 0000000000000..7bed7bc5773e4 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/routes/generation.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import boom from 'boom'; +import { API_BASE_URL } from '../../common/constants'; +import { + ServerFacade, + ExportTypesRegistry, + HeadlessChromiumDriverFactory, + RequestFacade, + ReportingResponseToolkit, + Logger, +} from '../../types'; +import { registerGenerateFromJobParams } from './generate_from_jobparams'; +import { registerGenerateCsvFromSavedObject } from './generate_from_savedobject'; +import { registerGenerateCsvFromSavedObjectImmediate } from './generate_from_savedobject_immediate'; +import { registerLegacy } from './legacy'; +import { createQueueFactory, enqueueJobFactory } from '../lib'; + +export function registerJobGenerationRoutes( + server: ServerFacade, + exportTypesRegistry: ExportTypesRegistry, + browserDriverFactory: HeadlessChromiumDriverFactory, + logger: Logger +) { + const config = server.config(); + const DOWNLOAD_BASE_URL = config.get('server.basePath') + `${API_BASE_URL}/jobs/download`; + // @ts-ignore TODO + const { errors: esErrors } = server.plugins.elasticsearch.getCluster('admin'); + + const esqueue = createQueueFactory(server, { exportTypesRegistry, browserDriverFactory }); + const enqueueJob = enqueueJobFactory(server, { exportTypesRegistry, esqueue }); + + /* + * Generates enqueued job details to use in responses + */ + async function handler( + exportTypeId: string, + jobParams: object, + request: RequestFacade, + h: ReportingResponseToolkit + ) { + const user = request.pre.user; + const headers = request.headers; + + const job = await enqueueJob(logger, exportTypeId, jobParams, user, headers, request); + + // return the queue's job information + const jobJson = job.toJSON(); + + return h + .response({ + path: `${DOWNLOAD_BASE_URL}/${jobJson.id}`, + job: jobJson, + }) + .type('application/json'); + } + + function handleError(exportTypeId: string, err: Error) { + if (err instanceof esErrors['401']) { + return boom.unauthorized(`Sorry, you aren't authenticated`); + } + if (err instanceof esErrors['403']) { + return boom.forbidden(`Sorry, you are not authorized to create ${exportTypeId} reports`); + } + if (err instanceof esErrors['404']) { + return boom.boomify(err, { statusCode: 404 }); + } + return err; + } + + registerGenerateFromJobParams(server, handler, handleError); + registerLegacy(server, handler, handleError); + + // Register beta panel-action download-related API's + if (config.get('xpack.reporting.csv.enablePanelActionDownload')) { + registerGenerateCsvFromSavedObject(server, handler, handleError); + registerGenerateCsvFromSavedObjectImmediate(server, logger); + } +} diff --git a/x-pack/legacy/plugins/reporting/server/routes/index.ts b/x-pack/legacy/plugins/reporting/server/routes/index.ts index c48a37a36812e..da664dcb91ae4 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/index.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/index.ts @@ -4,69 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import boom from 'boom'; -import { API_BASE_URL } from '../../common/constants'; -import { ServerFacade, RequestFacade, ReportingResponseToolkit, Logger } from '../../types'; -import { enqueueJobFactory } from '../lib/enqueue_job'; -import { registerGenerateFromJobParams } from './generate_from_jobparams'; -import { registerGenerateCsvFromSavedObject } from './generate_from_savedobject'; -import { registerGenerateCsvFromSavedObjectImmediate } from './generate_from_savedobject_immediate'; -import { registerJobs } from './jobs'; -import { registerLegacy } from './legacy'; - -export function registerRoutes(server: ServerFacade, logger: Logger) { - const config = server.config(); - const DOWNLOAD_BASE_URL = config.get('server.basePath') + `${API_BASE_URL}/jobs/download`; - // @ts-ignore TODO - const { errors: esErrors } = server.plugins.elasticsearch.getCluster('admin'); - const enqueueJob = enqueueJobFactory(server); - - /* - * Generates enqueued job details to use in responses - */ - async function handler( - exportTypeId: string, - jobParams: object, - request: RequestFacade, - h: ReportingResponseToolkit - ) { - const user = request.pre.user; - const headers = request.headers; - - const job = await enqueueJob(logger, exportTypeId, jobParams, user, headers, request); - - // return the queue's job information - const jobJson = job.toJSON(); - - return h - .response({ - path: `${DOWNLOAD_BASE_URL}/${jobJson.id}`, - job: jobJson, - }) - .type('application/json'); - } - - function handleError(exportTypeId: string, err: Error) { - if (err instanceof esErrors['401']) { - return boom.unauthorized(`Sorry, you aren't authenticated`); - } - if (err instanceof esErrors['403']) { - return boom.forbidden(`Sorry, you are not authorized to create ${exportTypeId} reports`); - } - if (err instanceof esErrors['404']) { - return boom.boomify(err, { statusCode: 404 }); - } - return err; - } - - registerGenerateFromJobParams(server, handler, handleError); - registerLegacy(server, handler, handleError); - - // Register beta panel-action download-related API's - if (config.get('xpack.reporting.csv.enablePanelActionDownload')) { - registerGenerateCsvFromSavedObject(server, handler, handleError); - registerGenerateCsvFromSavedObjectImmediate(server, logger); - } - - registerJobs(server); +import { + ServerFacade, + ExportTypesRegistry, + HeadlessChromiumDriverFactory, + Logger, +} from '../../types'; +import { registerJobGenerationRoutes } from './generation'; +import { registerJobInfoRoutes } from './jobs'; + +export function registerRoutes( + server: ServerFacade, + exportTypesRegistry: ExportTypesRegistry, + browserDriverFactory: HeadlessChromiumDriverFactory, + logger: Logger +) { + registerJobGenerationRoutes(server, exportTypesRegistry, browserDriverFactory, logger); + registerJobInfoRoutes(server, exportTypesRegistry, logger); } diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js index 2d1f48dd790a0..c4d4f6e42c9cb 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js +++ b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js @@ -6,8 +6,8 @@ import Hapi from 'hapi'; import { difference, memoize } from 'lodash'; -import { registerJobs } from './jobs'; -import { ExportTypesRegistry } from '../../common/export_types_registry'; +import { registerJobInfoRoutes } from './jobs'; +import { ExportTypesRegistry } from '../lib/export_types_registry'; jest.mock('./lib/authorized_user_pre_routing', () => { return { authorizedUserPreRoutingFactory: () => () => ({}) @@ -19,13 +19,17 @@ jest.mock('./lib/reporting_feature_pre_routing', () => { }; }); - let mockServer; +let exportTypesRegistry; +const mockLogger = { + error: jest.fn(), + debug: jest.fn(), +}; beforeEach(() => { mockServer = new Hapi.Server({ debug: false, port: 8080, routes: { log: { collect: true } } }); mockServer.config = memoize(() => ({ get: jest.fn() })); - const exportTypesRegistry = new ExportTypesRegistry(); + exportTypesRegistry = new ExportTypesRegistry(); exportTypesRegistry.register({ id: 'unencoded', jobType: 'unencodedJobType', @@ -44,9 +48,6 @@ beforeEach(() => { callWithRequest: jest.fn(), callWithInternalUser: jest.fn(), })) - }, - reporting: { - exportTypesRegistry } }; }); @@ -63,7 +64,7 @@ test(`returns 404 if job not found`, async () => { mockServer.plugins.elasticsearch.getCluster('admin') .callWithInternalUser.mockReturnValue(Promise.resolve(getHits())); - registerJobs(mockServer); + registerJobInfoRoutes(mockServer, exportTypesRegistry, mockLogger); const request = { method: 'GET', @@ -79,7 +80,7 @@ test(`returns 401 if not valid job type`, async () => { mockServer.plugins.elasticsearch.getCluster('admin') .callWithInternalUser.mockReturnValue(Promise.resolve(getHits({ jobtype: 'invalidJobType' }))); - registerJobs(mockServer); + registerJobInfoRoutes(mockServer, exportTypesRegistry, mockLogger); const request = { method: 'GET', @@ -91,12 +92,11 @@ test(`returns 401 if not valid job type`, async () => { }); describe(`when job is incomplete`, () => { - const getIncompleteResponse = async () => { mockServer.plugins.elasticsearch.getCluster('admin') .callWithInternalUser.mockReturnValue(Promise.resolve(getHits({ jobtype: 'unencodedJobType', status: 'pending' }))); - registerJobs(mockServer); + registerJobInfoRoutes(mockServer, exportTypesRegistry, mockLogger); const request = { method: 'GET', @@ -133,7 +133,7 @@ describe(`when job is failed`, () => { mockServer.plugins.elasticsearch.getCluster('admin') .callWithInternalUser.mockReturnValue(Promise.resolve(hits)); - registerJobs(mockServer); + registerJobInfoRoutes(mockServer, exportTypesRegistry, mockLogger); const request = { method: 'GET', @@ -178,7 +178,7 @@ describe(`when job is completed`, () => { }); mockServer.plugins.elasticsearch.getCluster('admin').callWithInternalUser.mockReturnValue(Promise.resolve(hits)); - registerJobs(mockServer); + registerJobInfoRoutes(mockServer, exportTypesRegistry, mockLogger); const request = { method: 'GET', diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.ts b/x-pack/legacy/plugins/reporting/server/routes/jobs.ts index 71d9f0d3ae13b..fd5014911d262 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/jobs.ts @@ -8,6 +8,8 @@ import boom from 'boom'; import { API_BASE_URL } from '../../common/constants'; import { ServerFacade, + ExportTypesRegistry, + Logger, RequestFacade, ReportingResponseToolkit, JobDocOutput, @@ -24,7 +26,11 @@ import { const MAIN_ENTRY = `${API_BASE_URL}/jobs`; -export function registerJobs(server: ServerFacade) { +export function registerJobInfoRoutes( + server: ServerFacade, + exportTypesRegistry: ExportTypesRegistry, + logger: Logger +) { const jobsQuery = jobsQueryFactory(server); const getRouteConfig = getRouteConfigFactoryManagementPre(server); const getRouteConfigDownload = getRouteConfigFactoryDownloadPre(server); @@ -119,7 +125,7 @@ export function registerJobs(server: ServerFacade) { }); // trigger a download of the output from a job - const jobResponseHandler = jobResponseHandlerFactory(server); + const jobResponseHandler = jobResponseHandlerFactory(server, exportTypesRegistry); server.route({ path: `${MAIN_ENTRY}/download/{docId}`, method: 'GET', @@ -136,13 +142,15 @@ export function registerJobs(server: ServerFacade) { const { statusCode } = response; if (statusCode !== 200) { - const logLevel = statusCode === 500 ? 'error' : 'debug'; - server.log( - [logLevel, 'reporting', 'download'], - `Report ${docId} has non-OK status: [${statusCode}] Reason: [${JSON.stringify( - response.source - )}]` - ); + if (statusCode === 500) { + logger.error(`Report ${docId} has failed: ${JSON.stringify(response.source)}`); + } else { + logger.debug( + `Report ${docId} has non-OK status: [${statusCode}] Reason: [${JSON.stringify( + response.source + )}]` + ); + } } if (!response.isBoom) { diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts index a69c19c006b61..c3a30f9dda454 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts @@ -9,6 +9,7 @@ import * as _ from 'lodash'; import contentDisposition from 'content-disposition'; import { ServerFacade, + ExportTypesRegistry, ExportTypeDefinition, JobDocExecuted, JobDocOutputExecuted, @@ -40,9 +41,10 @@ const getReportingHeaders = (output: JobDocOutputExecuted, exportType: ExportTyp return metaDataHeaders; }; -export function getDocumentPayloadFactory(server: ServerFacade) { - const exportTypesRegistry = server.plugins.reporting!.exportTypesRegistry; - +export function getDocumentPayloadFactory( + server: ServerFacade, + exportTypesRegistry: ExportTypesRegistry +) { function encodeContent(content: string | null, exportType: ExportTypeType) { switch (exportType.jobContentEncoding) { case 'base64': diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.js b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.js index 758c50816c381..6bc370506a255 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.js +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.js @@ -9,9 +9,9 @@ import { jobsQueryFactory } from '../../lib/jobs_query'; import { WHITELISTED_JOB_CONTENT_TYPES } from '../../../common/constants'; import { getDocumentPayloadFactory } from './get_document_payload'; -export function jobResponseHandlerFactory(server) { +export function jobResponseHandlerFactory(server, exportTypesRegistry) { const jobsQuery = jobsQueryFactory(server); - const getDocumentPayload = getDocumentPayloadFactory(server); + const getDocumentPayload = getDocumentPayloadFactory(server, exportTypesRegistry); return function jobResponseHandler(validJobTypes, user, h, params, opts = {}) { const { docId } = params; diff --git a/x-pack/legacy/plugins/reporting/server/usage/get_export_type_handler.js b/x-pack/legacy/plugins/reporting/server/usage/get_export_type_handler.ts similarity index 61% rename from x-pack/legacy/plugins/reporting/server/usage/get_export_type_handler.js rename to x-pack/legacy/plugins/reporting/server/usage/get_export_type_handler.ts index a1949b21aa086..f8913a0dcea6b 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/get_export_type_handler.js +++ b/x-pack/legacy/plugins/reporting/server/usage/get_export_type_handler.ts @@ -4,17 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { exportTypesRegistryFactory } from '../lib/export_types_registry'; +import { XPackMainPlugin } from '../../../xpack_main/xpack_main'; +import { ExportTypesRegistry } from '../lib/export_types_registry'; /* * Gets a handle to the Reporting export types registry and returns a few * functions for examining them - * @param {Object} server: Kibana server * @return {Object} export type handler */ -export async function getExportTypesHandler(server) { - const exportTypesRegistry = await exportTypesRegistryFactory(server); - +export function getExportTypesHandler(exportTypesRegistry: ExportTypesRegistry) { return { /* * Based on the X-Pack license and which export types are available, @@ -23,12 +21,17 @@ export async function getExportTypesHandler(server) { * @param {Object} xpackInfo: xpack_main plugin info object * @return {Object} availability of each export type */ - getAvailability(xpackInfo) { - const exportTypesAvailability = {}; + getAvailability(xpackInfo: XPackMainPlugin['info']) { + const exportTypesAvailability: { [exportType: string]: boolean } = {}; const xpackInfoAvailable = xpackInfo && xpackInfo.isAvailable(); - const licenseType = xpackInfo.license.getType(); - for(const exportType of exportTypesRegistry.getAll()) { - exportTypesAvailability[exportType.jobType] = xpackInfoAvailable ? exportType.validLicenses.includes(licenseType) : false; + const licenseType: string | undefined = xpackInfo.license.getType(); + if (!licenseType) { + throw new Error('No license type returned from XPackMainPlugin#info!'); + } + for (const exportType of exportTypesRegistry.getAll()) { + exportTypesAvailability[exportType.jobType] = xpackInfoAvailable + ? exportType.validLicenses.includes(licenseType) + : false; } return exportTypesAvailability; @@ -39,6 +42,6 @@ export async function getExportTypesHandler(server) { */ getNumExportTypes() { return exportTypesRegistry.getSize(); - } + }, }; } diff --git a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts b/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts index 0c85d39ae55d3..bd2d0cb835a79 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { ServerFacade, ESCallCluster } from '../../types'; +import { ServerFacade, ExportTypesRegistry, ESCallCluster } from '../../types'; import { AggregationBuckets, AggregationResults, @@ -16,7 +16,6 @@ import { RangeStats, } from './types'; import { decorateRangeStats } from './decorate_range_stats'; -// @ts-ignore untyped module import { getExportTypesHandler } from './get_export_type_handler'; const JOB_TYPES_KEY = 'jobTypes'; @@ -101,7 +100,11 @@ async function handleResponse( }; } -export async function getReportingUsage(server: ServerFacade, callCluster: ESCallCluster) { +export async function getReportingUsage( + server: ServerFacade, + callCluster: ESCallCluster, + exportTypesRegistry: ExportTypesRegistry +) { const config = server.config(); const reportingIndex = config.get('xpack.reporting.index'); @@ -138,13 +141,13 @@ export async function getReportingUsage(server: ServerFacade, callCluster: ESCal return callCluster('search', params) .then((response: AggregationResults) => handleResponse(server, response)) - .then(async (usage: RangeStatSets) => { + .then((usage: RangeStatSets) => { // Allow this to explicitly throw an exception if/when this config is deprecated, // because we shouldn't collect browserType in that case! const browserType = config.get('xpack.reporting.capture.browser.type'); const xpackInfo = server.plugins.xpack_main.info; - const exportTypesHandler = await getExportTypesHandler(server); + const exportTypesHandler = getExportTypesHandler(exportTypesRegistry); const availability = exportTypesHandler.getAvailability(xpackInfo) as FeatureAvailabilityMap; const { lastDay, last7Days, ...all } = usage; diff --git a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.js b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.js index dd040a40a126a..d5b7232551d60 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.js +++ b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.js @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import sinon from 'sinon'; +import { getExportTypesRegistry } from '../lib/export_types_registry'; import { getReportingUsageCollector } from './reporting_usage_collector'; +const exportTypesRegistry = getExportTypesRegistry(); + function getMockUsageCollection() { class MockUsageCollector { constructor(_server, { fetch }) { // eslint-disable-line no-unused-vars @@ -40,7 +43,6 @@ function getServerMock(customization) { }, }, }, - expose: () => {}, log: () => {}, config: () => ({ get: key => { @@ -67,8 +69,13 @@ describe('license checks', () => { .returns('basic'); const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); const usageCollection = getMockUsageCollection(); - const { fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithBasicLicenseMock); - usageStats = await getReportingUsage(callClusterMock); + const { fetch: getReportingUsage } = getReportingUsageCollector( + usageCollection, + serverWithBasicLicenseMock, + () => {}, + exportTypesRegistry + ); + usageStats = await getReportingUsage(callClusterMock, exportTypesRegistry); }); test('sets enables to true', async () => { @@ -93,8 +100,13 @@ describe('license checks', () => { .returns('none'); const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); const usageCollection = getMockUsageCollection(); - const { fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithNoLicenseMock); - usageStats = await getReportingUsage(callClusterMock); + const { fetch: getReportingUsage } = getReportingUsageCollector( + usageCollection, + serverWithNoLicenseMock, + () => {}, + exportTypesRegistry + ); + usageStats = await getReportingUsage(callClusterMock, exportTypesRegistry); }); test('sets enables to true', async () => { @@ -121,9 +133,11 @@ describe('license checks', () => { const usageCollection = getMockUsageCollection(); const { fetch: getReportingUsage } = getReportingUsageCollector( usageCollection, - serverWithPlatinumLicenseMock + serverWithPlatinumLicenseMock, + () => {}, + exportTypesRegistry ); - usageStats = await getReportingUsage(callClusterMock); + usageStats = await getReportingUsage(callClusterMock, exportTypesRegistry); }); test('sets enables to true', async () => { @@ -148,8 +162,13 @@ describe('license checks', () => { .returns('basic'); const callClusterMock = jest.fn(() => Promise.resolve({})); const usageCollection = getMockUsageCollection(); - const { fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithBasicLicenseMock); - usageStats = await getReportingUsage(callClusterMock); + const { fetch: getReportingUsage } = getReportingUsageCollector( + usageCollection, + serverWithBasicLicenseMock, + () => {}, + exportTypesRegistry + ); + usageStats = await getReportingUsage(callClusterMock, exportTypesRegistry); }); test('sets enables to true', async () => { @@ -170,7 +189,12 @@ describe('data modeling', () => { serverWithPlatinumLicenseMock.plugins.xpack_main.info.license.getType = sinon .stub() .returns('platinum'); - ({ fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithPlatinumLicenseMock)); + ({ fetch: getReportingUsage } = getReportingUsageCollector( + usageCollection, + serverWithPlatinumLicenseMock, + () => {}, + exportTypesRegistry + )); }); test('with normal looking usage data', async () => { @@ -295,6 +319,7 @@ describe('data modeling', () => { }) ) ); + const usageStats = await getReportingUsage(callClusterMock); expect(usageStats).toMatchInlineSnapshot(` Object { diff --git a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts index 0a7ef0a194434..40cf315a78cbb 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts @@ -7,7 +7,7 @@ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; // @ts-ignore untyped module import { KIBANA_STATS_TYPE_MONITORING } from '../../../monitoring/common/constants'; -import { ServerFacade, ESCallCluster } from '../../types'; +import { ServerFacade, ExportTypesRegistry, ESCallCluster } from '../../types'; import { KIBANA_REPORTING_TYPE } from '../../common/constants'; import { getReportingUsage } from './get_reporting_usage'; import { RangeStats } from './types'; @@ -19,12 +19,14 @@ import { RangeStats } from './types'; export function getReportingUsageCollector( usageCollection: UsageCollectionSetup, server: ServerFacade, - isReady: () => boolean + isReady: () => boolean, + exportTypesRegistry: ExportTypesRegistry ) { return usageCollection.makeUsageCollector({ type: KIBANA_REPORTING_TYPE, isReady, - fetch: (callCluster: ESCallCluster) => getReportingUsage(server, callCluster), + fetch: (callCluster: ESCallCluster) => + getReportingUsage(server, callCluster, exportTypesRegistry), /* * Format the response data into a model for internal upload @@ -49,8 +51,14 @@ export function getReportingUsageCollector( export function registerReportingUsageCollector( usageCollection: UsageCollectionSetup, server: ServerFacade, - isReady: () => boolean + isReady: () => boolean, + exportTypesRegistry: ExportTypesRegistry ) { - const collector = getReportingUsageCollector(usageCollection, server, isReady); + const collector = getReportingUsageCollector( + usageCollection, + server, + isReady, + exportTypesRegistry + ); usageCollection.registerCollector(collector); } diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index f43caf92ffb15..28f64b8eefcbc 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -13,9 +13,12 @@ import { CallCluster, } from '../../../../src/legacy/core_plugins/elasticsearch'; import { CancellationToken } from './common/cancellation_token'; +import { LevelLogger } from './server/lib/level_logger'; import { HeadlessChromiumDriverFactory } from './server/browsers/chromium/driver_factory'; import { BrowserType } from './server/browsers/types'; +export type ReportingPlugin = object; // For Plugin contract + export type Job = EventEmitter & { id: string; toJSON: () => { @@ -23,21 +26,6 @@ export type Job = EventEmitter & { }; }; -export interface ReportingPlugin { - queue: { - addJob: (type: string, payload: PayloadType, options: object) => Job; - }; - // TODO: convert exportTypesRegistry to TS - exportTypesRegistry: { - getById: (id: string) => ExportTypeDefinition; - getAll: () => Array>; - get: ( - callback: (item: ExportTypeDefinition) => boolean - ) => ExportTypeDefinition; - }; - browserDriverFactory: HeadlessChromiumDriverFactory; -} - export interface ReportingConfigOptions { browser: BrowserConfig; poll: { @@ -88,7 +76,6 @@ export type ReportingPluginSpecOptions = Legacy.PluginSpecOptions; export type ServerFacade = Legacy.Server & { plugins: { - reporting?: ReportingPlugin; xpack_main?: XPackMainPlugin & { status?: any; }; @@ -107,6 +94,15 @@ interface ReportingRequest { }; } +export type EnqueueJobFn = ( + parentLogger: LevelLogger, + exportTypeId: string, + jobParams: JobParamsType, + user: string, + headers: Record, + request: RequestFacade +) => Promise; + export type RequestFacade = ReportingRequest & Legacy.Request; export type ResponseFacade = ResponseObject & { @@ -246,6 +242,10 @@ export interface JobDocOutputExecuted { size: number; } +export interface ESQueue { + addJob: (type: string, payload: object, options: object) => Job; +} + export interface ESQueueWorker { on: (event: string, handler: any) => void; } @@ -304,7 +304,12 @@ export interface ESQueueInstance { } export type CreateJobFactory = (server: ServerFacade) => CreateJobFnType; -export type ExecuteJobFactory = (server: ServerFacade) => ExecuteJobFnType; +export type ExecuteJobFactory = ( + server: ServerFacade, + opts: { + browserDriverFactory: HeadlessChromiumDriverFactory; + } +) => ExecuteJobFnType; export interface ExportTypeDefinition< JobParamsType, @@ -322,18 +327,16 @@ export interface ExportTypeDefinition< validLicenses: string[]; } -export interface ExportTypesRegistry { - register: ( - exportTypeDefinition: ExportTypeDefinition< - JobParamsType, - CreateJobFnType, - JobPayloadType, - ExecuteJobFnType - > - ) => void; -} - +export { ExportTypesRegistry } from './server/lib/export_types_registry'; +export { HeadlessChromiumDriver } from './server/browsers/chromium/driver'; +export { HeadlessChromiumDriverFactory } from './server/browsers/chromium/driver_factory'; export { CancellationToken } from './common/cancellation_token'; -// Prefer to import this type using: `import { LevelLogger } from 'relative/path/server/lib';` -export { LevelLogger as Logger } from './server/lib/level_logger'; +export { LevelLogger as Logger }; + +export interface AbsoluteURLFactoryOptions { + defaultBasePath: string; + protocol: string; + hostname: string; + port: string | number; +} From 04a49fdce4973ca02e86f81e1025bb90d138bf7f Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Thu, 12 Dec 2019 12:47:50 -0500 Subject: [PATCH 09/36] [Lens] Fix sort crash (#52694) (#52909) --- .../indexpattern_plugin/state_helpers.test.ts | 15 +++++++++++---- .../public/indexpattern_plugin/state_helpers.ts | 9 ++++----- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts index ad3d3f3816262..28486c8201da0 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts @@ -32,8 +32,8 @@ describe('state_helpers', () => { operationType: 'terms', sourceField: 'source', params: { - orderBy: { type: 'alphabetical' }, - orderDirection: 'asc', + orderBy: { type: 'column', columnId: 'col2' }, + orderDirection: 'desc', size: 5, }, }; @@ -65,11 +65,18 @@ describe('state_helpers', () => { expect( deleteColumn({ state, columnId: 'col2', layerId: 'first' }).layers.first.columns ).toEqual({ - col1: termsColumn, + col1: { + ...termsColumn, + params: { + ...termsColumn.params, + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + }, + }, }); }); - it('should execute adjustments for other columns', () => { + it('should adjust when deleting other columns', () => { const termsColumn: TermsIndexPatternColumn = { label: 'Top values of source', dataType: 'string', diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts index db44d73a00337..f56f8089ea586 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts @@ -124,11 +124,10 @@ export function deleteColumn({ layerId: string; columnId: string; }): IndexPatternPrivateState { - const newColumns = adjustColumnReferencesForChangedColumn( - state.layers[layerId].columns, - columnId - ); - delete newColumns[columnId]; + const hypotheticalColumns = { ...state.layers[layerId].columns }; + delete hypotheticalColumns[columnId]; + + const newColumns = adjustColumnReferencesForChangedColumn(hypotheticalColumns, columnId); return { ...state, From 064968eb7cdded01405662fb59fea0f7e1b9cd2e Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 12 Dec 2019 11:13:06 -0700 Subject: [PATCH 10/36] skip flaky suite (#36011) (cherry picked from commit 065ca6e0417912bfea4761845bfc3215374d091b) --- x-pack/test/functional/apps/maps/layer_errors.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/maps/layer_errors.js b/x-pack/test/functional/apps/maps/layer_errors.js index 142ea7c4bf025..36a6e48eb88ef 100644 --- a/x-pack/test/functional/apps/maps/layer_errors.js +++ b/x-pack/test/functional/apps/maps/layer_errors.js @@ -65,7 +65,8 @@ export default function ({ getPageObjects }) { }); }); - describe('EMSFileSource with missing EMS id', () => { + // FLAKY: https://github.com/elastic/kibana/issues/36011 + describe.skip('EMSFileSource with missing EMS id', () => { const MISSING_EMS_ID = 'idThatDoesNotExitForEMSFileSource'; const LAYER_NAME = 'EMS_vector_shapes'; From a0a749e270db88513a026b54b8c8e067a5094168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Thu, 12 Dec 2019 20:41:39 +0100 Subject: [PATCH 11/36] [7.x] [Logs + Metrics UI] Remove eslint exceptions (#50979) (#52933) Backports the following commits to 7.x: - [Logs + Metrics UI] Remove eslint exceptions (#50979) --- .eslintrc.js | 7 -- .../public/components/formatted_time.tsx | 1 - .../log_entry_actions_menu.tsx | 2 +- .../logging/log_highlights_menu.tsx | 24 +++++-- .../logging/log_text_stream/text_styles.tsx | 2 +- .../components/metrics_explorer/metrics.tsx | 33 ++++----- .../components/saved_views/create_modal.tsx | 2 +- .../add_log_column_popover.tsx | 2 +- .../source_configuration_form_state.tsx | 2 +- .../waffle/waffle_inventory_switcher.tsx | 27 ++++--- .../log_analysis_capabilities.tsx | 2 +- .../logs/log_analysis/log_analysis_module.tsx | 10 +-- .../log_analysis/log_analysis_setup_state.tsx | 2 +- .../log_highlights/log_entry_highlights.tsx | 2 +- .../log_highlights/log_summary_highlights.ts | 10 ++- .../logs/log_highlights/next_and_previous.tsx | 2 +- .../logs/log_highlights/redux_bridges.tsx | 6 +- .../containers/logs/with_stream_items.ts | 2 +- .../use_metrics_explorer_data.ts | 3 + .../use_metrics_explorer_options.ts | 2 +- .../infra/public/hooks/use_saved_view.ts | 61 ++++++++-------- .../infra/public/hooks/use_track_metric.tsx | 3 + .../public/pages/infrastructure/index.tsx | 15 ++-- .../infrastructure/metrics_explorer/index.tsx | 7 +- .../use_metric_explorer_state.ts | 12 ++-- .../logs/log_entry_rate/page_content.tsx | 2 +- .../sections/anomalies/table.tsx | 2 +- .../analysis_setup_indices_form.tsx | 2 +- .../use_log_entry_rate_module.tsx | 2 +- .../use_log_entry_rate_results_url_state.tsx | 11 +-- .../metrics/components/chart_section_vis.tsx | 18 ++--- .../metrics/components/node_details_page.tsx | 10 +-- .../pages/metrics/components/section.tsx | 71 ++++++++++--------- .../pages/metrics/components/sub_section.tsx | 36 +++++----- .../metrics/containers/with_metrics_time.tsx | 11 ++- .../infra/public/pages/metrics/index.tsx | 42 +++++------ .../infra/public/utils/cancellable_effect.ts | 3 + .../public/utils/use_kibana_ui_setting.ts | 7 +- .../infra/public/utils/use_tracked_promise.ts | 2 + .../infra/public/utils/use_url_state.ts | 38 +++++++--- .../public/utils/use_visibility_state.ts | 2 +- 41 files changed, 269 insertions(+), 231 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index e01632815bc68..367ac892107ab 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -170,13 +170,6 @@ module.exports = { 'react-hooks/rules-of-hooks': 'off', }, }, - { - files: ['x-pack/legacy/plugins/infra/**/*.{js,ts,tsx}'], - rules: { - 'react-hooks/exhaustive-deps': 'off', - 'react-hooks/rules-of-hooks': 'off', - }, - }, { files: ['x-pack/legacy/plugins/lens/**/*.{js,ts,tsx}'], rules: { diff --git a/x-pack/legacy/plugins/infra/public/components/formatted_time.tsx b/x-pack/legacy/plugins/infra/public/components/formatted_time.tsx index 78255c55df124..46b505d4fab52 100644 --- a/x-pack/legacy/plugins/infra/public/components/formatted_time.tsx +++ b/x-pack/legacy/plugins/infra/public/components/formatted_time.tsx @@ -37,7 +37,6 @@ export const useFormattedTime = ( const dateFormat = formatMap[format]; const formattedTime = useMemo(() => getFormattedTime(time, dateFormat, fallbackFormat), [ - getFormattedTime, time, dateFormat, fallbackFormat, diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx index 92c6ddd193609..d018b3a0f38ff 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx @@ -51,7 +51,7 @@ export const LogEntryActionsMenu: React.FunctionComponent<{ /> , ], - [uptimeLink] + [apmLink, uptimeLink] ); const hasMenuItems = useMemo(() => menuItems.length > 0, [menuItems]); diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_highlights_menu.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_highlights_menu.tsx index 24a5e8bacb4f9..d13ccde7466cd 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_highlights_menu.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_highlights_menu.tsx @@ -16,7 +16,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { debounce } from 'lodash'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import euiStyled from '../../../../../common/eui_styled_components'; import { useVisibilityState } from '../../utils/use_visibility_state'; @@ -47,8 +47,25 @@ export const LogHighlightsMenu: React.FC = ({ } = useVisibilityState(false); // Input field state - const [highlightTerm, setHighlightTerm] = useState(''); + const [highlightTerm, _setHighlightTerm] = useState(''); + const debouncedOnChange = useMemo(() => debounce(onChange, 275), [onChange]); + const setHighlightTerm = useCallback( + valueOrUpdater => + _setHighlightTerm(previousHighlightTerm => { + const newHighlightTerm = + typeof valueOrUpdater === 'function' + ? valueOrUpdater(previousHighlightTerm) + : valueOrUpdater; + + if (newHighlightTerm !== previousHighlightTerm) { + debouncedOnChange([newHighlightTerm]); + } + + return newHighlightTerm; + }), + [debouncedOnChange] + ); const changeHighlightTerm = useCallback( e => { const value = e.target.value; @@ -57,9 +74,6 @@ export const LogHighlightsMenu: React.FC = ({ [setHighlightTerm] ); const clearHighlightTerm = useCallback(() => setHighlightTerm(''), [setHighlightTerm]); - useEffect(() => { - debouncedOnChange([highlightTerm]); - }, [highlightTerm]); const button = ( diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx index 1d40c88f5d1d0..e95ac6aa7923b 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx @@ -63,7 +63,7 @@ export const useMeasuredCharacterDimensions = (scale: TextScale) => { X ), - [scale] + [measureElement, scale] ); return { diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx index d59e709d9a19a..42df7c6915a0d 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx @@ -7,7 +7,7 @@ import { EuiComboBox } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useCallback, useState, useEffect } from 'react'; +import React, { useCallback, useState } from 'react'; import { FieldType } from 'ui/index_patterns'; import { colorTransformer, MetricsExplorerColor } from '../../../common/color_palette'; import { @@ -31,24 +31,19 @@ interface SelectedOption { export const MetricsExplorerMetrics = ({ options, onChange, fields, autoFocus = false }: Props) => { const colors = Object.keys(MetricsExplorerColor) as MetricsExplorerColor[]; - const [inputRef, setInputRef] = useState(null); - const [focusOnce, setFocusState] = useState(false); + const [shouldFocus, setShouldFocus] = useState(autoFocus); - useEffect(() => { - if (inputRef && autoFocus && !focusOnce) { - inputRef.focus(); - setFocusState(true); - } - }, [inputRef]); + // the EuiCombobox forwards the ref to an input element + const autoFocusInputElement = useCallback( + (inputElement: HTMLInputElement | null) => { + if (inputElement && shouldFocus) { + inputElement.focus(); + setShouldFocus(false); + } + }, + [shouldFocus] + ); - // I tried to use useRef originally but the EUIComboBox component's type definition - // would only accept an actual input element or a callback function (with the same type). - // This effectivly does the same thing but is compatible with EuiComboBox. - const handleInputRef = (ref: HTMLInputElement) => { - if (ref) { - setInputRef(ref); - } - }; const handleChange = useCallback( selectedOptions => { onChange( @@ -59,7 +54,7 @@ export const MetricsExplorerMetrics = ({ options, onChange, fields, autoFocus = })) ); }, - [options, onChange] + [onChange, options.aggregation, colors] ); const comboOptions = fields @@ -86,7 +81,7 @@ export const MetricsExplorerMetrics = ({ options, onChange, fields, autoFocus = selectedOptions={selectedOptions} onChange={handleChange} isClearable={true} - inputRef={handleInputRef} + inputRef={autoFocusInputElement} /> ); }; diff --git a/x-pack/legacy/plugins/infra/public/components/saved_views/create_modal.tsx b/x-pack/legacy/plugins/infra/public/components/saved_views/create_modal.tsx index 8df479f36e2f9..9b8907a1ff9e1 100644 --- a/x-pack/legacy/plugins/infra/public/components/saved_views/create_modal.tsx +++ b/x-pack/legacy/plugins/infra/public/components/saved_views/create_modal.tsx @@ -36,7 +36,7 @@ export const SavedViewCreateModal = ({ close, save, isInvalid }: Props) => { const saveView = useCallback(() => { save(viewName, includeTime); - }, [viewName, includeTime]); + }, [includeTime, save, viewName]); return ( diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/add_log_column_popover.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/add_log_column_popover.tsx index 9b83f62e7856b..fc8407c5298e6 100644 --- a/x-pack/legacy/plugins/infra/public/components/source_configuration/add_log_column_popover.tsx +++ b/x-pack/legacy/plugins/infra/public/components/source_configuration/add_log_column_popover.tsx @@ -94,7 +94,7 @@ export const AddLogColumnButtonAndPopover: React.FunctionComponent<{ addLogColumn(selectedOption.columnConfiguration); }, - [addLogColumn, availableColumnOptions] + [addLogColumn, availableColumnOptions, closePopover] ); return ( diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx index 3614a88c1e99e..262649e20709b 100644 --- a/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx +++ b/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx @@ -52,7 +52,7 @@ export const useSourceConfigurationFormState = (configuration?: SourceConfigurat const resetForm = useCallback(() => { indicesConfigurationFormState.resetForm(); logColumnsConfigurationFormState.resetForm(); - }, [indicesConfigurationFormState.resetForm, logColumnsConfigurationFormState.formState]); + }, [indicesConfigurationFormState, logColumnsConfigurationFormState]); const isFormDirty = useMemo( () => indicesConfigurationFormState.isFormDirty || logColumnsConfigurationFormState.isFormDirty, diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/waffle_inventory_switcher.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/waffle_inventory_switcher.tsx index 38e87038b7c4f..c8f03cef4d6ac 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/waffle_inventory_switcher.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/waffle_inventory_switcher.tsx @@ -17,28 +17,33 @@ import { } from '../../graphql/types'; import { findInventoryModel } from '../../../common/inventory_models'; -interface Props { +interface WaffleInventorySwitcherProps { nodeType: InfraNodeType; changeNodeType: (nodeType: InfraNodeType) => void; changeGroupBy: (groupBy: InfraSnapshotGroupbyInput[]) => void; changeMetric: (metric: InfraSnapshotMetricInput) => void; } -export const WaffleInventorySwitcher = (props: Props) => { +export const WaffleInventorySwitcher: React.FC = ({ + changeNodeType, + changeGroupBy, + changeMetric, + nodeType, +}) => { const [isOpen, setIsOpen] = useState(false); const closePopover = useCallback(() => setIsOpen(false), []); const openPopover = useCallback(() => setIsOpen(true), []); const goToNodeType = useCallback( - (nodeType: InfraNodeType) => { + (targetNodeType: InfraNodeType) => { closePopover(); - props.changeNodeType(nodeType); - props.changeGroupBy([]); - const inventoryModel = findInventoryModel(nodeType); - props.changeMetric({ + changeNodeType(targetNodeType); + changeGroupBy([]); + const inventoryModel = findInventoryModel(targetNodeType); + changeMetric({ type: inventoryModel.metrics.defaultSnapshot as InfraSnapshotMetricType, }); }, - [props.changeGroupBy, props.changeNodeType, props.changeMetric] + [closePopover, changeNodeType, changeGroupBy, changeMetric] ); const goToHost = useCallback(() => goToNodeType('host' as InfraNodeType), [goToNodeType]); const goToK8 = useCallback(() => goToNodeType('pod' as InfraNodeType), [goToNodeType]); @@ -68,10 +73,10 @@ export const WaffleInventorySwitcher = (props: Props) => { ], }, ], - [] + [goToDocker, goToHost, goToK8] ); const selectedText = useMemo(() => { - switch (props.nodeType) { + switch (nodeType) { case InfraNodeType.host: return i18n.translate('xpack.infra.waffle.nodeTypeSwitcher.hostsLabel', { defaultMessage: 'Hosts', @@ -81,7 +86,7 @@ export const WaffleInventorySwitcher = (props: Props) => { case InfraNodeType.container: return 'Docker'; } - }, [props.nodeType]); + }, [nodeType]); return ( diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx index 35a3ac737ada3..bb01043b0db6e 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx @@ -46,7 +46,7 @@ export const useLogAnalysisCapabilities = () => { useEffect(() => { fetchMlCapabilities(); - }, []); + }, [fetchMlCapabilities]); const isLoading = useMemo(() => fetchMlCapabilitiesRequest.state === 'pending', [ fetchMlCapabilitiesRequest.state, diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx index 189b58d7923f8..d7d0ecb6f2c8d 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx @@ -125,23 +125,23 @@ export const useLogAnalysisModule = ({ dispatchModuleStatus({ type: 'failedSetup' }); }); }, - [cleanUpModule, setUpModule] + [cleanUpModule, dispatchModuleStatus, setUpModule] ); const viewSetupForReconfiguration = useCallback(() => { dispatchModuleStatus({ type: 'requestedJobConfigurationUpdate' }); - }, []); + }, [dispatchModuleStatus]); const viewSetupForUpdate = useCallback(() => { dispatchModuleStatus({ type: 'requestedJobDefinitionUpdate' }); - }, []); + }, [dispatchModuleStatus]); const viewResults = useCallback(() => { dispatchModuleStatus({ type: 'viewedResults' }); - }, []); + }, [dispatchModuleStatus]); const jobIds = useMemo(() => moduleDescriptor.getJobIds(spaceId, sourceId), [ - moduleDescriptor.getJobIds, + moduleDescriptor, spaceId, sourceId, ]); diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx index 275c0194be3b2..74dbb3c7a8062 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx @@ -140,7 +140,7 @@ export const useAnalysisSetupState = ({ ? [...errors, ...index.errors] : errors; }, []); - }, [selectedIndexNames, validatedIndices, validateIndicesRequest.state]); + }, [isValidating, validateIndicesRequest.state, selectedIndexNames, validatedIndices]); return { cleanupAndSetup, diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_entry_highlights.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_entry_highlights.tsx index 6ead866fb960a..2b19958a9b1a1 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_entry_highlights.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_entry_highlights.tsx @@ -78,7 +78,7 @@ export const useLogEntryHighlights = ( } else { setLogEntryHighlights([]); } - }, [highlightTerms, startKey, endKey, filterQuery, sourceVersion]); + }, [endKey, filterQuery, highlightTerms, loadLogEntryHighlights, sourceVersion, startKey]); const logEntryHighlightsById = useMemo( () => diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_summary_highlights.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_summary_highlights.ts index 34c66afda010e..874c70e016496 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_summary_highlights.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_summary_highlights.ts @@ -74,7 +74,15 @@ export const useLogSummaryHighlights = ( } else { setLogSummaryHighlights([]); } - }, [highlightTerms, start, end, bucketSize, filterQuery, sourceVersion]); + }, [ + bucketSize, + debouncedLoadSummaryHighlights, + end, + filterQuery, + highlightTerms, + sourceVersion, + start, + ]); return { logSummaryHighlights, diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/next_and_previous.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/next_and_previous.tsx index 95ead50119eb4..62a43a5412825 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/next_and_previous.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/next_and_previous.tsx @@ -53,7 +53,7 @@ export const useNextAndPrevious = ({ const initialTimeKey = getUniqueLogEntryKey(entries[initialIndex]); setCurrentTimeKey(initialTimeKey); } - }, [currentTimeKey, entries, setCurrentTimeKey]); + }, [currentTimeKey, entries, setCurrentTimeKey, visibleMidpoint]); const indexOfCurrentTimeKey = useMemo(() => { if (currentTimeKey && entries.length > 0) { diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/redux_bridges.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/redux_bridges.tsx index 2b60c6edd97aa..9ea8987d4f326 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/redux_bridges.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/redux_bridges.tsx @@ -25,11 +25,11 @@ export const LogHighlightsPositionBridge = withLogPosition( const { setJumpToTarget, setVisibleMidpoint } = useContext(LogHighlightsState.Context); useEffect(() => { setVisibleMidpoint(visibleMidpoint); - }, [visibleMidpoint]); + }, [setVisibleMidpoint, visibleMidpoint]); useEffect(() => { setJumpToTarget(() => jumpToTargetPosition); - }, [jumpToTargetPosition]); + }, [jumpToTargetPosition, setJumpToTarget]); return null; } @@ -41,7 +41,7 @@ export const LogHighlightsFilterQueryBridge = withLogFilter( useEffect(() => { setFilterQuery(serializedFilterQuery); - }, [serializedFilterQuery]); + }, [serializedFilterQuery, setFilterQuery]); return null; } diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/with_stream_items.ts b/x-pack/legacy/plugins/infra/public/containers/logs/with_stream_items.ts index da468b4391e4e..9b20676486af2 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/with_stream_items.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/with_stream_items.ts @@ -35,7 +35,7 @@ export const WithStreamItems: React.FunctionComponent<{ createLogEntryStreamItem(logEntry, logEntryHighlightsById[logEntry.gid] || []) ), - [logEntries.entries, logEntryHighlightsById] + [isAutoReloading, logEntries.entries, logEntries.isReloading, logEntryHighlightsById] ); return children({ diff --git a/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.ts b/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.ts index 1418d6aef67ac..c2a599ea1ae78 100644 --- a/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.ts +++ b/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.ts @@ -96,6 +96,9 @@ export function useMetricsExplorerData( } setLoading(false); })(); + + // TODO: fix this dependency list while preserving the semantics + // eslint-disable-next-line react-hooks/exhaustive-deps }, [options, source, timerange, signal, afterKey]); return { error, loading, data }; } diff --git a/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.ts b/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.ts index 278f3e0a9c17d..de7a8d5805ecc 100644 --- a/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.ts +++ b/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.ts @@ -102,7 +102,7 @@ function useStateWithLocalStorage( const [state, setState] = useState(parseJsonOrDefault(storageState, defaultState)); useEffect(() => { localStorage.setItem(key, JSON.stringify(state)); - }, [state]); + }, [key, state]); return [state, setState]; } diff --git a/x-pack/legacy/plugins/infra/public/hooks/use_saved_view.ts b/x-pack/legacy/plugins/infra/public/hooks/use_saved_view.ts index 8db0ed28d9b21..4b12b6c51ea0e 100644 --- a/x-pack/legacy/plugins/infra/public/hooks/use_saved_view.ts +++ b/x-pack/legacy/plugins/infra/public/hooks/use_saved_view.ts @@ -26,29 +26,32 @@ export const useSavedView = (defaultViewState: ViewState, viewType: s >(viewType); const { create, error: errorOnCreate, createdId } = useCreateSavedObject(viewType); const { deleteObject, deletedId } = useDeleteSavedObject(viewType); - const deleteView = useCallback((id: string) => deleteObject(id), []); + const deleteView = useCallback((id: string) => deleteObject(id), [deleteObject]); const [createError, setCreateError] = useState(null); - useEffect(() => setCreateError(createError), [errorOnCreate, setCreateError]); + useEffect(() => setCreateError(errorOnCreate), [errorOnCreate]); - const saveView = useCallback((d: { [p: string]: any }) => { - const doSave = async () => { - const exists = await hasView(d.name); - if (exists) { - setCreateError( - i18n.translate('xpack.infra.savedView.errorOnCreate.duplicateViewName', { - defaultMessage: `A view with that name already exists.`, - }) - ); - return; - } - create(d); - }; - setCreateError(null); - doSave(); - }, []); + const saveView = useCallback( + (d: { [p: string]: any }) => { + const doSave = async () => { + const exists = await hasView(d.name); + if (exists) { + setCreateError( + i18n.translate('xpack.infra.savedView.errorOnCreate.duplicateViewName', { + defaultMessage: `A view with that name already exists.`, + }) + ); + return; + } + create(d); + }; + setCreateError(null); + doSave(); + }, + [create, hasView] + ); - const savedObjects = data ? data.savedObjects : []; + const savedObjects = useMemo(() => (data ? data.savedObjects : []), [data]); const views = useMemo(() => { const items: Array> = [ { @@ -61,19 +64,17 @@ export const useSavedView = (defaultViewState: ViewState, viewType: s }, ]; - if (data) { - data.savedObjects.forEach( - o => - o.type === viewType && - items.push({ - ...o.attributes, - id: o.id, - }) - ); - } + savedObjects.forEach( + o => + o.type === viewType && + items.push({ + ...o.attributes, + id: o.id, + }) + ); return items; - }, [savedObjects, defaultViewState]); + }, [defaultViewState, savedObjects, viewType]); return { views, diff --git a/x-pack/legacy/plugins/infra/public/hooks/use_track_metric.tsx b/x-pack/legacy/plugins/infra/public/hooks/use_track_metric.tsx index 379b3af3f1063..c5945ab808202 100644 --- a/x-pack/legacy/plugins/infra/public/hooks/use_track_metric.tsx +++ b/x-pack/legacy/plugins/infra/public/hooks/use_track_metric.tsx @@ -57,6 +57,9 @@ export function useTrackMetric( const trackUiMetric = getTrackerForApp(app); const id = setTimeout(() => trackUiMetric(metricType, decoratedMetric), Math.max(delay, 0)); return () => clearTimeout(id); + + // the dependencies are managed externally + // eslint-disable-next-line react-hooks/exhaustive-deps }, effectDependencies); } diff --git a/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx b/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx index fe48fcc62f77d..9efbbe790abc1 100644 --- a/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx @@ -24,6 +24,7 @@ import { MetricsExplorerPage } from './metrics_explorer'; import { SnapshotPage } from './snapshot'; import { SettingsPage } from '../shared/settings'; import { AppNavigation } from '../../components/navigation/app_navigation'; +import { SourceLoadingPage } from '../../components/source_loading_page'; interface InfrastructurePageProps extends RouteComponentProps { uiCapabilities: UICapabilities; @@ -95,11 +96,15 @@ export const InfrastructurePage = injectUICapabilities( {({ configuration, createDerivedIndexPattern }) => ( - + {configuration ? ( + + ) : ( + + )} )} diff --git a/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx b/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx index 63f5a81967618..4db4319b91d3c 100644 --- a/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx @@ -11,22 +11,17 @@ import { IIndexPattern } from 'src/plugins/data/public'; import { DocumentTitle } from '../../../components/document_title'; import { MetricsExplorerCharts } from '../../../components/metrics_explorer/charts'; import { MetricsExplorerToolbar } from '../../../components/metrics_explorer/toolbar'; -import { SourceLoadingPage } from '../../../components/source_loading_page'; import { SourceQuery } from '../../../../common/graphql/types'; import { NoData } from '../../../components/empty_states'; import { useMetricsExplorerState } from './use_metric_explorer_state'; import { useTrackPageview } from '../../../hooks/use_track_metric'; interface MetricsExplorerPageProps { - source: SourceQuery.Query['source']['configuration'] | undefined; + source: SourceQuery.Query['source']['configuration']; derivedIndexPattern: IIndexPattern; } export const MetricsExplorerPage = ({ source, derivedIndexPattern }: MetricsExplorerPageProps) => { - if (!source) { - return ; - } - const { loading, error, diff --git a/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.ts b/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.ts index 415a6ae89a8b1..57ea886169701 100644 --- a/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.ts +++ b/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.ts @@ -59,7 +59,7 @@ export const useMetricsExplorerState = ( setAfterKey(null); setTimeRange({ ...currentTimerange, from: start, to: end }); }, - [currentTimerange] + [currentTimerange, setTimeRange] ); const handleGroupByChange = useCallback( @@ -70,7 +70,7 @@ export const useMetricsExplorerState = ( groupBy: groupBy || void 0, }); }, - [options] + [options, setOptions] ); const handleFilterQuerySubmit = useCallback( @@ -81,7 +81,7 @@ export const useMetricsExplorerState = ( filterQuery: query, }); }, - [options] + [options, setOptions] ); const handleMetricsChange = useCallback( @@ -92,7 +92,7 @@ export const useMetricsExplorerState = ( metrics, }); }, - [options] + [options, setOptions] ); const handleAggregationChange = useCallback( @@ -109,7 +109,7 @@ export const useMetricsExplorerState = ( })); setOptions({ ...options, aggregation, metrics }); }, - [options] + [options, setOptions] ); const onViewStateChange = useCallback( @@ -124,7 +124,7 @@ export const useMetricsExplorerState = ( setOptions(vs.options); } }, - [setChartOptions, setTimeRange, setTimeRange] + [setChartOptions, setOptions, setTimeRange] ); return { diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx index e62164cb17b2c..e71985f73fbb8 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx @@ -36,7 +36,7 @@ export const LogEntryRatePageContent = () => { useEffect(() => { fetchModuleDefinition(); fetchJobStatus(); - }, []); + }, [fetchJobStatus, fetchModuleDefinition]); if (!hasLogAnalysisCapabilites) { return ; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx index 2057d75f72354..86760cf2da7d6 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx @@ -124,7 +124,7 @@ export const AnomaliesTable: React.FunctionComponent<{ setItemIdToExpandedRowMap(newItemIdToExpandedRowMap); } }, - [results, setTimeRange, timeRange, itemIdToExpandedRowMap, setItemIdToExpandedRowMap] + [itemIdToExpandedRowMap, jobId, results, setTimeRange, timeRange] ); const columns = [ diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/analysis_setup_indices_form.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/analysis_setup_indices_form.tsx index 35cad040323a6..5a4c21670191e 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/analysis_setup_indices_form.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/analysis_setup_indices_form.tsx @@ -54,7 +54,7 @@ export const AnalysisSetupIndicesForm: React.FunctionComponent<{ ); }), - [indices] + [handleCheckboxChange, indices] ); return ( diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_module.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_module.tsx index ab6a6578601bf..d1efedb176aba 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_module.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_module.tsx @@ -31,7 +31,7 @@ export const useLogEntryRateModule = ({ spaceId, timestampField, }), - [indexPattern] + [indexPattern, sourceId, spaceId, timestampField] ); return useLogAnalysisModule({ diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx index 017be6be49e16..6d4495c8d9e0f 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx @@ -8,7 +8,6 @@ import { fold } from 'fp-ts/lib/Either'; import { constant, identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import * as rt from 'io-ts'; -import { useEffect } from 'react'; import { useUrlState } from '../../../utils/use_url_state'; @@ -41,12 +40,9 @@ export const useLogAnalysisResultsUrlState = () => { pipe(urlTimeRangeRT.decode(value), fold(constant(undefined), identity)), encodeUrlState: urlTimeRangeRT.encode, urlStateKey: TIME_RANGE_URL_STATE_KEY, + writeDefaultState: true, }); - useEffect(() => { - setTimeRange(timeRange); - }, []); - const [autoRefresh, setAutoRefresh] = useUrlState({ defaultState: { isPaused: false, @@ -56,12 +52,9 @@ export const useLogAnalysisResultsUrlState = () => { pipe(autoRefreshRT.decode(value), fold(constant(undefined), identity)), encodeUrlState: autoRefreshRT.encode, urlStateKey: AUTOREFRESH_URL_STATE_KEY, + writeDefaultState: true, }); - useEffect(() => { - setAutoRefresh(autoRefresh); - }, []); - return { timeRange, setTimeRange, diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/components/chart_section_vis.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/components/chart_section_vis.tsx index 425b5a43f793f..309961cc39025 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/components/chart_section_vis.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/components/chart_section_vis.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { @@ -42,15 +42,15 @@ export const ChartSectionVis = ({ seriesOverrides, type, }: VisSectionProps) => { - if (!metric || !id) { - return null; - } const [dateFormat] = useKibanaUiSetting('dateFormat'); const valueFormatter = useCallback(getFormatter(formatter, formatterTemplate), [ formatter, formatterTemplate, ]); - const dateFormatter = useCallback(niceTimeFormatter(getMaxMinTimestamp(metric)), [metric]); + const dateFormatter = useMemo( + () => (metric != null ? niceTimeFormatter(getMaxMinTimestamp(metric)) : undefined), + [metric] + ); const handleTimeChange = useCallback( (from: number, to: number) => { if (onChangeRangeTime) { @@ -73,7 +73,9 @@ export const ChartSectionVis = ({ ), }; - if (!metric) { + if (!id) { + return null; + } else if (!metric) { return ( ); - } - - if (metric.series.some(seriesHasLessThen2DataPoints)) { + } else if (metric.series.some(seriesHasLessThen2DataPoints)) { return ( { - if (!props.metadata) { - return null; - } - const { parsedTimeRange } = props; const { metrics, loading, makeRequest, error } = useNodeDetails( props.requiredMetrics, @@ -65,11 +61,11 @@ export const NodeDetailsPage = (props: Props) => { const refetch = useCallback(() => { makeRequest(); - }, []); + }, [makeRequest]); useEffect(() => { makeRequest(); - }, [parsedTimeRange]); + }, [makeRequest, parsedTimeRange]); if (error) { return ; diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/components/section.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/components/section.tsx index 32d2e2eff8ab9..2f9ed9f54df82 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/components/section.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/components/section.tsx @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiTitle } from '@elastic/eui'; import React, { - useContext, Children, - isValidElement, cloneElement, FunctionComponent, - useMemo, + isValidElement, + useContext, } from 'react'; -import { EuiTitle } from '@elastic/eui'; + import { SideNavContext, SubNavItem } from '../lib/side_nav_context'; import { LayoutProps } from '../types'; @@ -31,35 +31,42 @@ export const Section: FunctionComponent = ({ stopLiveStreaming, }) => { const { addNavItem } = useContext(SideNavContext); - const subNavItems: SubNavItem[] = []; - const childrenWithProps = useMemo( - () => - Children.map(children, child => { - if (isValidElement(child)) { - const metric = (metrics && metrics.find(m => m.id === child.props.id)) || null; - if (metric) { - subNavItems.push({ - id: child.props.id, - name: child.props.label, - onClick: () => { - const el = document.getElementById(child.props.id); - if (el) { - el.scrollIntoView(); - } - }, - }); - } - return cloneElement(child, { - metrics, - onChangeRangeTime, - isLiveStreaming, - stopLiveStreaming, - }); - } - return null; - }), - [children, metrics, onChangeRangeTime, isLiveStreaming, stopLiveStreaming] + const subNavItems = Children.toArray(children).reduce( + (accumulatedChildren, child) => { + if (!isValidElement(child)) { + return accumulatedChildren; + } + const metric = metrics?.find(m => m.id === child.props.id) ?? null; + if (metric === null) { + return accumulatedChildren; + } + return [ + ...accumulatedChildren, + { + id: child.props.id, + name: child.props.label, + onClick: () => { + const el = document.getElementById(child.props.id); + if (el) { + el.scrollIntoView(); + } + }, + }, + ]; + }, + [] + ); + + const childrenWithProps = Children.map(children, child => + isValidElement(child) + ? cloneElement(child, { + metrics, + onChangeRangeTime, + isLiveStreaming, + stopLiveStreaming, + }) + : null ); if (metrics && subNavItems.length) { diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/components/sub_section.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/components/sub_section.tsx index f3db3b1670199..325d510293135 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/components/sub_section.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/components/sub_section.tsx @@ -23,29 +23,25 @@ export const SubSection: FunctionComponent = ({ isLiveStreaming, stopLiveStreaming, }) => { - if (!children || !metrics) { + const metric = useMemo(() => metrics?.find(m => m.id === id), [id, metrics]); + + if (!children || !metric) { return null; } - const metric = metrics.find(m => m.id === id); - if (!metric) { + + const childrenWithProps = Children.map(children, child => { + if (isValidElement(child)) { + return cloneElement(child, { + metric, + id, + onChangeRangeTime, + isLiveStreaming, + stopLiveStreaming, + }); + } return null; - } - const childrenWithProps = useMemo( - () => - Children.map(children, child => { - if (isValidElement(child)) { - return cloneElement(child, { - metric, - id, - onChangeRangeTime, - isLiveStreaming, - stopLiveStreaming, - }); - } - return null; - }), - [children, metric, id, onChangeRangeTime, isLiveStreaming, stopLiveStreaming] - ); + }); + return (
{label ? ( diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/containers/with_metrics_time.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/containers/with_metrics_time.tsx index 432725b6f62b0..64d2ddb67139d 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/containers/with_metrics_time.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/containers/with_metrics_time.tsx @@ -59,13 +59,10 @@ export const useMetricsTime = () => { const [parsedTimeRange, setParsedTimeRange] = useState(parseRange(defaultRange)); - const updateTimeRange = useCallback( - (range: MetricsTimeInput) => { - setTimeRange(range); - setParsedTimeRange(parseRange(range)); - }, - [setParsedTimeRange] - ); + const updateTimeRange = useCallback((range: MetricsTimeInput) => { + setTimeRange(range); + setParsedTimeRange(parseRange(range)); + }, []); return { timeRange, diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx index 93253406aec2d..b330ad02f1022 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx @@ -112,26 +112,28 @@ export const MetricDetail = withMetricPageProviders( })} /> - + {metadata ? ( + + ) : null} )} diff --git a/x-pack/legacy/plugins/infra/public/utils/cancellable_effect.ts b/x-pack/legacy/plugins/infra/public/utils/cancellable_effect.ts index bb7d253ea1557..a986af07f0c9a 100644 --- a/x-pack/legacy/plugins/infra/public/utils/cancellable_effect.ts +++ b/x-pack/legacy/plugins/infra/public/utils/cancellable_effect.ts @@ -27,5 +27,8 @@ export const useCancellableEffect = ( effect(() => cancellationSignal.isCancelled); return cancellationSignal.cancel; + + // the dependencies are managed externally + // eslint-disable-next-line react-hooks/exhaustive-deps }, deps); }; diff --git a/x-pack/legacy/plugins/infra/public/utils/use_kibana_ui_setting.ts b/x-pack/legacy/plugins/infra/public/utils/use_kibana_ui_setting.ts index c48f95a6521cf..1b08fb4231243 100644 --- a/x-pack/legacy/plugins/infra/public/utils/use_kibana_ui_setting.ts +++ b/x-pack/legacy/plugins/infra/public/utils/use_kibana_ui_setting.ts @@ -28,10 +28,15 @@ import { useObservable } from './use_observable'; export const useKibanaUiSetting = (key: string, defaultValue?: any) => { const uiSettingsClient = npSetup.core.uiSettings; - const uiSetting$ = useMemo(() => uiSettingsClient.get$(key, defaultValue), [uiSettingsClient]); + const uiSetting$ = useMemo(() => uiSettingsClient.get$(key, defaultValue), [ + defaultValue, + key, + uiSettingsClient, + ]); const uiSetting = useObservable(uiSetting$); const setUiSetting = useCallback((value: any) => uiSettingsClient.set(key, value), [ + key, uiSettingsClient, ]); diff --git a/x-pack/legacy/plugins/infra/public/utils/use_tracked_promise.ts b/x-pack/legacy/plugins/infra/public/utils/use_tracked_promise.ts index 366caf0dfb156..c23bab7026aaa 100644 --- a/x-pack/legacy/plugins/infra/public/utils/use_tracked_promise.ts +++ b/x-pack/legacy/plugins/infra/public/utils/use_tracked_promise.ts @@ -190,6 +190,8 @@ export const useTrackedPromise = ( return newPendingPromise.promise; }, + // the dependencies are managed by the caller + // eslint-disable-next-line react-hooks/exhaustive-deps dependencies ); diff --git a/x-pack/legacy/plugins/infra/public/utils/use_url_state.ts b/x-pack/legacy/plugins/infra/public/utils/use_url_state.ts index d03a5aaa9d697..79a5d552bcd78 100644 --- a/x-pack/legacy/plugins/infra/public/utils/use_url_state.ts +++ b/x-pack/legacy/plugins/infra/public/utils/use_url_state.ts @@ -5,10 +5,10 @@ */ import { Location } from 'history'; -import { useMemo, useCallback } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { decode, encode, RisonValue } from 'rison-node'; - import { QueryString } from 'ui/utils/query_string'; + import { useHistory } from './history_context'; export const useUrlState = ({ @@ -16,21 +16,26 @@ export const useUrlState = ({ decodeUrlState, encodeUrlState, urlStateKey, + writeDefaultState = false, }: { defaultState: State; decodeUrlState: (value: RisonValue | undefined) => State | undefined; encodeUrlState: (value: State) => RisonValue | undefined; urlStateKey: string; + writeDefaultState?: boolean; }) => { const history = useHistory(); + // history.location is mutable so we can't reliably use useMemo + const queryString = history?.location ? getQueryStringFromLocation(history.location) : ''; + const urlStateString = useMemo(() => { - if (!history) { + if (!queryString) { return; } - return getParamFromQueryString(getQueryStringFromLocation(history.location), urlStateKey); - }, [history && history.location, urlStateKey]); + return getParamFromQueryString(queryString, urlStateKey); + }, [queryString, urlStateKey]); const decodedState = useMemo(() => decodeUrlState(decodeRisonUrlState(urlStateString)), [ decodeUrlState, @@ -44,27 +49,38 @@ export const useUrlState = ({ const setState = useCallback( (newState: State | undefined) => { - if (!history) { + if (!history || !history.location) { return; } - const location = history.location; + const currentLocation = history.location; const newLocation = replaceQueryStringInLocation( - location, + currentLocation, replaceStateKeyInQueryString( urlStateKey, typeof newState !== 'undefined' ? encodeUrlState(newState) : undefined - )(getQueryStringFromLocation(location)) + )(getQueryStringFromLocation(currentLocation)) ); - if (newLocation !== location) { + if (newLocation !== currentLocation) { history.replace(newLocation); } }, - [encodeUrlState, history, history && history.location, urlStateKey] + [encodeUrlState, history, urlStateKey] ); + const [shouldInitialize, setShouldInitialize] = useState( + writeDefaultState && typeof decodedState === 'undefined' + ); + + useEffect(() => { + if (shouldInitialize) { + setShouldInitialize(false); + setState(defaultState); + } + }, [shouldInitialize, setState, defaultState]); + return [state, setState] as [typeof state, typeof setState]; }; diff --git a/x-pack/legacy/plugins/infra/public/utils/use_visibility_state.ts b/x-pack/legacy/plugins/infra/public/utils/use_visibility_state.ts index 5763834b1cc2a..f4d8b572e4f7f 100644 --- a/x-pack/legacy/plugins/infra/public/utils/use_visibility_state.ts +++ b/x-pack/legacy/plugins/infra/public/utils/use_visibility_state.ts @@ -20,6 +20,6 @@ export const useVisibilityState = (initialState: boolean) => { show, toggle, }), - [isVisible, show, hide] + [hide, isVisible, show, toggle] ); }; From e447134243327d3d17015d057f900944378a8192 Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Thu, 12 Dec 2019 12:46:24 -0700 Subject: [PATCH 12/36] [Data Plugin]: Remove `export *` for common code from public/server index files (#52821) (#52894) --- .../data/common/es_query/es_query/index.ts | 2 - .../{utils => filters}/get_display_value.ts | 0 .../get_index_pattern_from_filter.test.ts | 0 .../get_index_pattern_from_filter.ts | 0 .../data/common/es_query/filters/index.ts | 7 +- src/plugins/data/common/es_query/index.ts | 3 +- .../common/es_query/kuery/functions/is.js | 2 +- .../common/es_query/kuery/functions/range.js | 2 +- ...et_time_zone_from_settings.ts => utils.ts} | 0 .../data/common/es_query/utils/index.ts | 22 ------ src/plugins/data/public/index.ts | 68 +++++++++++++++++-- .../apply_filter_popover_content.tsx | 4 +- .../ui/filter_bar/filter_editor/index.tsx | 4 +- .../data/public/ui/filter_bar/filter_item.tsx | 4 +- src/plugins/data/server/index.ts | 64 +++++++++++++++-- .../components/description_step/index.tsx | 3 +- 16 files changed, 136 insertions(+), 49 deletions(-) rename src/plugins/data/common/es_query/{utils => filters}/get_display_value.ts (100%) rename src/plugins/data/common/es_query/{utils => filters}/get_index_pattern_from_filter.test.ts (100%) rename src/plugins/data/common/es_query/{utils => filters}/get_index_pattern_from_filter.ts (100%) rename src/plugins/data/common/es_query/{utils/get_time_zone_from_settings.ts => utils.ts} (100%) delete mode 100644 src/plugins/data/common/es_query/utils/index.ts diff --git a/src/plugins/data/common/es_query/es_query/index.ts b/src/plugins/data/common/es_query/es_query/index.ts index 82cbc543e19db..39b10be4c75b3 100644 --- a/src/plugins/data/common/es_query/es_query/index.ts +++ b/src/plugins/data/common/es_query/es_query/index.ts @@ -20,7 +20,5 @@ export { buildEsQuery, EsQueryConfig } from './build_es_query'; export { buildQueryFromFilters } from './from_filters'; export { luceneStringToDsl } from './lucene_string_to_dsl'; -export { migrateFilter } from './migrate_filter'; export { decorateQuery } from './decorate_query'; -export { filterMatchesIndex } from './filter_matches_index'; export { getEsQueryConfig } from './get_es_query_config'; diff --git a/src/plugins/data/common/es_query/utils/get_display_value.ts b/src/plugins/data/common/es_query/filters/get_display_value.ts similarity index 100% rename from src/plugins/data/common/es_query/utils/get_display_value.ts rename to src/plugins/data/common/es_query/filters/get_display_value.ts diff --git a/src/plugins/data/common/es_query/utils/get_index_pattern_from_filter.test.ts b/src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.test.ts similarity index 100% rename from src/plugins/data/common/es_query/utils/get_index_pattern_from_filter.test.ts rename to src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.test.ts diff --git a/src/plugins/data/common/es_query/utils/get_index_pattern_from_filter.ts b/src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.ts similarity index 100% rename from src/plugins/data/common/es_query/utils/get_index_pattern_from_filter.ts rename to src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.ts diff --git a/src/plugins/data/common/es_query/filters/index.ts b/src/plugins/data/common/es_query/filters/index.ts index 403ff2b79b55f..990d588359442 100644 --- a/src/plugins/data/common/es_query/filters/index.ts +++ b/src/plugins/data/common/es_query/filters/index.ts @@ -21,13 +21,14 @@ import { omit, get } from 'lodash'; import { Filter } from './meta_filter'; export * from './build_filters'; -export * from './get_filter_params'; -export * from './get_filter_field'; - export * from './custom_filter'; export * from './exists_filter'; export * from './geo_bounding_box_filter'; export * from './geo_polygon_filter'; +export * from './get_display_value'; +export * from './get_filter_field'; +export * from './get_filter_params'; +export * from './get_index_pattern_from_filter'; export * from './match_all_filter'; export * from './meta_filter'; export * from './missing_filter'; diff --git a/src/plugins/data/common/es_query/index.ts b/src/plugins/data/common/es_query/index.ts index 937fe09903b6b..e585fda8aff80 100644 --- a/src/plugins/data/common/es_query/index.ts +++ b/src/plugins/data/common/es_query/index.ts @@ -19,6 +19,5 @@ import * as esQuery from './es_query'; import * as esFilters from './filters'; import * as esKuery from './kuery'; -import * as utils from './utils'; -export { esFilters, esQuery, utils, esKuery }; +export { esFilters, esQuery, esKuery }; diff --git a/src/plugins/data/common/es_query/kuery/functions/is.js b/src/plugins/data/common/es_query/kuery/functions/is.js index 4f2f298c4707d..120dd9352d9a4 100644 --- a/src/plugins/data/common/es_query/kuery/functions/is.js +++ b/src/plugins/data/common/es_query/kuery/functions/is.js @@ -20,7 +20,7 @@ import { get, isUndefined } from 'lodash'; import { getPhraseScript } from '../../filters'; import { getFields } from './utils/get_fields'; -import { getTimeZoneFromSettings } from '../../utils/get_time_zone_from_settings'; +import { getTimeZoneFromSettings } from '../../utils'; import { getFullFieldNameNode } from './utils/get_full_field_name_node'; import * as ast from '../ast'; diff --git a/src/plugins/data/common/es_query/kuery/functions/range.js b/src/plugins/data/common/es_query/kuery/functions/range.js index 80181cfc003f1..d5eba8e20253e 100644 --- a/src/plugins/data/common/es_query/kuery/functions/range.js +++ b/src/plugins/data/common/es_query/kuery/functions/range.js @@ -22,7 +22,7 @@ import { nodeTypes } from '../node_types'; import * as ast from '../ast'; import { getRangeScript } from '../../filters'; import { getFields } from './utils/get_fields'; -import { getTimeZoneFromSettings } from '../../utils/get_time_zone_from_settings'; +import { getTimeZoneFromSettings } from '../../utils'; import { getFullFieldNameNode } from './utils/get_full_field_name_node'; export function buildNodeParams(fieldName, params) { diff --git a/src/plugins/data/common/es_query/utils/get_time_zone_from_settings.ts b/src/plugins/data/common/es_query/utils.ts similarity index 100% rename from src/plugins/data/common/es_query/utils/get_time_zone_from_settings.ts rename to src/plugins/data/common/es_query/utils.ts diff --git a/src/plugins/data/common/es_query/utils/index.ts b/src/plugins/data/common/es_query/utils/index.ts deleted file mode 100644 index 79856c9e0267e..0000000000000 --- a/src/plugins/data/common/es_query/utils/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export * from './get_time_zone_from_settings'; -export * from './get_index_pattern_from_filter'; -export * from './get_display_value'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index e54278698a05a..967887764237d 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -22,19 +22,75 @@ export function plugin(initializerContext: PluginInitializerContext) { return new DataPublicPlugin(initializerContext); } -export * from '../common'; +/** + * Types to be shared externally + * @public + */ +export { IRequestTypesMap, IResponseTypesMap } from './search'; +export * from './types'; +export { + // field formats + ContentType, // only used in agg_type + FIELD_FORMAT_IDS, + IFieldFormat, + IFieldFormatId, + IFieldFormatType, + // index patterns + IIndexPattern, + IFieldType, + IFieldSubType, + // kbn field types + ES_FIELD_TYPES, + KBN_FIELD_TYPES, + // query + Query, + // timefilter + RefreshInterval, + TimeRange, +} from '../common'; +/** + * Static code to be shared externally + * @public + */ export * from './autocomplete_provider'; export * from './field_formats_provider'; export * from './index_patterns'; - -export * from './types'; - -export { IRequestTypesMap, IResponseTypesMap } from './search'; export * from './search'; export * from './query'; - export * from './ui'; +export { + // es query + esFilters, + esKuery, + esQuery, + // field formats + BoolFormat, + BytesFormat, + ColorFormat, + DateFormat, + DateNanosFormat, + DEFAULT_CONVERTER_COLOR, + DurationFormat, + FieldFormat, + getHighlightRequest, // only used in search source + IpFormat, + NumberFormat, + PercentFormat, + RelativeDateFormat, + SourceFormat, + StaticLookupFormat, + StringFormat, + TEXT_CONTEXT_TYPE, // only used in agg_types + TruncateFormat, + UrlFormat, + // index patterns + isFilterable, + // kbn field types + castEsToKbnFieldTypeName, + getKbnFieldType, + getKbnTypeNames, +} from '../common'; // Export plugin after all other imports import { DataPublicPlugin } from './plugin'; diff --git a/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx b/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx index affbb8acecb20..92582ef1d15c2 100644 --- a/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx +++ b/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx @@ -30,7 +30,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { mapAndFlattenFilters, esFilters, utils, IIndexPattern } from '../..'; +import { mapAndFlattenFilters, esFilters, IIndexPattern } from '../..'; import { FilterLabel } from '../filter_bar'; interface Props { @@ -56,7 +56,7 @@ export class ApplyFiltersPopoverContent extends Component { }; } private getLabel(filter: esFilters.Filter) { - const valueLabel = utils.getDisplayValueFromFilter(filter, this.props.indexPatterns); + const valueLabel = esFilters.getDisplayValueFromFilter(filter, this.props.indexPatterns); return ; } diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx index 12da4cbab02da..b058d231b8306 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx @@ -48,7 +48,7 @@ import { Operator } from './lib/filter_operators'; import { PhraseValueInput } from './phrase_value_input'; import { PhrasesValuesInput } from './phrases_values_input'; import { RangeValueInput } from './range_value_input'; -import { esFilters, utils, IIndexPattern, IFieldType } from '../../..'; +import { esFilters, IIndexPattern, IFieldType } from '../../..'; interface Props { filter: esFilters.Filter; @@ -371,7 +371,7 @@ class FilterEditorUI extends Component { } private getIndexPatternFromFilter() { - return utils.getIndexPatternFromFilter(this.props.filter, this.props.indexPatterns); + return esFilters.getIndexPatternFromFilter(this.props.filter, this.props.indexPatterns); } private getFieldFromFilter() { diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index 1921f6672755d..1c64ec7436ac4 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -24,7 +24,7 @@ import React, { Component } from 'react'; import { IUiSettingsClient } from 'src/core/public'; import { FilterEditor } from './filter_editor'; import { FilterView } from './filter_view'; -import { esFilters, utils, IIndexPattern } from '../..'; +import { esFilters, IIndexPattern } from '../..'; interface Props { id: string; @@ -60,7 +60,7 @@ class FilterItemUI extends Component { this.props.className ); - const valueLabel = utils.getDisplayValueFromFilter(filter, this.props.indexPatterns); + const valueLabel = esFilters.getDisplayValueFromFilter(filter, this.props.indexPatterns); const dataTestSubjKey = filter.meta.key ? `filter-key-${filter.meta.key}` : ''; const dataTestSubjValue = filter.meta.value ? `filter-value-${valueLabel}` : ''; const dataTestSubjDisabled = `filter-${ diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 81906a63bd49d..022eb0ae50295 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -24,14 +24,70 @@ export function plugin(initializerContext: PluginInitializerContext) { return new DataServerPlugin(initializerContext); } -export { DataServerPlugin as Plugin }; +/** + * Types to be shared externally + * @public + */ +export { IRequestTypesMap, IResponseTypesMap } from './search'; +export { + // field formats + FIELD_FORMAT_IDS, + IFieldFormat, + IFieldFormatId, + IFieldFormatType, + // index patterns + IIndexPattern, + IFieldType, + IFieldSubType, + // kbn field types + ES_FIELD_TYPES, + KBN_FIELD_TYPES, + // query + Query, + // timefilter + RefreshInterval, + TimeRange, +} from '../common'; + +/** + * Static code to be shared externally + * @public + */ export { IndexPatternsFetcher, FieldDescriptor, shouldReadFieldFromDocValues, } from './index_patterns'; - export * from './search'; -export * from '../common'; +export { + // es query + esFilters, + esKuery, + esQuery, + // field formats + BoolFormat, + BytesFormat, + ColorFormat, + DateFormat, + DateNanosFormat, + DEFAULT_CONVERTER_COLOR, + DurationFormat, + FieldFormat, + IpFormat, + NumberFormat, + PercentFormat, + RelativeDateFormat, + SourceFormat, + StaticLookupFormat, + StringFormat, + TruncateFormat, + UrlFormat, + // index patterns + isFilterable, + // kbn field types + castEsToKbnFieldTypeName, + getKbnFieldType, + getKbnTypeNames, +} from '../common'; -export { IRequestTypesMap, IResponseTypesMap } from './search'; +export { DataServerPlugin as Plugin }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/index.tsx index 29e1bc228e066..e6fec597ed8ea 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/index.tsx @@ -22,7 +22,6 @@ import { IIndexPattern, esFilters, Query, - utils, } from '../../../../../../../../../../src/plugins/data/public'; import { FilterLabel } from './filter_label'; @@ -126,7 +125,7 @@ const getDescriptionItem = ( From f84f46fbf05d57c37d537d86af44a4debe4a8b09 Mon Sep 17 00:00:00 2001 From: Melori Arellano Date: Thu, 12 Dec 2019 13:06:10 -0700 Subject: [PATCH 13/36] [DOCS]Clarify that by default server.host only allows local connections (#52802) (#52947) * [DOCS]Clarify that by default server.host only allows local connections * Update docs/setup/access.asciidoc Co-Authored-By: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/setup/settings.asciidoc Co-Authored-By: gchaps <33642766+gchaps@users.noreply.github.com> * Update docs/setup/settings.asciidoc Co-Authored-By: gchaps <33642766+gchaps@users.noreply.github.com> --- docs/images/kibana-status-page-7_5_0.png | Bin 0 -> 95607 bytes docs/images/kibana-status-page.png | Bin 254446 -> 0 bytes docs/setup/access.asciidoc | 9 +++--- docs/setup/settings.asciidoc | 34 +++++++++++------------ 4 files changed, 21 insertions(+), 22 deletions(-) create mode 100644 docs/images/kibana-status-page-7_5_0.png delete mode 100644 docs/images/kibana-status-page.png diff --git a/docs/images/kibana-status-page-7_5_0.png b/docs/images/kibana-status-page-7_5_0.png new file mode 100644 index 0000000000000000000000000000000000000000..2dac4c3f94c351a0f95511c3e8248f7cea5c8a69 GIT binary patch literal 95607 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 XzONS{Sv>A=;E%L~{F72K!+`$-F|a-A literal 0 HcmV?d00001 diff --git a/docs/images/kibana-status-page.png b/docs/images/kibana-status-page.png deleted file mode 100644 index b269dbd3573039fbeb5ae269b604a07d5e57a004..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 254446 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

M1B&AN0s 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

a@u6z3}hK7gh;2Q!0|NB>c#j$w^2rQv2 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

FPw3O&8 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

BE7oQF`? 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 diff --git a/docs/setup/access.asciidoc b/docs/setup/access.asciidoc index 538b42781b127..a7374a37ddaec 100644 --- a/docs/setup/access.asciidoc +++ b/docs/setup/access.asciidoc @@ -2,8 +2,8 @@ == Accessing Kibana Kibana is a web application that you access through port 5601. All you need to do is point your web browser at the -machine where Kibana is running and specify the port number. For example, `localhost:5601` or -`http://YOURDOMAIN.com:5601`. +machine where Kibana is running and specify the port number. For example, `localhost:5601` or `http://YOURDOMAIN.com:5601`. +If you want to allow remote users to connect, set the parameter `server.host` in `kibana.yml` to a non-loopback address. When you access Kibana, the <> page loads by default with the default index pattern selected. The time filter is set to the last 15 minutes and the search query is set to match-all (\*). @@ -15,9 +15,10 @@ If you still don't see any results, it's possible that you don't *have* any docu [[status]] === Checking Kibana Status -You can reach the Kibana server's status page by navigating to `localhost:5601/status`. The status page displays +You can reach the Kibana server's status page by navigating to the status endpoint, for example, `localhost:5601/status`. The status page displays information about the server's resource usage and lists the installed plugins. -image::images/kibana-status-page.png[] +[role="screenshot"] +image::images/kibana-status-page-7_5_0.png[] NOTE: For JSON-formatted server status details, use the API endpoint at `localhost:5601/api/status` diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 3fef2b1abb727..00b9e599911b3 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -7,9 +7,7 @@ if you installed {kib} from an archive distribution (`.tar.gz` or `.zip`), by default it is in `$KIBANA_HOME/config`. By default, with package distributions (Debian or RPM), it is in `/etc/kibana`. -The default settings configure Kibana to run on `localhost:5601`. To change the -host or port number, or connect to Elasticsearch running on a different machine, -you'll need to update your `kibana.yml` file. You can also enable SSL and set a +The default host and port settings configure {kib} to run on `localhost:5601`. To change this behavior and allow remote users to connect, you'll need to update your `kibana.yml` file. You can also enable SSL and set a variety of other options. Finally, environment variables can be injected into configuration using `${MY_ENV_VAR}` syntax. @@ -32,7 +30,7 @@ strongly recommend that you keep the default CSP rules that ship with Kibana. `csp.strict:`:: *Default: `false`* Blocks access to Kibana to any browser that does not enforce even rudimentary CSP rules. In practice, this will disable -support for older, less safe browsers like Internet Explorer. +support for older, less safe browsers like Internet Explorer. See <> for more information. `csp.warnLegacyBrowsers:`:: *Default: `true`* Shows a warning message after @@ -65,7 +63,7 @@ connects to this Kibana instance. `elasticsearch.requestHeadersWhitelist:`:: *Default: `[ 'authorization' ]`* List of Kibana client-side headers to send to Elasticsearch. To send *no* client-side headers, set this value to [] (an empty list). -Removing the `authorization` header from being whitelisted means that you cannot +Removing the `authorization` header from being whitelisted means that you cannot use <> in Kibana. `elasticsearch.requestTimeout:`:: *Default: 30000* Time in milliseconds to wait @@ -171,19 +169,19 @@ The following example shows a valid logging rotate configuration: enable log rotation. If you do not have a `logging.dest` set that is different from `stdout` that feature would not take any effect. -`logging.rotate.everyBytes:`:: [experimental] *Default: 10485760* The maximum size of a log file (that is `not an exact` limit). After the +`logging.rotate.everyBytes:`:: [experimental] *Default: 10485760* The maximum size of a log file (that is `not an exact` limit). After the limit is reached, a new log file is generated. The default size limit is 10485760 (10 MB) and this option should be at least greater than 1024. -`logging.rotate.keepFiles:`:: [experimental] *Default: 7* The number of most recent rotated log files to keep -on disk. Older files are deleted during log rotation. The default value is 7. The `logging.rotate.keepFiles` +`logging.rotate.keepFiles:`:: [experimental] *Default: 7* The number of most recent rotated log files to keep +on disk. Older files are deleted during log rotation. The default value is 7. The `logging.rotate.keepFiles` option has to be in the range of 2 to 1024 files. -`logging.rotate.pollingInterval:`:: [experimental] *Default: 10000* The number of milliseconds for the polling strategy in case +`logging.rotate.pollingInterval:`:: [experimental] *Default: 10000* The number of milliseconds for the polling strategy in case the `logging.rotate.usePolling` is enabled. That option has to be in the range of 5000 to 3600000 milliseconds. -`logging.rotate.usePolling:`:: [experimental] *Default: false* By default we try to understand the best way to monitoring -the log file. However, there is some systems where it could not be always accurate. In those cases, if needed, +`logging.rotate.usePolling:`:: [experimental] *Default: false* By default we try to understand the best way to monitoring +the log file. However, there is some systems where it could not be always accurate. In those cases, if needed, the `polling` method could be used enabling that option. `logging.silent:`:: *Default: false* Set the value of this setting to `true` to @@ -311,7 +309,7 @@ This setting may not be used when `server.compression.enabled` is set to `false` send on all responses to the client from the Kibana server. `server.host:`:: *Default: "localhost"* This setting specifies the host of the -back end server. +back end server. To allow remote users to connect, set the value to the IP address or DNS name of the {kib} server. `server.keepaliveTimeout:`:: *Default: "120000"* The number of milliseconds to wait for additional data before restarting the `server.socketTimeout` counter. @@ -364,15 +362,15 @@ supported protocols with versions. Valid protocols: `TLSv1`, `TLSv1.1`, `TLSv1.2 setting this to `true` enables unauthenticated users to access the Kibana server status API and status page. -`telemetry.allowChangingOptInStatus`:: *Default: true*. If `true`, -users are able to change the telemetry setting at a later time in -<>. If `false`, -{kib} looks at the value of `telemetry.optIn` to determine whether to send +`telemetry.allowChangingOptInStatus`:: *Default: true*. If `true`, +users are able to change the telemetry setting at a later time in +<>. If `false`, +{kib} looks at the value of `telemetry.optIn` to determine whether to send telemetry data or not. `telemetry.allowChangingOptInStatus` and `telemetry.optIn` cannot be `false` at the same time. -`telemetry.optIn`:: *Default: true* If `true`, telemetry data is sent to Elastic. - If `false`, collection of telemetry data is disabled. +`telemetry.optIn`:: *Default: true* If `true`, telemetry data is sent to Elastic. + If `false`, collection of telemetry data is disabled. To enable telemetry and prevent users from disabling it, set `telemetry.allowChangingOptInStatus` to `false` and `telemetry.optIn` to `true`. From 27c4a8b895e0398295d849210dcc1f60c4eab022 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Thu, 12 Dec 2019 15:14:35 -0500 Subject: [PATCH 14/36] [7.x] Print out agent debugging links during CI (#52812) (#52901) --- vars/agentInfo.groovy | 42 ++++++++++++++++++++++++++++++++++++++ vars/kibanaPipeline.groovy | 2 ++ 2 files changed, 44 insertions(+) create mode 100644 vars/agentInfo.groovy diff --git a/vars/agentInfo.groovy b/vars/agentInfo.groovy new file mode 100644 index 0000000000000..b53ed23f81ff0 --- /dev/null +++ b/vars/agentInfo.groovy @@ -0,0 +1,42 @@ +def print() { + try { + def startTime = sh(script: "date -d '-3 minutes' -Iseconds | sed s/+/%2B/", returnStdout: true).trim() + def endTime = sh(script: "date -d '+1 hour 30 minutes' -Iseconds | sed s/+/%2B/", returnStdout: true).trim() + + def resourcesUrl = + ( + "https://infra-stats.elastic.co/app/kibana#/visualize/edit/8bd92360-1b92-11ea-b719-aba04518cc34" + + "?_g=(time:(from:'${startTime}',to:'${endTime}'))" + + "&_a=(query:'host.name:${env.NODE_NAME}')" + ) + .replaceAll("'", '%27') // Need to escape ' because of the shell echo below, but can't really replace "'" with "\'" because of groovy sandbox + .replaceAll(/\)$/, '%29') // This is just here because the URL parsing in the Jenkins console doesn't work right + + def logsStartTime = sh(script: "date -d '-3 minutes' +%s", returnStdout: true).trim() + def logsUrl = + ( + "https://infra-stats.elastic.co/app/infra#/logs" + + "?_g=()&flyoutOptions=(flyoutId:!n,flyoutVisibility:hidden,surroundingLogsId:!n)" + + "&logFilter=(expression:'host.name:${env.NODE_NAME}',kind:kuery)" + + "&logPosition=(position:(time:${logsStartTime}000),streamLive:!f)" + ) + .replaceAll("'", '%27') + .replaceAll('\\)', '%29') + + sh script: """ + set +x + echo 'Resource Graph:' + echo '${resourcesUrl}' + echo '' + echo 'Agent Logs:' + echo '${logsUrl}' + echo '' + echo 'SSH Command:' + echo "ssh -F ssh_config \$(hostname --ip-address)" + """, label: "Worker/Agent/Node debug links" + } catch(ex) { + print ex.toString() + } +} + +return this diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index dbb33f2766dac..18f214554b444 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -116,6 +116,8 @@ def legacyJobRunner(name) { def jobRunner(label, useRamDisk, closure) { node(label) { + agentInfo.print() + if (useRamDisk) { // Move to a temporary workspace, so that we can symlink the real workspace into /dev/shm def originalWorkspace = env.WORKSPACE From 1d0aa3db61791bab8c9df171c432b20109717dd8 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 12 Dec 2019 13:51:35 -0700 Subject: [PATCH 15/36] [ML] DF Analytics: create classification jobs results view (#52584) (#52941) * wip: create classification results page + table and evaluate panel * enable view link for classification jobs * wip: fetch classification eval data * wip: display confusion matrix in datagrid * evaluate panel: add heatmap for cells and doc count * Update use of loadEvalData in expanded row component * Add metric type for evaluate endpoint and fix localization error * handle no incorrect prediction classes case for confusion matrix. remove unused translation * setCellProps needs to be called from a lifecycle method - wrap in useEffect * TypeScript improvements * fix datagrid column resize affecting results table * allow custom prediction field for classification jobs * ensure values are rounded correctly and add tooltip * temp workaroun for datagrid width issues --- .../data_frame_analytics/_index.scss | 1 + .../data_frame_analytics/common/analytics.ts | 186 +++++-- .../data_frame_analytics/common/fields.ts | 57 ++- .../data_frame_analytics/common/index.ts | 2 + .../_classification_exploration.scss | 4 + .../classification_exploration/_index.scss | 1 + .../classification_exploration.tsx | 120 +++++ .../column_data.tsx | 80 +++ .../evaluate_panel.tsx | 343 +++++++++++++ .../classification_exploration/index.ts | 7 + .../results_table.tsx | 481 ++++++++++++++++++ .../use_explore_data.ts | 156 ++++++ .../error_callout.tsx | 0 .../components/error_callout/index.ts | 7 + .../components/loading_panel/index.ts | 7 + .../loading_panel/loading_panel.tsx | 14 + .../regression_exploration/evaluate_panel.tsx | 107 ++-- .../regression_exploration.tsx | 17 +- .../regression_exploration/results_table.tsx | 4 +- .../use_explore_data.ts | 5 - .../pages/analytics_exploration/page.tsx | 4 + .../components/analytics_list/actions.tsx | 5 +- .../analytics_list/expanded_row.tsx | 20 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 25 files changed, 1514 insertions(+), 116 deletions(-) create mode 100644 x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_classification_exploration.scss create mode 100644 x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_index.scss create mode 100644 x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/column_data.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts rename x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/{regression_exploration => error_callout}/error_callout.tsx (100%) create mode 100644 x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/loading_panel/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/loading_panel/loading_panel.tsx diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/_index.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/_index.scss index c231c405b5369..962d3f4c7bd54 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/_index.scss +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/_index.scss @@ -1,5 +1,6 @@ @import 'pages/analytics_exploration/components/exploration/index'; @import 'pages/analytics_exploration/components/regression_exploration/index'; +@import 'pages/analytics_exploration/components/classification_exploration/index'; @import 'pages/analytics_management/components/analytics_list/index'; @import 'pages/analytics_management/components/create_analytics_form/index'; @import 'pages/analytics_management/components/create_analytics_flyout/index'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 0642c1fbe6186..cadc1f01c6dda 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -35,6 +35,7 @@ interface ClassificationAnalysis { dependent_variable: string; training_percent?: number; num_top_classes?: string; + prediction_field_name?: string; }; } @@ -74,13 +75,33 @@ export interface RegressionEvaluateResponse { }; } +export interface PredictedClass { + predicted_class: string; + count: number; +} + +export interface ConfusionMatrix { + actual_class: string; + actual_class_doc_count: number; + predicted_classes: PredictedClass[]; + other_predicted_class_doc_count: number; +} + +export interface ClassificationEvaluateResponse { + classification: { + multiclass_confusion_matrix: { + confusion_matrix: ConfusionMatrix[]; + }; + }; +} + interface GenericAnalysis { [key: string]: Record; } interface LoadEvaluateResult { success: boolean; - eval: RegressionEvaluateResponse | null; + eval: RegressionEvaluateResponse | ClassificationEvaluateResponse | null; error: string | null; } @@ -109,6 +130,7 @@ export const getAnalysisType = (analysis: AnalysisConfig) => { export const getDependentVar = (analysis: AnalysisConfig) => { let depVar = ''; + if (isRegressionAnalysis(analysis)) { depVar = analysis.regression.dependent_variable; } @@ -124,17 +146,26 @@ export const getPredictionFieldName = (analysis: AnalysisConfig) => { let predictionFieldName; if (isRegressionAnalysis(analysis) && analysis.regression.prediction_field_name !== undefined) { predictionFieldName = analysis.regression.prediction_field_name; + } else if ( + isClassificationAnalysis(analysis) && + analysis.classification.prediction_field_name !== undefined + ) { + predictionFieldName = analysis.classification.prediction_field_name; } return predictionFieldName; }; -export const getPredictedFieldName = (resultsField: string, analysis: AnalysisConfig) => { +export const getPredictedFieldName = ( + resultsField: string, + analysis: AnalysisConfig, + forSort?: boolean +) => { // default is 'ml' const predictionFieldName = getPredictionFieldName(analysis); const defaultPredictionField = `${getDependentVar(analysis)}_prediction`; const predictedField = `${resultsField}.${ predictionFieldName ? predictionFieldName : defaultPredictionField - }`; + }${isClassificationAnalysis(analysis) && !forSort ? '.keyword' : ''}`; return predictedField; }; @@ -153,13 +184,32 @@ export const isClassificationAnalysis = (arg: any): arg is ClassificationAnalysi return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.CLASSIFICATION; }; -export const isRegressionResultsSearchBoolQuery = ( - arg: any -): arg is RegressionResultsSearchBoolQuery => { +export const isResultsSearchBoolQuery = (arg: any): arg is ResultsSearchBoolQuery => { const keys = Object.keys(arg); return keys.length === 1 && keys[0] === 'bool'; }; +export const isRegressionEvaluateResponse = (arg: any): arg is RegressionEvaluateResponse => { + const keys = Object.keys(arg); + return ( + keys.length === 1 && + keys[0] === ANALYSIS_CONFIG_TYPE.REGRESSION && + arg?.regression?.mean_squared_error !== undefined && + arg?.regression?.r_squared !== undefined + ); +}; + +export const isClassificationEvaluateResponse = ( + arg: any +): arg is ClassificationEvaluateResponse => { + const keys = Object.keys(arg); + return ( + keys.length === 1 && + keys[0] === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && + arg?.classification?.multiclass_confusion_matrix !== undefined + ); +}; + export interface DataFrameAnalyticsConfig { id: DataFrameAnalyticsId; // Description attribute is not supported yet @@ -254,17 +304,14 @@ export function getValuesFromResponse(response: RegressionEvaluateResponse) { return { meanSquaredError, rSquared }; } -interface RegressionResultsSearchBoolQuery { +interface ResultsSearchBoolQuery { bool: Dictionary; } -interface RegressionResultsSearchTermQuery { +interface ResultsSearchTermQuery { term: Dictionary; } -export type RegressionResultsSearchQuery = - | RegressionResultsSearchBoolQuery - | RegressionResultsSearchTermQuery - | SavedSearchQuery; +export type ResultsSearchQuery = ResultsSearchBoolQuery | ResultsSearchTermQuery | SavedSearchQuery; export function getEvalQueryBody({ resultsField, @@ -274,16 +321,16 @@ export function getEvalQueryBody({ }: { resultsField: string; isTraining: boolean; - searchQuery?: RegressionResultsSearchQuery; + searchQuery?: ResultsSearchQuery; ignoreDefaultQuery?: boolean; }) { - let query: RegressionResultsSearchQuery = { + let query: ResultsSearchQuery = { term: { [`${resultsField}.is_training`]: { value: isTraining } }, }; if (searchQuery !== undefined && ignoreDefaultQuery === true) { query = searchQuery; - } else if (searchQuery !== undefined && isRegressionResultsSearchBoolQuery(searchQuery)) { + } else if (searchQuery !== undefined && isResultsSearchBoolQuery(searchQuery)) { const searchQueryClone = cloneDeep(searchQuery); searchQueryClone.bool.must.push(query); query = searchQueryClone; @@ -291,6 +338,27 @@ export function getEvalQueryBody({ return query; } +interface EvaluateMetrics { + classification: { + multiclass_confusion_matrix: object; + }; + regression: { + r_squared: object; + mean_squared_error: object; + }; +} + +interface LoadEvalDataConfig { + isTraining: boolean; + index: string; + dependentVariable: string; + resultsField: string; + predictionFieldName?: string; + searchQuery?: ResultsSearchQuery; + ignoreDefaultQuery?: boolean; + jobType: ANALYSIS_CONFIG_TYPE; +} + export const loadEvalData = async ({ isTraining, index, @@ -299,34 +367,38 @@ export const loadEvalData = async ({ predictionFieldName, searchQuery, ignoreDefaultQuery, -}: { - isTraining: boolean; - index: string; - dependentVariable: string; - resultsField: string; - predictionFieldName?: string; - searchQuery?: RegressionResultsSearchQuery; - ignoreDefaultQuery?: boolean; -}) => { + jobType, +}: LoadEvalDataConfig) => { const results: LoadEvaluateResult = { success: false, eval: null, error: null }; const defaultPredictionField = `${dependentVariable}_prediction`; - const predictedField = `${resultsField}.${ + let predictedField = `${resultsField}.${ predictionFieldName ? predictionFieldName : defaultPredictionField }`; + if (jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION) { + predictedField = `${predictedField}.keyword`; + } + const query = getEvalQueryBody({ resultsField, isTraining, searchQuery, ignoreDefaultQuery }); + const metrics: EvaluateMetrics = { + classification: { + multiclass_confusion_matrix: {}, + }, + regression: { + r_squared: {}, + mean_squared_error: {}, + }, + }; + const config = { index, query, evaluation: { - regression: { + [jobType]: { actual_field: dependentVariable, predicted_field: predictedField, - metrics: { - r_squared: {}, - mean_squared_error: {}, - }, + metrics: metrics[jobType as keyof EvaluateMetrics], }, }, }; @@ -341,3 +413,57 @@ export const loadEvalData = async ({ return results; } }; + +interface TrackTotalHitsSearchResponse { + hits: { + total: { + value: number; + relation: string; + }; + hits: any[]; + }; +} + +interface LoadDocsCountConfig { + ignoreDefaultQuery?: boolean; + isTraining: boolean; + searchQuery: SavedSearchQuery; + resultsField: string; + destIndex: string; +} + +interface LoadDocsCountResponse { + docsCount: number | null; + success: boolean; +} + +export const loadDocsCount = async ({ + ignoreDefaultQuery = true, + isTraining, + searchQuery, + resultsField, + destIndex, +}: LoadDocsCountConfig): Promise => { + const query = getEvalQueryBody({ resultsField, isTraining, ignoreDefaultQuery, searchQuery }); + + try { + const body: SearchQuery = { + track_total_hits: true, + query, + }; + + const resp: TrackTotalHitsSearchResponse = await ml.esSearch({ + index: destIndex, + size: 0, + body, + }); + + const docsCount = resp.hits.total && resp.hits.total.value; + return { docsCount, success: docsCount !== undefined }; + } catch (e) { + return { + docsCount: null, + success: false, + }; + } +}; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/fields.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/fields.ts index 5621d77f66469..216836db4ccbc 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/fields.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/fields.ts @@ -77,7 +77,7 @@ export const sortRegressionResultsFields = ( ) => { const dependentVariable = getDependentVar(jobConfig.analysis); const resultsField = jobConfig.dest.results_field; - const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis); + const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis, true); if (a === `${resultsField}.is_training`) { return -1; } @@ -96,6 +96,14 @@ export const sortRegressionResultsFields = ( if (b === dependentVariable) { return 1; } + + if (a === `${resultsField}.prediction_probability`) { + return -1; + } + if (b === `${resultsField}.prediction_probability`) { + return 1; + } + return a.localeCompare(b); }; @@ -107,7 +115,7 @@ export const sortRegressionResultsColumns = ( ) => (a: string, b: string) => { const dependentVariable = getDependentVar(jobConfig.analysis); const resultsField = jobConfig.dest.results_field; - const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis); + const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis, true); const typeofA = typeof obj[a]; const typeofB = typeof obj[b]; @@ -136,6 +144,14 @@ export const sortRegressionResultsColumns = ( return 1; } + if (a === `${resultsField}.prediction_probability`) { + return -1; + } + + if (b === `${resultsField}.prediction_probability`) { + return 1; + } + if (typeofA !== 'string' && typeofB === 'string') { return 1; } @@ -184,6 +200,43 @@ export function getFlattenedFields(obj: EsDocSource, resultsField: string): EsFi return flatDocFields.filter(f => f !== ML__ID_COPY); } +export const getDefaultClassificationFields = ( + docs: EsDoc[], + jobConfig: DataFrameAnalyticsConfig +): EsFieldName[] => { + if (docs.length === 0) { + return []; + } + const resultsField = jobConfig.dest.results_field; + const newDocFields = getFlattenedFields(docs[0]._source, resultsField); + return newDocFields + .filter(k => { + if (k === `${resultsField}.is_training`) { + return true; + } + // predicted value of dependent variable + if (k === getPredictedFieldName(resultsField, jobConfig.analysis, true)) { + return true; + } + // actual value of dependent variable + if (k === getDependentVar(jobConfig.analysis)) { + return true; + } + + if (k === `${resultsField}.prediction_probability`) { + return true; + } + + if (k.split('.')[0] === resultsField) { + return false; + } + + return docs.some(row => row._source[k] !== null); + }) + .sort((a, b) => sortRegressionResultsFields(a, b, jobConfig)) + .slice(0, DEFAULT_REGRESSION_COLUMNS); +}; + export const getDefaultRegressionFields = ( docs: EsDoc[], jobConfig: DataFrameAnalyticsConfig diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/index.ts index 02a1c30259cce..f7794af8b5861 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/index.ts @@ -20,6 +20,7 @@ export { RegressionEvaluateResponse, getValuesFromResponse, loadEvalData, + loadDocsCount, Eval, getPredictedFieldName, INDEX_STATUS, @@ -31,6 +32,7 @@ export { export { getDefaultSelectableFields, getDefaultRegressionFields, + getDefaultClassificationFields, getFlattenedFields, sortColumns, sortRegressionResultsColumns, diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_classification_exploration.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_classification_exploration.scss new file mode 100644 index 0000000000000..1141dddf398b0 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_classification_exploration.scss @@ -0,0 +1,4 @@ +.euiFormRow.mlDataFrameAnalyticsClassification__actualLabel { + padding-top: $euiSize * 4; +} + diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_index.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_index.scss new file mode 100644 index 0000000000000..88edd92951d41 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_index.scss @@ -0,0 +1 @@ +@import 'classification_exploration'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx new file mode 100644 index 0000000000000..f424ebee58120 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, Fragment, useState, useEffect } from 'react'; +import { EuiCallOut, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ml } from '../../../../../services/ml_api_service'; +import { DataFrameAnalyticsConfig } from '../../../../common'; +import { EvaluatePanel } from './evaluate_panel'; +import { ResultsTable } from './results_table'; +import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; +import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; +import { LoadingPanel } from '../loading_panel'; + +interface GetDataFrameAnalyticsResponse { + count: number; + data_frame_analytics: DataFrameAnalyticsConfig[]; +} + +export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( + + + {i18n.translate('xpack.ml.dataframe.analytics.classificationExploration.tableJobIdTitle', { + defaultMessage: 'Destination index for classification job ID {jobId}', + values: { jobId }, + })} + + +); + +interface Props { + jobId: string; + jobStatus: DATA_FRAME_TASK_STATE; +} + +export const ClassificationExploration: FC = ({ jobId, jobStatus }) => { + const [jobConfig, setJobConfig] = useState(undefined); + const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); + const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); + + const loadJobConfig = async () => { + setIsLoadingJobConfig(true); + try { + const analyticsConfigs: GetDataFrameAnalyticsResponse = await ml.dataFrameAnalytics.getDataFrameAnalytics( + jobId + ); + if ( + Array.isArray(analyticsConfigs.data_frame_analytics) && + analyticsConfigs.data_frame_analytics.length > 0 + ) { + setJobConfig(analyticsConfigs.data_frame_analytics[0]); + setIsLoadingJobConfig(false); + } else { + setJobConfigErrorMessage( + i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationNoResultsMessage', + { + defaultMessage: 'No results found.', + } + ) + ); + } + } catch (e) { + if (e.message !== undefined) { + setJobConfigErrorMessage(e.message); + } else { + setJobConfigErrorMessage(JSON.stringify(e)); + } + setIsLoadingJobConfig(false); + } + }; + + useEffect(() => { + loadJobConfig(); + }, []); + + if (jobConfigErrorMessage !== undefined) { + return ( + + + + +

{jobConfigErrorMessage}

+ + + ); + } + + return ( + + {isLoadingJobConfig === true && jobConfig === undefined && } + {isLoadingJobConfig === false && jobConfig !== undefined && ( + + )} + + {isLoadingJobConfig === true && jobConfig === undefined && } + {isLoadingJobConfig === false && jobConfig !== undefined && ( + + )} + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/column_data.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/column_data.tsx new file mode 100644 index 0000000000000..5a08dd159affb --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/column_data.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { ConfusionMatrix, PredictedClass } from '../../../../common/analytics'; + +interface ColumnData { + actual_class: string; + actual_class_doc_count: number; + predicted_class?: string; + count?: number; + error_count?: number; +} + +export function getColumnData(confusionMatrixData: ConfusionMatrix[]) { + const colData: Partial = []; + + confusionMatrixData.forEach((classData: any) => { + const correctlyPredictedClass = classData.predicted_classes.find( + (pc: PredictedClass) => pc.predicted_class === classData.actual_class + ); + const incorrectlyPredictedClass = classData.predicted_classes.find( + (pc: PredictedClass) => pc.predicted_class !== classData.actual_class + ); + + let accuracy; + if (correctlyPredictedClass !== undefined) { + accuracy = correctlyPredictedClass.count / classData.actual_class_doc_count; + // round to 2 decimal places without converting to string; + accuracy = Math.round(accuracy * 100) / 100; + } + + let error; + if (incorrectlyPredictedClass !== undefined) { + error = incorrectlyPredictedClass.count / classData.actual_class_doc_count; + error = Math.round(error * 100) / 100; + } + + let col: any = { + actual_class: classData.actual_class, + actual_class_doc_count: classData.actual_class_doc_count, + }; + + if (correctlyPredictedClass !== undefined) { + col = { + ...col, + predicted_class: correctlyPredictedClass.predicted_class, + [correctlyPredictedClass.predicted_class]: accuracy, + count: correctlyPredictedClass.count, + accuracy, + }; + } + + if (incorrectlyPredictedClass !== undefined) { + col = { + ...col, + [incorrectlyPredictedClass.predicted_class]: error, + error_count: incorrectlyPredictedClass.count, + }; + } + + colData.push(col); + }); + + const columns: any = [ + { + id: 'actual_class', + display: , + }, + ]; + + colData.forEach((data: any) => { + columns.push({ id: data.predicted_class }); + }); + + return { columns, columnData: colData }; +} diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx new file mode 100644 index 0000000000000..ddf52943c2feb --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx @@ -0,0 +1,343 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useState, useEffect, Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiDataGrid, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiIconTip, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { ErrorCallout } from '../error_callout'; +import { + getDependentVar, + getPredictionFieldName, + loadEvalData, + loadDocsCount, + DataFrameAnalyticsConfig, +} from '../../../../common'; +import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; +import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; +import { + isResultsSearchBoolQuery, + isClassificationEvaluateResponse, + ConfusionMatrix, + ResultsSearchQuery, + ANALYSIS_CONFIG_TYPE, +} from '../../../../common/analytics'; +import { LoadingPanel } from '../loading_panel'; +import { getColumnData } from './column_data'; + +const defaultPanelWidth = 500; + +interface Props { + jobConfig: DataFrameAnalyticsConfig; + jobStatus: DATA_FRAME_TASK_STATE; + searchQuery: ResultsSearchQuery; +} + +export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) => { + const [isLoading, setIsLoading] = useState(false); + const [confusionMatrixData, setConfusionMatrixData] = useState([]); + const [columns, setColumns] = useState([]); + const [columnsData, setColumnsData] = useState([]); + const [popoverContents, setPopoverContents] = useState([]); + const [docsCount, setDocsCount] = useState(null); + const [error, setError] = useState(null); + const [panelWidth, setPanelWidth] = useState(defaultPanelWidth); + + // Column visibility + const [visibleColumns, setVisibleColumns] = useState(() => + columns.map(({ id }: { id: string }) => id) + ); + + const index = jobConfig.dest.index; + const dependentVariable = getDependentVar(jobConfig.analysis); + const predictionFieldName = getPredictionFieldName(jobConfig.analysis); + // default is 'ml' + const resultsField = jobConfig.dest.results_field; + + const loadData = async ({ + isTrainingClause, + ignoreDefaultQuery = true, + }: { + isTrainingClause: { query: string; operator: string }; + ignoreDefaultQuery?: boolean; + }) => { + setIsLoading(true); + + const evalData = await loadEvalData({ + isTraining: false, + index, + dependentVariable, + resultsField, + predictionFieldName, + searchQuery, + ignoreDefaultQuery, + jobType: ANALYSIS_CONFIG_TYPE.CLASSIFICATION, + }); + + const docsCountResp = await loadDocsCount({ + isTraining: false, + searchQuery, + resultsField, + destIndex: jobConfig.dest.index, + }); + + if ( + evalData.success === true && + evalData.eval && + isClassificationEvaluateResponse(evalData.eval) + ) { + const confusionMatrix = + evalData.eval?.classification?.multiclass_confusion_matrix?.confusion_matrix; + setError(null); + setConfusionMatrixData(confusionMatrix || []); + setIsLoading(false); + } else { + setIsLoading(false); + setConfusionMatrixData([]); + setError(evalData.error); + } + + if (docsCountResp.success === true) { + setDocsCount(docsCountResp.docsCount); + } else { + setDocsCount(null); + } + }; + + const resizeHandler = () => { + const tablePanelWidth: number = + document.getElementById('mlDataFrameAnalyticsTableResultsPanel')?.clientWidth || + defaultPanelWidth; + // Keep the evaluate panel width slightly smaller than the results table + // to ensure results table can resize correctly. Temporary workaround DataGrid issue with flex + const newWidth = tablePanelWidth - 8; + setPanelWidth(newWidth); + }; + + useEffect(() => { + window.addEventListener('resize', resizeHandler); + resizeHandler(); + return () => { + window.removeEventListener('resize', resizeHandler); + }; + }, []); + + useEffect(() => { + if (confusionMatrixData.length > 0) { + const { columns: derivedColumns, columnData } = getColumnData(confusionMatrixData); + // Initialize all columns as visible + setVisibleColumns(() => derivedColumns.map(({ id }: { id: string }) => id)); + setColumns(derivedColumns); + setColumnsData(columnData); + setPopoverContents({ + numeric: ({ + cellContentsElement, + children, + }: { + cellContentsElement: any; + children: any; + }) => { + const rowIndex = children?.props?.rowIndex; + const colId = children?.props?.columnId; + const gridItem = columnData[rowIndex]; + + if (gridItem !== undefined) { + const count = colId === gridItem.actual_class ? gridItem.count : gridItem.error_count; + return `${count} / ${gridItem.actual_class_doc_count} * 100 = ${cellContentsElement.textContent}`; + } + + return cellContentsElement.textContent; + }, + }); + } + }, [confusionMatrixData]); + + useEffect(() => { + const hasIsTrainingClause = + isResultsSearchBoolQuery(searchQuery) && + searchQuery.bool.must.filter( + (clause: any) => clause.match && clause.match[`${resultsField}.is_training`] !== undefined + ); + const isTrainingClause = + hasIsTrainingClause && + hasIsTrainingClause[0] && + hasIsTrainingClause[0].match[`${resultsField}.is_training`]; + + loadData({ isTrainingClause }); + }, [JSON.stringify(searchQuery)]); + + const renderCellValue = ({ + rowIndex, + columnId, + setCellProps, + }: { + rowIndex: number; + columnId: string; + setCellProps: any; + }) => { + const cellValue = columnsData[rowIndex][columnId]; + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + setCellProps({ + style: { + backgroundColor: `rgba(0, 179, 164, ${cellValue})`, + }, + }); + }, [rowIndex, columnId, setCellProps]); + return ( + {typeof cellValue === 'number' ? `${Math.round(cellValue * 100)}%` : cellValue} + ); + }; + + if (isLoading === true) { + return ; + } + + return ( + + + + + + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.evaluateJobIdTitle', + { + defaultMessage: 'Evaluation of classification job ID {jobId}', + values: { jobId: jobConfig.id }, + } + )} + + + + + {getTaskStateBadge(jobStatus)} + + + + {error !== null && ( + + + + )} + {error === null && ( + + + + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixHelpText', + { + defaultMessage: 'Normalized confusion matrix', + } + )} + + + + + + + + {docsCount !== null && ( + + + + + + )} + {/* BEGIN TABLE ELEMENTS */} + + + + + + + + + {columns.length > 0 && columnsData.length > 0 && ( + + + + + + + + + + + + + + + + + + + + + + + )} + + + + + )} + {/* END TABLE ELEMENTS */} + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/index.ts new file mode 100644 index 0000000000000..4c75d8315b230 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ClassificationExploration } from './classification_exploration'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx new file mode 100644 index 0000000000000..1be158499a3f4 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx @@ -0,0 +1,481 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FC, useEffect, useState } from 'react'; +import moment from 'moment-timezone'; + +import { i18n } from '@kbn/i18n'; +import { + EuiBadge, + EuiButtonIcon, + EuiCallOut, + EuiCheckbox, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiPanel, + EuiPopover, + EuiPopoverTitle, + EuiProgress, + EuiSpacer, + EuiText, + EuiToolTip, + Query, +} from '@elastic/eui'; + +import { Query as QueryType } from '../../../analytics_management/components/analytics_list/common'; + +import { + ColumnType, + mlInMemoryTableBasicFactory, + OnTableChangeArg, + SortingPropType, + SORT_DIRECTION, +} from '../../../../../components/ml_in_memory_table'; + +import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils'; +import { SavedSearchQuery } from '../../../../../contexts/kibana'; + +import { + sortRegressionResultsColumns, + sortRegressionResultsFields, + toggleSelectedField, + DataFrameAnalyticsConfig, + EsFieldName, + EsDoc, + MAX_COLUMNS, + getPredictedFieldName, + INDEX_STATUS, + SEARCH_SIZE, + defaultSearchQuery, +} from '../../../../common'; +import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; +import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; + +import { useExploreData, TableItem } from './use_explore_data'; +import { ExplorationTitle } from './classification_exploration'; + +const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; + +const MlInMemoryTableBasic = mlInMemoryTableBasicFactory(); +interface Props { + jobConfig: DataFrameAnalyticsConfig; + jobStatus: DATA_FRAME_TASK_STATE; + setEvaluateSearchQuery: React.Dispatch>; +} + +export const ResultsTable: FC = React.memo( + ({ jobConfig, jobStatus, setEvaluateSearchQuery }) => { + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(25); + const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); + const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); + const [searchError, setSearchError] = useState(undefined); + const [searchString, setSearchString] = useState(undefined); + + function toggleColumnsPopover() { + setColumnsPopoverVisible(!isColumnsPopoverVisible); + } + + function closeColumnsPopover() { + setColumnsPopoverVisible(false); + } + + function toggleColumn(column: EsFieldName) { + if (tableItems.length > 0 && jobConfig !== undefined) { + // spread to a new array otherwise the component wouldn't re-render + setSelectedFields([...toggleSelectedField(selectedFields, column)]); + } + } + + const { + errorMessage, + loadExploreData, + sortField, + sortDirection, + status, + tableItems, + } = useExploreData(jobConfig, selectedFields, setSelectedFields); + + let docFields: EsFieldName[] = []; + let docFieldsCount = 0; + if (tableItems.length > 0) { + docFields = Object.keys(tableItems[0]); + docFields.sort((a, b) => sortRegressionResultsFields(a, b, jobConfig)); + docFieldsCount = docFields.length; + } + + const columns: Array> = []; + + if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) { + columns.push( + ...selectedFields.sort(sortRegressionResultsColumns(tableItems[0], jobConfig)).map(k => { + const column: ColumnType = { + field: k, + name: k, + sortable: true, + truncateText: true, + }; + + const render = (d: any, fullItem: EsDoc) => { + if (Array.isArray(d) && d.every(item => typeof item === 'string')) { + // If the cells data is an array of strings, return as a comma separated list. + // The list will get limited to 5 items with `…` at the end if there's more in the original array. + return `${d.slice(0, 5).join(', ')}${d.length > 5 ? ', …' : ''}`; + } else if (Array.isArray(d)) { + // If the cells data is an array of e.g. objects, display a 'array' badge with a + // tooltip that explains that this type of field is not supported in this table. + return ( + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.indexArrayBadgeContent', + { + defaultMessage: 'array', + } + )} + + + ); + } else if (typeof d === 'object' && d !== null) { + // If the cells data is an object, display a 'object' badge with a + // tooltip that explains that this type of field is not supported in this table. + return ( + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.indexObjectBadgeContent', + { + defaultMessage: 'object', + } + )} + + + ); + } + + return d; + }; + + let columnType; + + if (tableItems.length > 0) { + columnType = typeof tableItems[0][k]; + } + + if (typeof columnType !== 'undefined') { + switch (columnType) { + case 'boolean': + column.dataType = 'boolean'; + break; + case 'Date': + column.align = 'right'; + column.render = (d: any) => + formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000); + break; + case 'number': + column.dataType = 'number'; + column.render = render; + break; + default: + column.render = render; + break; + } + } else { + column.render = render; + } + + return column; + }) + ); + } + + useEffect(() => { + if (jobConfig !== undefined) { + const predictedFieldName = getPredictedFieldName( + jobConfig.dest.results_field, + jobConfig.analysis + ); + const predictedFieldSelected = selectedFields.includes(predictedFieldName); + + const field = predictedFieldSelected ? predictedFieldName : selectedFields[0]; + const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC; + loadExploreData({ field, direction, searchQuery }); + } + }, [JSON.stringify(searchQuery)]); + + useEffect(() => { + // by default set the sorting to descending on the prediction field (`_prediction`). + // if that's not available sort ascending on the first column. + // also check if the current sorting field is still available. + if (jobConfig !== undefined && columns.length > 0 && !selectedFields.includes(sortField)) { + const predictedFieldName = getPredictedFieldName( + jobConfig.dest.results_field, + jobConfig.analysis + ); + const predictedFieldSelected = selectedFields.includes(predictedFieldName); + + const field = predictedFieldSelected ? predictedFieldName : selectedFields[0]; + const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC; + loadExploreData({ field, direction, searchQuery }); + } + }, [jobConfig, columns.length, sortField, sortDirection, tableItems.length]); + + let sorting: SortingPropType = false; + let onTableChange; + + if (columns.length > 0 && sortField !== '' && sortField !== undefined) { + sorting = { + sort: { + field: sortField, + direction: sortDirection, + }, + }; + + onTableChange = ({ + page = { index: 0, size: 10 }, + sort = { field: sortField, direction: sortDirection }, + }: OnTableChangeArg) => { + const { index, size } = page; + setPageIndex(index); + setPageSize(size); + + if (sort.field !== sortField || sort.direction !== sortDirection) { + loadExploreData({ ...sort, searchQuery }); + } + }; + } + + const pagination = { + initialPageIndex: pageIndex, + initialPageSize: pageSize, + totalItemCount: tableItems.length, + pageSizeOptions: PAGE_SIZE_OPTIONS, + hidePerPageOptions: false, + }; + + const onQueryChange = ({ query, error }: { query: QueryType; error: any }) => { + if (error) { + setSearchError(error.message); + } else { + try { + const esQueryDsl = Query.toESQuery(query); + setSearchQuery(esQueryDsl); + setSearchString(query.text); + setSearchError(undefined); + // set query for use in evaluate panel + setEvaluateSearchQuery(esQueryDsl); + } catch (e) { + setSearchError(e.toString()); + } + } + }; + + const search = { + onChange: onQueryChange, + defaultQuery: searchString, + box: { + incremental: false, + placeholder: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.searchBoxPlaceholder', + { + defaultMessage: 'E.g. avg>0.5', + } + ), + }, + filters: [ + { + type: 'field_value_toggle_group', + field: `${jobConfig.dest.results_field}.is_training`, + items: [ + { + value: false, + name: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.isTestingLabel', + { + defaultMessage: 'Testing', + } + ), + }, + { + value: true, + name: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.isTrainingLabel', + { + defaultMessage: 'Training', + } + ), + }, + ], + }, + ], + }; + + if (jobConfig === undefined) { + return null; + } + // if it's a searchBar syntax error leave the table visible so they can try again + if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) { + return ( + + + + + + + {getTaskStateBadge(jobStatus)} + + + +

{errorMessage}

+
+
+ ); + } + + const tableError = + status === INDEX_STATUS.ERROR && errorMessage.includes('parsing_exception') + ? errorMessage + : searchError; + + return ( + + + + + + + + + {getTaskStateBadge(jobStatus)} + + + + + + + {docFieldsCount > MAX_COLUMNS && ( + + {i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.fieldSelection', + { + defaultMessage: + '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected', + values: { selectedFieldsLength: selectedFields.length, docFieldsCount }, + } + )} + + )} + + + + + } + isOpen={isColumnsPopoverVisible} + closePopover={closeColumnsPopover} + ownFocus + > + + {i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.selectFieldsPopoverTitle', + { + defaultMessage: 'Select fields', + } + )} + +
+ {docFields.map(d => ( + toggleColumn(d)} + disabled={selectedFields.includes(d) && selectedFields.length === 1} + /> + ))} +
+
+
+
+
+
+
+ {status === INDEX_STATUS.LOADING && } + {status !== INDEX_STATUS.LOADING && ( + + )} + {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( + + {tableItems.length === SEARCH_SIZE && ( + + + + )} + + + + )} +
+ ); + } +); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts new file mode 100644 index 0000000000000..ba12fcab98a36 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState } from 'react'; + +import { SearchResponse } from 'elasticsearch'; + +import { SortDirection, SORT_DIRECTION } from '../../../../../components/ml_in_memory_table'; + +import { ml } from '../../../../../services/ml_api_service'; +import { getNestedProperty } from '../../../../../util/object_utils'; +import { SavedSearchQuery } from '../../../../../contexts/kibana'; + +import { + getDefaultClassificationFields, + getFlattenedFields, + DataFrameAnalyticsConfig, + EsFieldName, + getPredictedFieldName, + INDEX_STATUS, + SEARCH_SIZE, + defaultSearchQuery, + SearchQuery, +} from '../../../../common'; + +export type TableItem = Record; + +interface LoadExploreDataArg { + field: string; + direction: SortDirection; + searchQuery: SavedSearchQuery; +} +export interface UseExploreDataReturnType { + errorMessage: string; + loadExploreData: (arg: LoadExploreDataArg) => void; + sortField: EsFieldName; + sortDirection: SortDirection; + status: INDEX_STATUS; + tableItems: TableItem[]; +} + +export const useExploreData = ( + jobConfig: DataFrameAnalyticsConfig | undefined, + selectedFields: EsFieldName[], + setSelectedFields: React.Dispatch> +): UseExploreDataReturnType => { + const [errorMessage, setErrorMessage] = useState(''); + const [status, setStatus] = useState(INDEX_STATUS.UNUSED); + const [tableItems, setTableItems] = useState([]); + const [sortField, setSortField] = useState(''); + const [sortDirection, setSortDirection] = useState(SORT_DIRECTION.ASC); + + const loadExploreData = async ({ field, direction, searchQuery }: LoadExploreDataArg) => { + if (jobConfig !== undefined) { + setErrorMessage(''); + setStatus(INDEX_STATUS.LOADING); + + try { + const resultsField = jobConfig.dest.results_field; + const body: SearchQuery = { + query: searchQuery, + }; + + if (field !== undefined) { + body.sort = [ + { + [field]: { + order: direction, + }, + }, + ]; + } + + const resp: SearchResponse = await ml.esSearch({ + index: jobConfig.dest.index, + size: SEARCH_SIZE, + body, + }); + + setSortField(field); + setSortDirection(direction); + + const docs = resp.hits.hits; + + if (docs.length === 0) { + setTableItems([]); + setStatus(INDEX_STATUS.LOADED); + return; + } + + if (selectedFields.length === 0) { + const newSelectedFields = getDefaultClassificationFields(docs, jobConfig); + setSelectedFields(newSelectedFields); + } + + // Create a version of the doc's source with flattened field names. + // This avoids confusion later on if a field name has dots in its name + // or is a nested fields when displaying it via EuiInMemoryTable. + const flattenedFields = getFlattenedFields(docs[0]._source, resultsField); + const transformedTableItems = docs.map(doc => { + const item: TableItem = {}; + flattenedFields.forEach(ff => { + item[ff] = getNestedProperty(doc._source, ff); + if (item[ff] === undefined) { + // If the attribute is undefined, it means it was not a nested property + // but had dots in its actual name. This selects the property by its + // full name and assigns it to `item[ff]`. + item[ff] = doc._source[`"${ff}"`]; + } + if (item[ff] === undefined) { + const parts = ff.split('.'); + if (parts[0] === resultsField && parts.length >= 2) { + parts.shift(); + if (doc._source[resultsField] !== undefined) { + item[ff] = doc._source[resultsField][parts.join('.')]; + } + } + } + }); + return item; + }); + + setTableItems(transformedTableItems); + setStatus(INDEX_STATUS.LOADED); + } catch (e) { + if (e.message !== undefined) { + setErrorMessage(e.message); + } else { + setErrorMessage(JSON.stringify(e)); + } + setTableItems([]); + setStatus(INDEX_STATUS.ERROR); + } + } + }; + + useEffect(() => { + if (jobConfig !== undefined) { + loadExploreData({ + field: getPredictedFieldName(jobConfig.dest.results_field, jobConfig.analysis), + direction: SORT_DIRECTION.DESC, + searchQuery: defaultSearchQuery, + }); + } + }, [jobConfig && jobConfig.id]); + + return { errorMessage, loadExploreData, sortField, sortDirection, status, tableItems }; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/index.ts new file mode 100644 index 0000000000000..4f86d0d061c97 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ErrorCallout } from './error_callout'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/loading_panel/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/loading_panel/index.ts new file mode 100644 index 0000000000000..40b9e000d6b07 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/loading_panel/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { LoadingPanel } from './loading_panel'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/loading_panel/loading_panel.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/loading_panel/loading_panel.tsx new file mode 100644 index 0000000000000..f71fbc944f0ed --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/loading_panel/loading_panel.tsx @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiLoadingSpinner, EuiPanel } from '@elastic/eui'; + +export const LoadingPanel: FC = () => ( + + + +); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx index d877ed40e587d..a8a015c6ef345 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx @@ -8,40 +8,30 @@ import React, { FC, Fragment, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; -import { ErrorCallout } from './error_callout'; +import { ErrorCallout } from '../error_callout'; import { getValuesFromResponse, getDependentVar, getPredictionFieldName, loadEvalData, + loadDocsCount, Eval, DataFrameAnalyticsConfig, } from '../../../../common'; -import { ml } from '../../../../../services/ml_api_service'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; import { EvaluateStat } from './evaluate_stat'; import { - getEvalQueryBody, - isRegressionResultsSearchBoolQuery, - RegressionResultsSearchQuery, - SearchQuery, + isResultsSearchBoolQuery, + isRegressionEvaluateResponse, + ResultsSearchQuery, + ANALYSIS_CONFIG_TYPE, } from '../../../../common/analytics'; interface Props { jobConfig: DataFrameAnalyticsConfig; jobStatus: DATA_FRAME_TASK_STATE; - searchQuery: RegressionResultsSearchQuery; -} - -interface TrackTotalHitsSearchResponse { - hits: { - total: { - value: number; - relation: string; - }; - hits: any[]; - }; + searchQuery: ResultsSearchQuery; } const defaultEval: Eval = { meanSquaredError: '', rSquared: '', error: null }; @@ -60,40 +50,6 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) // default is 'ml' const resultsField = jobConfig.dest.results_field; - const loadDocsCount = async ({ - ignoreDefaultQuery = true, - isTraining, - }: { - ignoreDefaultQuery?: boolean; - isTraining: boolean; - }): Promise<{ - docsCount: number | null; - success: boolean; - }> => { - const query = getEvalQueryBody({ resultsField, isTraining, ignoreDefaultQuery, searchQuery }); - - try { - const body: SearchQuery = { - track_total_hits: true, - query, - }; - - const resp: TrackTotalHitsSearchResponse = await ml.esSearch({ - index: jobConfig.dest.index, - size: 0, - body, - }); - - const docsCount = resp.hits.total && resp.hits.total.value; - return { docsCount, success: true }; - } catch (e) { - return { - docsCount: null, - success: false, - }; - } - }; - const loadGeneralizationData = async (ignoreDefaultQuery: boolean = true) => { setIsLoadingGeneralization(true); @@ -105,9 +61,14 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) predictionFieldName, searchQuery, ignoreDefaultQuery, + jobType: ANALYSIS_CONFIG_TYPE.REGRESSION, }); - if (genErrorEval.success === true && genErrorEval.eval) { + if ( + genErrorEval.success === true && + genErrorEval.eval && + isRegressionEvaluateResponse(genErrorEval.eval) + ) { const { meanSquaredError, rSquared } = getValuesFromResponse(genErrorEval.eval); setGeneralizationEval({ meanSquaredError, @@ -136,9 +97,14 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) predictionFieldName, searchQuery, ignoreDefaultQuery, + jobType: ANALYSIS_CONFIG_TYPE.REGRESSION, }); - if (trainingErrorEval.success === true && trainingErrorEval.eval) { + if ( + trainingErrorEval.success === true && + trainingErrorEval.eval && + isRegressionEvaluateResponse(trainingErrorEval.eval) + ) { const { meanSquaredError, rSquared } = getValuesFromResponse(trainingErrorEval.eval); setTrainingEval({ meanSquaredError, @@ -165,7 +131,13 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) if (isTrainingClause !== undefined && isTrainingClause.query === 'false') { loadGeneralizationData(); - const docsCountResp = await loadDocsCount({ isTraining: false }); + const docsCountResp = await loadDocsCount({ + isTraining: false, + searchQuery, + resultsField, + destIndex: jobConfig.dest.index, + }); + if (docsCountResp.success === true) { setGeneralizationDocsCount(docsCountResp.docsCount); } else { @@ -182,7 +154,13 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) // searchBar query is filtering for training data loadTrainingData(); - const docsCountResp = await loadDocsCount({ isTraining: true }); + const docsCountResp = await loadDocsCount({ + isTraining: true, + searchQuery, + resultsField, + destIndex: jobConfig.dest.index, + }); + if (docsCountResp.success === true) { setTrainingDocsCount(docsCountResp.docsCount); } else { @@ -201,6 +179,9 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) const genDocsCountResp = await loadDocsCount({ ignoreDefaultQuery: false, isTraining: false, + searchQuery, + resultsField, + destIndex: jobConfig.dest.index, }); if (genDocsCountResp.success === true) { setGeneralizationDocsCount(genDocsCountResp.docsCount); @@ -212,6 +193,9 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) const trainDocsCountResp = await loadDocsCount({ ignoreDefaultQuery: false, isTraining: true, + searchQuery, + resultsField, + destIndex: jobConfig.dest.index, }); if (trainDocsCountResp.success === true) { setTrainingDocsCount(trainDocsCountResp.docsCount); @@ -223,7 +207,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) useEffect(() => { const hasIsTrainingClause = - isRegressionResultsSearchBoolQuery(searchQuery) && + isResultsSearchBoolQuery(searchQuery) && searchQuery.bool.must.filter( (clause: any) => clause.match && clause.match[`${resultsField}.is_training`] !== undefined ); @@ -241,10 +225,13 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) - {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.jobIdTitle', { - defaultMessage: 'Regression job ID {jobId}', - values: { jobId: jobConfig.id }, - })} + {i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.evaluateJobIdTitle', + { + defaultMessage: 'Evaluation of regression job ID {jobId}', + values: { jobId: jobConfig.id }, + } + )} diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx index 2f7ff4feed2a8..12a41e1e7d851 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx @@ -5,31 +5,26 @@ */ import React, { FC, Fragment, useState, useEffect } from 'react'; -import { EuiCallOut, EuiLoadingSpinner, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { EuiCallOut, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ml } from '../../../../../services/ml_api_service'; import { DataFrameAnalyticsConfig } from '../../../../common'; import { EvaluatePanel } from './evaluate_panel'; import { ResultsTable } from './results_table'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { RegressionResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; +import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; +import { LoadingPanel } from '../loading_panel'; interface GetDataFrameAnalyticsResponse { count: number; data_frame_analytics: DataFrameAnalyticsConfig[]; } -const LoadingPanel: FC = () => ( - - - -); - export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( - {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.jobIdTitle', { - defaultMessage: 'Regression job ID {jobId}', + {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle', { + defaultMessage: 'Destination index for regression job ID {jobId}', values: { jobId }, })} @@ -45,7 +40,7 @@ export const RegressionExploration: FC = ({ jobId, jobStatus }) => { const [jobConfig, setJobConfig] = useState(undefined); const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); const loadJobConfig = async () => { setIsLoadingJobConfig(true); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx index 37c2e40c89c3c..1828297365f7a 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx @@ -60,6 +60,8 @@ import { ExplorationTitle } from './regression_exploration'; const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; +const MlInMemoryTableBasic = mlInMemoryTableBasicFactory(); + interface Props { jobConfig: DataFrameAnalyticsConfig; jobStatus: DATA_FRAME_TASK_STATE; @@ -363,8 +365,6 @@ export const ResultsTable: FC = React.memo( ? errorMessage : searchError; - const MlInMemoryTableBasic = mlInMemoryTableBasicFactory(); - return ( diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts index 3a83ad238d0e1..8e9cf45c14ec7 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts @@ -3,11 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ import React, { useEffect, useState } from 'react'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx index b3d13db0a3550..b00a38e2b5f65 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx @@ -24,6 +24,7 @@ import { NavigationMenu } from '../../../components/navigation_menu'; import { Exploration } from './components/exploration'; import { RegressionExploration } from './components/regression_exploration'; +import { ClassificationExploration } from './components/classification_exploration'; import { ANALYSIS_CONFIG_TYPE } from '../../common/analytics'; import { DATA_FRAME_TASK_STATE } from '../analytics_management/components/analytics_list/common'; @@ -72,6 +73,9 @@ export const Page: FC<{ {analysisType === ANALYSIS_CONFIG_TYPE.REGRESSION && ( )} + {analysisType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && ( + + )} diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx index e189c961ccbc9..fc3c00cbcf3e3 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx @@ -17,6 +17,7 @@ import { getAnalysisType, isRegressionAnalysis, isOutlierAnalysis, + isClassificationAnalysis, } from '../../../../common/analytics'; import { getResultsUrl, isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common'; @@ -31,7 +32,9 @@ export const AnalyticsViewAction = { const analysisType = getAnalysisType(item.config.analysis); const jobStatus = item.stats.state; const isDisabled = - !isRegressionAnalysis(item.config.analysis) && !isOutlierAnalysis(item.config.analysis); + !isRegressionAnalysis(item.config.analysis) && + !isOutlierAnalysis(item.config.analysis) && + !isClassificationAnalysis(item.config.analysis); const url = getResultsUrl(item.id, analysisType, jobStatus); return ( diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx index 91b73307ef56c..8772be698bf58 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx @@ -25,7 +25,11 @@ import { Eval, } from '../../../../common'; import { isCompletedAnalyticsJob } from './common'; -import { isRegressionAnalysis } from '../../../../common/analytics'; +import { + isRegressionAnalysis, + ANALYSIS_CONFIG_TYPE, + isRegressionEvaluateResponse, +} from '../../../../common/analytics'; import { ExpandedRowMessagesPane } from './expanded_row_messages_pane'; function getItemDescription(value: any) { @@ -81,9 +85,14 @@ export const ExpandedRow: FC = ({ item }) => { dependentVariable, resultsField, predictionFieldName, + jobType: ANALYSIS_CONFIG_TYPE.REGRESSION, }); - if (genErrorEval.success === true && genErrorEval.eval) { + if ( + genErrorEval.success === true && + genErrorEval.eval && + isRegressionEvaluateResponse(genErrorEval.eval) + ) { const { meanSquaredError, rSquared } = getValuesFromResponse(genErrorEval.eval); setGeneralizationEval({ meanSquaredError, @@ -106,9 +115,14 @@ export const ExpandedRow: FC = ({ item }) => { dependentVariable, resultsField, predictionFieldName, + jobType: ANALYSIS_CONFIG_TYPE.REGRESSION, }); - if (trainingErrorEval.success === true && trainingErrorEval.eval) { + if ( + trainingErrorEval.success === true && + trainingErrorEval.eval && + isRegressionEvaluateResponse(trainingErrorEval.eval) + ) { const { meanSquaredError, rSquared } = getValuesFromResponse(trainingErrorEval.eval); setTrainingEval({ meanSquaredError, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ce7714909f9a2..5c5947de48a06 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7872,7 +7872,6 @@ "xpack.ml.dataframe.analytics.regressionExploration.evaluateError": "データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.generalError": "データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "一般化エラー", - "xpack.ml.dataframe.analytics.regressionExploration.jobIdTitle": "ジョブ ID {jobId}", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText": "平均二乗エラー", "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutBody": "インデックスのクエリが結果を返しませんでした。ジョブが完了済みで、インデックスにドキュメントがあることを確認してください。", "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutTitle": "空のインデックスクエリ結果。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c2c51a4781615..59102702fc9b3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7904,7 +7904,6 @@ "xpack.ml.dataframe.analytics.regressionExploration.evaluateError": "加载数据时出错。", "xpack.ml.dataframe.analytics.regressionExploration.generalError": "加载数据时出错。", "xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "泛化误差", - "xpack.ml.dataframe.analytics.regressionExploration.jobIdTitle": "作业 ID {jobId}", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText": "均方误差", "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutBody": "该索引的查询未返回结果。请确保作业已完成且索引包含文档。", "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutTitle": "空的索引查询结果。", From 3dc8f5a3f409fedf58d37ab04732b49dd740626e Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 12 Dec 2019 13:52:33 -0700 Subject: [PATCH 16/36] [ML] DF Analytics creation: ensure advanced editor can be validated when empty (#52831) (#52942) * Check for undefined analysis config before validating * update tests --- .../use_create_analytics_form/reducer.test.ts | 2 +- .../hooks/use_create_analytics_form/reducer.ts | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts index 7db9420396a9a..754f7a1136a97 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts @@ -20,7 +20,7 @@ type SourceIndex = DataFrameAnalyticsConfig['source']['index']; const getMockState = ({ index, - modelMemoryLimit, + modelMemoryLimit = '100mb', }: { index: SourceIndex; modelMemoryLimit?: string; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index eda80ca64c86f..06c8a6c6a8846 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -56,7 +56,7 @@ const getSourceIndexString = (state: State) => { }; export const validateAdvancedEditor = (state: State): State => { - const { jobIdEmpty, jobIdValid, jobIdExists, createIndexPattern } = state.form; + const { jobIdEmpty, jobIdValid, jobIdExists, jobType, createIndexPattern } = state.form; const { jobConfig } = state; state.advancedEditorMessages = []; @@ -85,14 +85,25 @@ export const validateAdvancedEditor = (state: State): State => { const destinationIndexPatternTitleExists = state.indexPatternsMap[destinationIndexName] !== undefined; const mml = jobConfig.model_memory_limit; - const modelMemoryLimitEmpty = mml === ''; + const modelMemoryLimitEmpty = mml === '' || mml === undefined; if (!modelMemoryLimitEmpty && mml !== undefined) { const { valid } = validateModelMemoryLimitUnits(mml); state.form.modelMemoryLimitUnitValid = valid; } let dependentVariableEmpty = false; - if (isRegressionAnalysis(jobConfig.analysis) || isClassificationAnalysis(jobConfig.analysis)) { + + if ( + jobConfig.analysis === undefined && + (jobType === JOB_TYPES.CLASSIFICATION || jobType === JOB_TYPES.REGRESSION) + ) { + dependentVariableEmpty = true; + } + + if ( + jobConfig.analysis !== undefined && + (isRegressionAnalysis(jobConfig.analysis) || isClassificationAnalysis(jobConfig.analysis)) + ) { const dependentVariableName = getDependentVar(jobConfig.analysis) || ''; dependentVariableEmpty = dependentVariableName === ''; } From fbe8a749f209d3bc68b9439133896c815a85aeab Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 12 Dec 2019 22:22:14 +0100 Subject: [PATCH 17/36] [ML] Color Range Legend component (#52794) (#52935) Introduces ColorRangeLegend, a reusable component to display a color range legend to go along with color coded table cells or visualizations such as heatmaps. --- .../_color_range_legend.scss | 18 ++ .../components/color_range_legend/_index.scss | 1 + .../color_range_legend/color_range_legend.tsx | 153 ++++++++++++++ .../components/color_range_legend/index.ts | 14 ++ .../use_color_range.test.ts | 59 ++++++ .../color_range_legend/use_color_range.ts | 188 ++++++++++++++++++ .../components/exploration/exploration.tsx | 107 +++++----- .../plugins/ml/public/application/index.scss | 1 + 8 files changed, 485 insertions(+), 56 deletions(-) create mode 100644 x-pack/legacy/plugins/ml/public/application/components/color_range_legend/_color_range_legend.scss create mode 100644 x-pack/legacy/plugins/ml/public/application/components/color_range_legend/_index.scss create mode 100644 x-pack/legacy/plugins/ml/public/application/components/color_range_legend/color_range_legend.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/components/color_range_legend/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.test.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.ts diff --git a/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/_color_range_legend.scss b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/_color_range_legend.scss new file mode 100644 index 0000000000000..b164e605a2488 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/_color_range_legend.scss @@ -0,0 +1,18 @@ +/* Overrides for d3/svg default styles */ +.mlColorRangeLegend { + text { + @include fontSize($euiFontSizeXS - 2px); + fill: $euiColorDarkShade; + } + + .axis path { + fill: none; + stroke: none; + } + + .axis line { + fill: none; + stroke: $euiColorMediumShade; + shape-rendering: crispEdges; + } +} diff --git a/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/_index.scss new file mode 100644 index 0000000000000..c7cd3faac0dcf --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/_index.scss @@ -0,0 +1 @@ +@import 'color_range_legend'; diff --git a/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/color_range_legend.tsx b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/color_range_legend.tsx new file mode 100644 index 0000000000000..d6f0d347a57ec --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/color_range_legend.tsx @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useRef, FC } from 'react'; +import d3 from 'd3'; + +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; + +const COLOR_RANGE_RESOLUTION = 10; + +interface ColorRangeLegendProps { + colorRange: (d: number) => string; + justifyTicks?: boolean; + showTicks?: boolean; + title?: string; + width?: number; +} + +/** + * Component to render a legend for color ranges to be used for color coding + * table cells and visualizations. + * + * This current version supports normalized value ranges (0-1) only. + * + * @param props ColorRangeLegendProps + */ +export const ColorRangeLegend: FC = ({ + colorRange, + justifyTicks = false, + showTicks = true, + title, + width = 250, +}) => { + const d3Container = useRef(null); + + const scale = d3.range(COLOR_RANGE_RESOLUTION + 1).map(d => ({ + offset: (d / COLOR_RANGE_RESOLUTION) * 100, + stopColor: colorRange(d / COLOR_RANGE_RESOLUTION), + })); + + useEffect(() => { + if (d3Container.current === null) { + return; + } + + const wrapperHeight = 32; + const wrapperWidth = width; + + // top: 2 — adjust vertical alignment with title text + // bottom: 20 — room for axis ticks and labels + // left/right: 1 — room for first and last axis tick + // when justifyTicks is enabled, the left margin is increased to not cut off the first tick label + const margin = { top: 2, bottom: 20, left: justifyTicks || !showTicks ? 1 : 4, right: 1 }; + + const legendWidth = wrapperWidth - margin.left - margin.right; + const legendHeight = wrapperHeight - margin.top - margin.bottom; + + // remove, then redraw the legend + d3.select(d3Container.current) + .selectAll('*') + .remove(); + + const wrapper = d3 + .select(d3Container.current) + .classed('mlColorRangeLegend', true) + .attr('width', wrapperWidth) + .attr('height', wrapperHeight) + .append('g') + .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + + // append gradient bar + const gradient = wrapper + .append('defs') + .append('linearGradient') + .attr('id', 'mlColorRangeGradient') + .attr('x1', '0%') + .attr('y1', '0%') + .attr('x2', '100%') + .attr('y2', '0%') + .attr('spreadMethod', 'pad'); + + scale.forEach(function(d) { + gradient + .append('stop') + .attr('offset', `${d.offset}%`) + .attr('stop-color', d.stopColor) + .attr('stop-opacity', 1); + }); + + wrapper + .append('rect') + .attr('x1', 0) + .attr('y1', 0) + .attr('width', legendWidth) + .attr('height', legendHeight) + .style('fill', 'url(#mlColorRangeGradient)'); + + const axisScale = d3.scale + .linear() + .domain([0, 1]) + .range([0, legendWidth]); + + // Using this formatter ensures we get e.g. `0` and not `0.0`, but still `0.1`, `0.2` etc. + const tickFormat = d3.format(''); + const legendAxis = d3.svg + .axis() + .scale(axisScale) + .orient('bottom') + .tickFormat(tickFormat) + .tickSize(legendHeight + 4) + .ticks(legendWidth / 40); + + wrapper + .append('g') + .attr('class', 'legend axis') + .attr('transform', 'translate(0, 0)') + .call(legendAxis); + + // Adjust the alignment of the first and last tick text + // so that the tick labels don't overflow the color range. + if (justifyTicks || !showTicks) { + const text = wrapper.selectAll('text')[0]; + if (text.length > 1) { + d3.select(text[0]).style('text-anchor', 'start'); + d3.select(text[text.length - 1]).style('text-anchor', 'end'); + } + } + + if (!showTicks) { + wrapper.selectAll('.axis line').style('display', 'none'); + } + }, [JSON.stringify(scale), d3Container.current]); + + if (title === undefined) { + return ; + } + + return ( + + + + {title} + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/index.ts b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/index.ts new file mode 100644 index 0000000000000..93a1ec40f1d5e --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ColorRangeLegend } from './color_range_legend'; +export { + colorRangeOptions, + colorRangeScaleOptions, + useColorRange, + COLOR_RANGE, + COLOR_RANGE_SCALE, +} from './use_color_range'; diff --git a/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.test.ts b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.test.ts new file mode 100644 index 0000000000000..f047ae800266b --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { influencerColorScaleFactory } from './use_color_range'; + +jest.mock('../../contexts/ui/use_ui_chrome_context'); + +describe('useColorRange', () => { + test('influencerColorScaleFactory(1)', () => { + const influencerColorScale = influencerColorScaleFactory(1); + + expect(influencerColorScale(0)).toBe(0); + expect(influencerColorScale(0.1)).toBe(0.1); + expect(influencerColorScale(0.2)).toBe(0.2); + expect(influencerColorScale(0.3)).toBe(0.3); + expect(influencerColorScale(0.4)).toBe(0.4); + expect(influencerColorScale(0.5)).toBe(0.5); + expect(influencerColorScale(0.6)).toBe(0.6); + expect(influencerColorScale(0.7)).toBe(0.7); + expect(influencerColorScale(0.8)).toBe(0.8); + expect(influencerColorScale(0.9)).toBe(0.9); + expect(influencerColorScale(1)).toBe(1); + }); + + test('influencerColorScaleFactory(2)', () => { + const influencerColorScale = influencerColorScaleFactory(2); + + expect(influencerColorScale(0)).toBe(0); + expect(influencerColorScale(0.1)).toBe(0); + expect(influencerColorScale(0.2)).toBe(0); + expect(influencerColorScale(0.3)).toBe(0); + expect(influencerColorScale(0.4)).toBe(0); + expect(influencerColorScale(0.5)).toBe(0); + expect(influencerColorScale(0.6)).toBe(0.04999999999999999); + expect(influencerColorScale(0.7)).toBe(0.09999999999999998); + expect(influencerColorScale(0.8)).toBe(0.15000000000000002); + expect(influencerColorScale(0.9)).toBe(0.2); + expect(influencerColorScale(1)).toBe(0.25); + }); + + test('influencerColorScaleFactory(3)', () => { + const influencerColorScale = influencerColorScaleFactory(3); + + expect(influencerColorScale(0)).toBe(0); + expect(influencerColorScale(0.1)).toBe(0); + expect(influencerColorScale(0.2)).toBe(0); + expect(influencerColorScale(0.3)).toBe(0); + expect(influencerColorScale(0.4)).toBe(0.05000000000000003); + expect(influencerColorScale(0.5)).toBe(0.125); + expect(influencerColorScale(0.6)).toBe(0.2); + expect(influencerColorScale(0.7)).toBe(0.27499999999999997); + expect(influencerColorScale(0.8)).toBe(0.35000000000000003); + expect(influencerColorScale(0.9)).toBe(0.425); + expect(influencerColorScale(1)).toBe(0.5); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.ts b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.ts new file mode 100644 index 0000000000000..f9c5e6ff81f9e --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import d3 from 'd3'; + +import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; +import euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json'; + +import { i18n } from '@kbn/i18n'; + +import { useUiChromeContext } from '../../contexts/ui/use_ui_chrome_context'; + +/** + * Custom color scale factory that takes the amount of feature influencers + * into account to adjust the contrast of the color range. This is used for + * color coding for outlier detection where the amount of feature influencers + * affects the threshold from which the influencers value can actually be + * considered influential. + * + * @param n number of influencers + * @returns a function suitable as a preprocessor for d3.scale.linear() + */ +export const influencerColorScaleFactory = (n: number) => (t: number) => { + // for 1 influencer or less we fall back to a plain linear scale. + if (n <= 1) { + return t; + } + + if (t < 1 / n) { + return 0; + } + if (t < 3 / n) { + return (n / 4) * (t - 1 / n); + } + return 0.5 + (t - 3 / n); +}; + +export enum COLOR_RANGE_SCALE { + LINEAR = 'linear', + INFLUENCER = 'influencer', + SQRT = 'sqrt', +} + +/** + * Color range scale options in the format for EuiSelect's options prop. + */ +export const colorRangeScaleOptions = [ + { + value: COLOR_RANGE_SCALE.LINEAR, + text: i18n.translate('xpack.ml.components.colorRangeLegend.linearScaleLabel', { + defaultMessage: 'Linear', + }), + }, + { + value: COLOR_RANGE_SCALE.INFLUENCER, + text: i18n.translate('xpack.ml.components.colorRangeLegend.influencerScaleLabel', { + defaultMessage: 'Influencer custom scale', + }), + }, + { + value: COLOR_RANGE_SCALE.SQRT, + text: i18n.translate('xpack.ml.components.colorRangeLegend.sqrtScaleLabel', { + defaultMessage: 'Sqrt', + }), + }, +]; + +export enum COLOR_RANGE { + BLUE = 'blue', + RED = 'red', + RED_GREEN = 'red-green', + GREEN_RED = 'green-red', + YELLOW_GREEN_BLUE = 'yellow-green-blue', +} + +/** + * Color range options in the format for EuiSelect's options prop. + */ +export const colorRangeOptions = [ + { + value: COLOR_RANGE.BLUE, + text: i18n.translate('xpack.ml.components.colorRangeLegend.blueColorRangeLabel', { + defaultMessage: 'Blue', + }), + }, + { + value: COLOR_RANGE.RED, + text: i18n.translate('xpack.ml.components.colorRangeLegend.redColorRangeLabel', { + defaultMessage: 'Red', + }), + }, + { + value: COLOR_RANGE.RED_GREEN, + text: i18n.translate('xpack.ml.components.colorRangeLegend.redGreenColorRangeLabel', { + defaultMessage: 'Red - Green', + }), + }, + { + value: COLOR_RANGE.GREEN_RED, + text: i18n.translate('xpack.ml.components.colorRangeLegend.greenRedColorRangeLabel', { + defaultMessage: 'Green - Red', + }), + }, + { + value: COLOR_RANGE.YELLOW_GREEN_BLUE, + text: i18n.translate('xpack.ml.components.colorRangeLegend.yellowGreenBlueColorRangeLabel', { + defaultMessage: 'Yellow - Green - Blue', + }), + }, +]; + +/** + * A custom Yellow-Green-Blue color range to demonstrate the support + * for more complex ranges with more than two colors. + */ +const coloursYGB = [ + '#FFFFDD', + '#AAF191', + '#80D385', + '#61B385', + '#3E9583', + '#217681', + '#285285', + '#1F2D86', + '#000086', +]; +const colourRangeYGB = d3.range(0, 1, 1.0 / (coloursYGB.length - 1)); +colourRangeYGB.push(1); + +const colorDomains = { + [COLOR_RANGE.BLUE]: [0, 1], + [COLOR_RANGE.RED]: [0, 1], + [COLOR_RANGE.RED_GREEN]: [0, 1], + [COLOR_RANGE.GREEN_RED]: [0, 1], + [COLOR_RANGE.YELLOW_GREEN_BLUE]: colourRangeYGB, +}; + +/** + * Custom hook to get a d3 based color range to be used for color coding in table cells. + * + * @param colorRange COLOR_RANGE enum. + * @param colorRangeScale COLOR_RANGE_SCALE enum. + * @param featureCount + */ +export const useColorRange = ( + colorRange = COLOR_RANGE.BLUE, + colorRangeScale = COLOR_RANGE_SCALE.LINEAR, + featureCount = 1 +) => { + const euiTheme = useUiChromeContext() + .getUiSettingsClient() + .get('theme:darkMode') + ? euiThemeDark + : euiThemeLight; + + const colorRanges = { + [COLOR_RANGE.BLUE]: [d3.rgb(euiTheme.euiColorEmptyShade), d3.rgb(euiTheme.euiColorVis1)], + [COLOR_RANGE.RED]: [d3.rgb(euiTheme.euiColorEmptyShade), d3.rgb(euiTheme.euiColorDanger)], + [COLOR_RANGE.RED_GREEN]: ['red', 'green'], + [COLOR_RANGE.GREEN_RED]: ['green', 'red'], + [COLOR_RANGE.YELLOW_GREEN_BLUE]: coloursYGB, + }; + + const linearScale = d3.scale + .linear() + .domain(colorDomains[colorRange]) + // typings for .range() incorrectly don't allow passing in a color extent. + // @ts-ignore + .range(colorRanges[colorRange]); + const influencerColorScale = influencerColorScaleFactory(featureCount); + const influencerScaleLinearWrapper = (n: number) => linearScale(influencerColorScale(n)); + + const scaleTypes = { + [COLOR_RANGE_SCALE.LINEAR]: linearScale, + [COLOR_RANGE_SCALE.INFLUENCER]: influencerScaleLinearWrapper, + [COLOR_RANGE_SCALE.SQRT]: d3.scale + .sqrt() + .domain(colorDomains[colorRange]) + // typings for .range() incorrectly don't allow passing in a color extent. + // @ts-ignore + .range(colorRanges[colorRange]), + }; + + return scaleTypes[colorRangeScale]; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx index c4bba08353d84..31e6d409b1c4f 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx @@ -4,13 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useEffect, useState } from 'react'; +import React, { FC, useEffect, useState } from 'react'; import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; -import d3 from 'd3'; - import { EuiBadge, EuiButtonIcon, @@ -18,7 +16,6 @@ import { EuiCheckbox, EuiFlexGroup, EuiFlexItem, - EuiFormRow, EuiPanel, EuiPopover, EuiPopoverTitle, @@ -30,9 +27,12 @@ import { Query, } from '@elastic/eui'; -import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; -import euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json'; - +import { + useColorRange, + ColorRangeLegend, + COLOR_RANGE, + COLOR_RANGE_SCALE, +} from '../../../../../components/color_range_legend'; import { ColumnType, mlInMemoryTableBasicFactory, @@ -41,8 +41,6 @@ import { SORT_DIRECTION, } from '../../../../../components/ml_in_memory_table'; -import { useUiChromeContext } from '../../../../../contexts/ui/use_ui_chrome_context'; - import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils'; import { ml } from '../../../../../services/ml_api_service'; @@ -67,16 +65,6 @@ import { import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; import { SavedSearchQuery } from '../../../../../contexts/kibana'; -const customColorScaleFactory = (n: number) => (t: number) => { - if (t < 1 / n) { - return 0; - } - if (t < 3 / n) { - return (n / 4) * (t - 1 / n); - } - return 0.5 + (t - 3 / n); -}; - const FEATURE_INFLUENCE = 'feature_influence'; interface GetDataFrameAnalyticsResponse { @@ -102,6 +90,16 @@ interface Props { jobStatus: DATA_FRAME_TASK_STATE; } +const getFeatureCount = (jobConfig?: DataFrameAnalyticsConfig, tableItems: TableItem[] = []) => { + if (jobConfig === undefined || tableItems.length === 0) { + return 0; + } + + return Object.keys(tableItems[0]).filter(key => + key.includes(`${jobConfig.dest.results_field}.${FEATURE_INFLUENCE}.`) + ).length; +}; + export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { const [jobConfig, setJobConfig] = useState(undefined); @@ -126,12 +124,6 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { })(); }, []); - const euiTheme = useUiChromeContext() - .getUiSettingsClient() - .get('theme:darkMode') - ? euiThemeDark - : euiThemeLight; - const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false); @@ -169,23 +161,13 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { const columns: Array> = []; - if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) { - // table cell color coding takes into account: - // - whether the theme is dark/light - // - the number of analysis features - // based on that - const cellBgColorScale = d3.scale - .linear() - .domain([0, 1]) - // typings for .range() incorrectly don't allow passing in a color extent. - // @ts-ignore - .range([d3.rgb(euiTheme.euiColorEmptyShade), d3.rgb(euiTheme.euiColorVis1)]); - const featureCount = Object.keys(tableItems[0]).filter(key => - key.includes(`${jobConfig.dest.results_field}.${FEATURE_INFLUENCE}.`) - ).length; - const customScale = customColorScaleFactory(featureCount); - const cellBgColor = (n: number) => cellBgColorScale(customScale(n)); + const cellBgColor = useColorRange( + COLOR_RANGE.BLUE, + COLOR_RANGE_SCALE.INFLUENCER, + getFeatureCount(jobConfig, tableItems) + ); + if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) { columns.push( ...selectedFields.sort(sortColumns(tableItems[0], jobConfig.dest.results_field)).map(k => { const column: ColumnType = { @@ -504,21 +486,34 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { )} {(columns.length > 0 || searchQuery !== defaultSearchQuery) && sortField !== '' && ( - - {tableItems.length === SEARCH_SIZE && ( - + + + + {tableItems.length === SEARCH_SIZE && ( + + {i18n.translate( + 'xpack.ml.dataframe.analytics.exploration.documentsShownHelpText', + { + defaultMessage: 'Showing first {searchSize} documents', + values: { searchSize: SEARCH_SIZE }, + } + )} + )} - > - - - )} - + + + + + = React.memo(({ jobId, jobStatus }) => { search={search} error={tableError} /> - + )} ); diff --git a/x-pack/legacy/plugins/ml/public/application/index.scss b/x-pack/legacy/plugins/ml/public/application/index.scss index dbcdf288b6a85..ecef2bbf9a597 100644 --- a/x-pack/legacy/plugins/ml/public/application/index.scss +++ b/x-pack/legacy/plugins/ml/public/application/index.scss @@ -28,6 +28,7 @@ @import 'components/annotations/annotation_description_list/index'; // SASSTODO: This file overwrites EUI directly @import 'components/anomalies_table/index'; // SASSTODO: This file overwrites EUI directly @import 'components/chart_tooltip/index'; + @import 'components/color_range_legend/index'; @import 'components/controls/index'; @import 'components/entity_cell/index'; @import 'components/field_title_bar/index'; From 5263bb5c71bc7bfbc84d97db3e649c1d673bb294 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Thu, 12 Dec 2019 22:22:27 +0100 Subject: [PATCH 18/36] Deangularize and typescriptify ui/saved_object (#51562) (#52915) --- .../__tests__/get_saved_dashboard_mock.ts | 2 +- .../dashboard/dashboard_app_controller.tsx | 4 +- .../kibana/public/dashboard/legacy_imports.ts | 2 +- .../public/dashboard/lib/save_dashboard.ts | 4 +- .../saved_dashboard/saved_dashboard.d.ts | 2 +- .../saved_dashboard/saved_dashboards.js | 5 +- .../__tests__/directives/discover_field.js | 2 +- .../__tests__/directives/field_chooser.js | 2 +- .../__tests__/action_add_filter.js | 2 +- .../__tests__/action_set_predecessor_count.js | 2 +- .../__tests__/action_set_query_parameters.js | 2 +- .../__tests__/action_set_successor_count.js | 2 +- .../doc_table/__tests__/lib/rows_headers.js | 2 +- .../public/discover/helpers/build_services.ts | 37 +- .../kibana/public/discover/plugin.ts | 4 +- .../discover/saved_searches/_saved_search.js | 72 --- .../discover/saved_searches/_saved_search.ts | 71 +++ .../saved_searches/{index.js => index.ts} | 0 .../{saved_searches.js => saved_searches.ts} | 35 +- .../embeddable/visualize_embeddable.ts | 2 +- .../saved_visualizations/_saved_vis.js | 24 +- .../saved_visualizations.js | 10 +- .../timelion/public/services/saved_sheets.js | 5 +- .../__tests__/find_object_by_title.js | 2 +- .../saved_objects/__tests__/saved_object.js | 78 +-- .../{saved_object.d.ts => constants.ts} | 32 +- .../saved_objects/helpers/apply_es_resp.ts | 82 +++ .../helpers/build_saved_object.ts | 126 ++++ .../helpers/check_for_duplicate_title.ts | 69 +++ .../helpers/confirm_modal_promise.tsx | 59 ++ .../saved_objects/helpers/create_source.ts | 85 +++ .../display_duplicate_title_confirm_modal.ts | 49 ++ .../{ => helpers}/find_object_by_title.ts | 28 +- .../helpers/hydrate_index_pattern.ts | 55 ++ .../helpers/initialize_saved_object.ts | 59 ++ .../helpers/parse_search_source.ts | 97 ++++ .../helpers/save_saved_object.ts | 128 +++++ .../helpers/serialize_saved_object.ts | 98 ++++ src/legacy/ui/public/saved_objects/index.ts | 3 +- .../ui/public/saved_objects/saved_object.js | 541 ------------------ .../ui/public/saved_objects/saved_object.ts | 63 ++ ...bject_loader.js => saved_object_loader.ts} | 88 +-- src/legacy/ui/public/saved_objects/types.ts | 90 +++ .../plugins/graph/public/types/persistence.ts | 2 +- .../services/gis_map_saved_object_loader.js | 8 +- 45 files changed, 1297 insertions(+), 838 deletions(-) delete mode 100644 src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.js create mode 100644 src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.ts rename src/legacy/core_plugins/kibana/public/discover/saved_searches/{index.js => index.ts} (100%) rename src/legacy/core_plugins/kibana/public/discover/saved_searches/{saved_searches.js => saved_searches.ts} (60%) rename src/legacy/ui/public/saved_objects/{saved_object.d.ts => constants.ts} (55%) create mode 100644 src/legacy/ui/public/saved_objects/helpers/apply_es_resp.ts create mode 100644 src/legacy/ui/public/saved_objects/helpers/build_saved_object.ts create mode 100644 src/legacy/ui/public/saved_objects/helpers/check_for_duplicate_title.ts create mode 100644 src/legacy/ui/public/saved_objects/helpers/confirm_modal_promise.tsx create mode 100644 src/legacy/ui/public/saved_objects/helpers/create_source.ts create mode 100644 src/legacy/ui/public/saved_objects/helpers/display_duplicate_title_confirm_modal.ts rename src/legacy/ui/public/saved_objects/{ => helpers}/find_object_by_title.ts (75%) create mode 100644 src/legacy/ui/public/saved_objects/helpers/hydrate_index_pattern.ts create mode 100644 src/legacy/ui/public/saved_objects/helpers/initialize_saved_object.ts create mode 100644 src/legacy/ui/public/saved_objects/helpers/parse_search_source.ts create mode 100644 src/legacy/ui/public/saved_objects/helpers/save_saved_object.ts create mode 100644 src/legacy/ui/public/saved_objects/helpers/serialize_saved_object.ts delete mode 100644 src/legacy/ui/public/saved_objects/saved_object.js create mode 100644 src/legacy/ui/public/saved_objects/saved_object.ts rename src/legacy/ui/public/saved_objects/{saved_object_loader.js => saved_object_loader.ts} (60%) create mode 100644 src/legacy/ui/public/saved_objects/types.ts diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.ts b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.ts index 01468eadffb84..bf7135098ea74 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.ts @@ -41,5 +41,5 @@ export function getSavedDashboardMock( getQuery: () => ({ query: '', language: 'kuery' }), getFilters: () => [], ...config, - }; + } as SavedObjectDashboard; } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx index fd49b26e0d948..47f0120da501e 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx @@ -35,7 +35,7 @@ import { State, AppStateClass as TAppStateClass, KbnUrl, - SaveOptions, + SavedObjectSaveOpts, unhashUrl, } from './legacy_imports'; import { FilterStateManager, IndexPattern } from '../../../data/public'; @@ -603,7 +603,7 @@ export class DashboardAppController { * @return {Promise} * @resolved {String} - The id of the doc */ - function save(saveOptions: SaveOptions): Promise { + function save(saveOptions: SavedObjectSaveOpts): Promise { return saveDashboard(angular.toJson, timefilter, dashboardStateManager, saveOptions) .then(function(id) { if (id) { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts index af0a833399a52..adae063a1470b 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts @@ -30,7 +30,7 @@ export const legacyChrome = chrome; export { State } from 'ui/state_management/state'; export { AppState } from 'ui/state_management/app_state'; export { AppStateClass } from 'ui/state_management/app_state'; -export { SaveOptions } from 'ui/saved_objects/saved_object'; +export { SavedObjectSaveOpts } from 'ui/saved_objects/types'; export { npSetup, npStart } from 'ui/new_platform'; export { SavedObjectRegistryProvider } from 'ui/saved_objects'; export { IPrivate } from 'ui/private'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/save_dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/save_dashboard.ts index e0d82373d3ad9..e322677433ce6 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/lib/save_dashboard.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/save_dashboard.ts @@ -18,7 +18,7 @@ */ import { TimefilterContract } from 'src/plugins/data/public'; -import { SaveOptions } from '../legacy_imports'; +import { SavedObjectSaveOpts } from '../legacy_imports'; import { updateSavedDashboard } from './update_saved_dashboard'; import { DashboardStateManager } from '../dashboard_state_manager'; @@ -34,7 +34,7 @@ export function saveDashboard( toJson: (obj: any) => string, timeFilter: TimefilterContract, dashboardStateManager: DashboardStateManager, - saveOptions: SaveOptions + saveOptions: SavedObjectSaveOpts ): Promise { dashboardStateManager.saveState(); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts index 4c417ed2954d3..20544fa97fdb0 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SavedObject } from 'ui/saved_objects/saved_object'; +import { SavedObject } from 'ui/saved_objects/types'; import { SearchSourceContract } from '../../../../../ui/public/courier'; import { esFilters, Query, RefreshInterval } from '../../../../../../plugins/data/public'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.js b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.js index 9b7d036590f1d..6b561c18a5d42 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.js @@ -22,6 +22,7 @@ import './saved_dashboard'; import { uiModules } from 'ui/modules'; import { SavedObjectLoader, SavedObjectsClientProvider } from 'ui/saved_objects'; import { savedObjectManagementRegistry } from '../../management/saved_object_registry'; +import { npStart } from '../../../../../ui/public/new_platform'; const module = uiModules.get('app/dashboard'); @@ -35,7 +36,7 @@ savedObjectManagementRegistry.register({ }); // This is the only thing that gets injected into controllers -module.service('savedDashboards', function (Private, SavedDashboard, kbnUrl, chrome) { +module.service('savedDashboards', function (Private, SavedDashboard) { const savedObjectClient = Private(SavedObjectsClientProvider); - return new SavedObjectLoader(SavedDashboard, kbnUrl, chrome, savedObjectClient); + return new SavedObjectLoader(SavedDashboard, savedObjectClient, npStart.core.chrome); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js index 92df04c536e43..5c6ba86455588 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js @@ -32,7 +32,7 @@ describe('discoverField', function () { let $scope; let indexPattern; let $elem; - beforeEach(() => pluginInstance.initializeServices(true)); + beforeEach(() => pluginInstance.initializeServices()); beforeEach(() => pluginInstance.initializeInnerAngular()); beforeEach(ngMock.module('app/discover')); beforeEach(ngMock.inject(function (Private, $rootScope, $compile) { diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js index 34c6483349af6..a707402ff92b0 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js @@ -70,7 +70,7 @@ describe('discover field chooser directives', function () { on-remove-field="removeField" > `); - beforeEach(() => pluginInstance.initializeServices(true)); + beforeEach(() => pluginInstance.initializeServices()); beforeEach(() => pluginInstance.initializeInnerAngular()); beforeEach(ngMock.module('app/discover', ($provide) => { diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_add_filter.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_add_filter.js index 645ca32924ede..aa3c260eed52d 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_add_filter.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_add_filter.js @@ -27,7 +27,7 @@ import { npStart } from 'ui/new_platform'; describe('context app', function () { beforeEach(() => pluginInstance.initializeInnerAngular()); - beforeEach(() => pluginInstance.initializeServices(true)); + beforeEach(() => pluginInstance.initializeServices()); beforeEach(ngMock.module('app/discover')); beforeEach(ngMock.module(function createServiceStubs($provide) { $provide.value('indexPatterns', createIndexPatternsStub()); diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_predecessor_count.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_predecessor_count.js index a8bef6fe75c79..e1821eada79ed 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_predecessor_count.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_predecessor_count.js @@ -26,7 +26,7 @@ import { getQueryParameterActions } from '../actions'; describe('context app', function () { beforeEach(() => pluginInstance.initializeInnerAngular()); - beforeEach(() => pluginInstance.initializeServices(true)); + beforeEach(() => pluginInstance.initializeServices()); beforeEach(ngMock.module('app/discover')); describe('action setPredecessorCount', function () { diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_query_parameters.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_query_parameters.js index a43a8a11a7bf8..348ea2c55790a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_query_parameters.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_query_parameters.js @@ -27,7 +27,7 @@ import { getQueryParameterActions } from '../actions'; describe('context app', function () { beforeEach(() => pluginInstance.initializeInnerAngular()); - beforeEach(() => pluginInstance.initializeServices(true)); + beforeEach(() => pluginInstance.initializeServices()); beforeEach(ngMock.module('app/discover')); describe('action setQueryParameters', function () { diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_successor_count.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_successor_count.js index 4bbd462aaa4b0..c03158722347b 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_successor_count.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_successor_count.js @@ -27,7 +27,7 @@ import { getQueryParameterActions } from '../actions'; describe('context app', function () { beforeEach(() => pluginInstance.initializeInnerAngular()); - beforeEach(() => pluginInstance.initializeServices(true)); + beforeEach(() => pluginInstance.initializeServices()); beforeEach(ngMock.module('app/discover')); describe('action setSuccessorCount', function () { diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/lib/rows_headers.js b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/lib/rows_headers.js index 666261610fa7c..c8e8e216d0dea 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/lib/rows_headers.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/lib/rows_headers.js @@ -37,7 +37,7 @@ describe('Doc Table', function () { let fakeRowVals; let stubFieldFormatConverter; - beforeEach(() => pluginInstance.initializeServices(true)); + beforeEach(() => pluginInstance.initializeServices()); beforeEach(() => pluginInstance.initializeInnerAngular()); beforeEach(ngMock.module('app/discover')); beforeEach( diff --git a/src/legacy/core_plugins/kibana/public/discover/helpers/build_services.ts b/src/legacy/core_plugins/kibana/public/discover/helpers/build_services.ts index b72bd27a31cf9..14ea4e99b0de4 100644 --- a/src/legacy/core_plugins/kibana/public/discover/helpers/build_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/helpers/build_services.ts @@ -25,14 +25,12 @@ import { IUiSettingsClient, } from 'kibana/public'; import * as docViewsRegistry from 'ui/registry/doc_views'; -import chromeLegacy from 'ui/chrome'; -import { IPrivate } from 'ui/private'; import { FilterManager, TimefilterContract, IndexPatternsContract } from 'src/plugins/data/public'; // @ts-ignore import { createSavedSearchesService } from '../saved_searches/saved_searches'; // @ts-ignore -import { createSavedSearchFactory } from '../saved_searches/_saved_search'; import { DiscoverStartPlugins } from '../plugin'; +import { DataStart } from '../../../../data/public'; import { EuiUtilsStart } from '../../../../../../plugins/eui_utils/public'; import { SavedSearch } from '../types'; import { SharePluginStart } from '../../../../../../plugins/share/public'; @@ -42,6 +40,7 @@ export interface DiscoverServices { capabilities: Capabilities; chrome: ChromeStart; core: CoreStart; + data: DataStart; docLinks: DocLinksStart; docViewsRegistry: docViewsRegistry.DocViewsRegistry; eui_utils: EuiUtilsStart; @@ -52,35 +51,19 @@ export interface DiscoverServices { share: SharePluginStart; timefilter: TimefilterContract; toastNotifications: ToastsStart; - // legacy getSavedSearchById: (id: string) => Promise; getSavedSearchUrlById: (id: string) => Promise; uiSettings: IUiSettingsClient; } - -export async function buildGlobalAngularServices() { - const injector = await chromeLegacy.dangerouslyGetActiveInjector(); - const Private = injector.get('Private'); - const kbnUrl = injector.get('kbnUrl'); - const SavedSearchFactory = createSavedSearchFactory(Private); - const service = createSavedSearchesService(Private, SavedSearchFactory, kbnUrl, chromeLegacy); - return { - getSavedSearchById: async (id: string) => service.get(id), - getSavedSearchUrlById: async (id: string) => service.urlFor(id), +export async function buildServices(core: CoreStart, plugins: DiscoverStartPlugins) { + const services = { + savedObjectsClient: core.savedObjects.client, + indexPatterns: plugins.data.indexPatterns, + chrome: core.chrome, + overlays: core.overlays, }; -} - -export async function buildServices(core: CoreStart, plugins: DiscoverStartPlugins, test: false) { - const globalAngularServices = !test - ? await buildGlobalAngularServices() - : { - getSavedSearchById: async (id: string) => void id, - getSavedSearchUrlById: async (id: string) => void id, - State: null, - }; - + const savedObjectService = createSavedSearchesService(services); return { - ...globalAngularServices, addBasePath: core.http.basePath.prepend, capabilities: core.application.capabilities, chrome: core.chrome, @@ -90,6 +73,8 @@ export async function buildServices(core: CoreStart, plugins: DiscoverStartPlugi docViewsRegistry, eui_utils: plugins.eui_utils, filterManager: plugins.data.query.filterManager, + getSavedSearchById: async (id: string) => savedObjectService.get(id), + getSavedSearchUrlById: async (id: string) => savedObjectService.urlFor(id), indexPatterns: plugins.data.indexPatterns, inspector: plugins.inspector, // @ts-ignore diff --git a/src/legacy/core_plugins/kibana/public/discover/plugin.ts b/src/legacy/core_plugins/kibana/public/discover/plugin.ts index 6679e8a3b8826..a5a1ead93188a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/discover/plugin.ts @@ -106,11 +106,11 @@ export class DiscoverPlugin implements Plugin { this.innerAngularInitialized = true; }; - this.initializeServices = async (test = false) => { + this.initializeServices = async () => { if (this.servicesInitialized) { return; } - const services = await buildServices(core, plugins, test); + const services = await buildServices(core, plugins); setServices(services); this.servicesInitialized = true; }; diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.js b/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.js deleted file mode 100644 index db2b2b5b22af7..0000000000000 --- a/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.js +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { createLegacyClass } from 'ui/utils/legacy_class'; -import { SavedObjectProvider } from 'ui/saved_objects/saved_object'; - -import { uiModules } from 'ui/modules'; - -const module = uiModules.get('discover/saved_searches', []); - -export function createSavedSearchFactory(Private) { - const SavedObject = Private(SavedObjectProvider); - createLegacyClass(SavedSearch).inherits(SavedObject); - function SavedSearch(id) { - SavedObject.call(this, { - type: SavedSearch.type, - mapping: SavedSearch.mapping, - searchSource: SavedSearch.searchSource, - id: id, - defaults: { - title: '', - description: '', - columns: [], - hits: 0, - sort: [], - version: 1, - }, - }); - - this.showInRecentlyAccessed = true; - } - - SavedSearch.type = 'search'; - - SavedSearch.mapping = { - title: 'text', - description: 'text', - hits: 'integer', - columns: 'keyword', - sort: 'keyword', - version: 'integer', - }; - - // Order these fields to the top, the rest are alphabetical - SavedSearch.fieldOrder = ['title', 'description']; - - SavedSearch.searchSource = true; - - SavedSearch.prototype.getFullPath = function () { - return `/app/kibana#/discover/${this.id}`; - }; - - return SavedSearch; -} - -module.factory('SavedSearch', createSavedSearchFactory); diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.ts b/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.ts new file mode 100644 index 0000000000000..113d13287bd12 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { SavedObjectKibanaServices } from 'ui/saved_objects/types'; +import { createSavedObjectClass } from 'ui/saved_objects/saved_object'; + +export function createSavedSearchClass(services: SavedObjectKibanaServices) { + const SavedObjectClass = createSavedObjectClass(services); + + class SavedSearch extends SavedObjectClass { + public static type: string = 'search'; + public static mapping = { + title: 'text', + description: 'text', + hits: 'integer', + columns: 'keyword', + sort: 'keyword', + version: 'integer', + }; + // Order these fields to the top, the rest are alphabetical + public static fieldOrder = ['title', 'description']; + public static searchSource = true; + + public id: string; + public showInRecentlyAccessed: boolean; + + constructor(id: string) { + super({ + id, + type: 'search', + mapping: { + title: 'text', + description: 'text', + hits: 'integer', + columns: 'keyword', + sort: 'keyword', + version: 'integer', + }, + searchSource: true, + defaults: { + title: '', + description: '', + columns: [], + hits: 0, + sort: [], + version: 1, + }, + }); + this.showInRecentlyAccessed = true; + this.id = id; + this.getFullPath = () => `/app/kibana#/discover/${String(id)}`; + } + } + + return SavedSearch; +} diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/index.js b/src/legacy/core_plugins/kibana/public/discover/saved_searches/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/saved_searches/index.js rename to src/legacy/core_plugins/kibana/public/discover/saved_searches/index.ts diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.js b/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts similarity index 60% rename from src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.js rename to src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts index 7ebcba903cc7f..46fdd3a7baedc 100644 --- a/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.js +++ b/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts @@ -16,12 +16,14 @@ * specific language governing permissions and limitations * under the License. */ - -import './_saved_search'; +import { npStart } from 'ui/new_platform'; +// @ts-ignore import { uiModules } from 'ui/modules'; -import { SavedObjectLoader, SavedObjectsClientProvider } from 'ui/saved_objects'; +import { SavedObjectLoader } from 'ui/saved_objects'; +import { SavedObjectKibanaServices } from 'ui/saved_objects/types'; +// @ts-ignore import { savedObjectManagementRegistry } from '../../management/saved_object_registry'; - +import { createSavedSearchClass } from './_saved_search'; // Register this service with the saved object registry so it can be // edited by the object editor. @@ -30,9 +32,13 @@ savedObjectManagementRegistry.register({ title: 'searches', }); -export function createSavedSearchesService(Private, SavedSearch, kbnUrl, chrome) { - const savedObjectClient = Private(SavedObjectsClientProvider); - const savedSearchLoader = new SavedObjectLoader(SavedSearch, kbnUrl, chrome, savedObjectClient); +export function createSavedSearchesService(services: SavedObjectKibanaServices) { + const SavedSearchClass = createSavedSearchClass(services); + const savedSearchLoader = new SavedObjectLoader( + SavedSearchClass, + services.savedObjectsClient, + services.chrome + ); // Customize loader properties since adding an 's' on type doesn't work for type 'search' . savedSearchLoader.loaderProperties = { name: 'searches', @@ -40,11 +46,18 @@ export function createSavedSearchesService(Private, SavedSearch, kbnUrl, chrome) nouns: 'saved searches', }; - savedSearchLoader.urlFor = (id) => { - return kbnUrl.eval('#/discover/{{id}}', { id: id }); - }; + savedSearchLoader.urlFor = (id: string) => `#/discover/${encodeURIComponent(id)}`; return savedSearchLoader; } +// this is needed for saved object management const module = uiModules.get('discover/saved_searches'); -module.service('savedSearches', createSavedSearchesService); +module.service('savedSearches', () => { + const services = { + savedObjectsClient: npStart.core.savedObjects.client, + indexPatterns: npStart.plugins.data.indexPatterns, + chrome: npStart.core.chrome, + overlays: npStart.core.overlays, + }; + return createSavedSearchesService(services); +}); diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts index 7ab60f8867c38..8e414984a0c08 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts @@ -22,7 +22,7 @@ import { PersistedState } from 'ui/persisted_state'; import { Subscription } from 'rxjs'; import * as Rx from 'rxjs'; import { buildPipeline } from 'ui/visualize/loader/pipeline_helpers'; -import { SavedObject } from 'ui/saved_objects/saved_object'; +import { SavedObject } from 'ui/saved_objects/types'; import { Vis } from 'ui/vis'; import { queryGeohashBounds } from 'ui/visualize/loader/utils'; import { getTableAggs } from 'ui/visualize/loader/pipeline_helpers/utilities'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js index ae4b4d1c779df..a30973ab6a461 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js +++ b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js @@ -38,7 +38,7 @@ import { uiModules .get('app/visualize') - .factory('SavedVis', function (Promise, savedSearches, Private) { + .factory('SavedVis', function (savedSearches, Private) { const SavedObject = Private(SavedObjectProvider); createLegacyClass(SavedVis).inherits(SavedObject); function SavedVis(opts) { @@ -95,18 +95,15 @@ uiModules return `/app/kibana#${VisualizeConstants.EDIT_PATH}/${this.id}`; }; - SavedVis.prototype._afterEsResp = function () { + SavedVis.prototype._afterEsResp = async function () { const self = this; - return self._getLinkedSavedSearch() - .then(function () { - self.searchSource.setField('size', 0); - - return self.vis ? self._updateVis() : self._createVis(); - }); + await self._getLinkedSavedSearch(); + self.searchSource.setField('size', 0); + return self.vis ? self._updateVis() : self._createVis(); }; - SavedVis.prototype._getLinkedSavedSearch = Promise.method(function () { + SavedVis.prototype._getLinkedSavedSearch = async function () { const self = this; const linkedSearch = !!self.savedSearchId; const current = self.savedSearch; @@ -122,13 +119,10 @@ uiModules } if (linkedSearch) { - return savedSearches.get(self.savedSearchId) - .then(function (savedSearch) { - self.savedSearch = savedSearch; - self.searchSource.setParent(self.savedSearch.searchSource); - }); + self.savedSearch = await savedSearches.get(self.savedSearchId); + self.searchSource.setParent(self.savedSearch.searchSource); } - }); + }; SavedVis.prototype._createVis = function () { const self = this; diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js index c391eb872e29d..dd12b7e38798f 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js +++ b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js @@ -24,6 +24,7 @@ import { savedObjectManagementRegistry } from '../../management/saved_object_reg import { start as visualizations } from '../../../../visualizations/public/np_ready/public/legacy'; import { createVisualizeEditUrl } from '../visualize_constants'; import { findListItems } from './find_list_items'; +import { npStart } from '../../../../../ui/public/new_platform'; const app = uiModules.get('app/visualize'); @@ -34,14 +35,13 @@ savedObjectManagementRegistry.register({ title: 'visualizations', }); -app.service('savedVisualizations', function (SavedVis, Private, kbnUrl, chrome) { +app.service('savedVisualizations', function (SavedVis, Private) { const visTypes = visualizations.types; const savedObjectClient = Private(SavedObjectsClientProvider); const saveVisualizationLoader = new SavedObjectLoader( SavedVis, - kbnUrl, - chrome, - savedObjectClient + savedObjectClient, + npStart.core.chrome ); saveVisualizationLoader.mapHitSource = function (source, id) { @@ -73,7 +73,7 @@ app.service('savedVisualizations', function (SavedVis, Private, kbnUrl, chrome) }; saveVisualizationLoader.urlFor = function (id) { - return kbnUrl.eval('#/visualize/edit/{{id}}', { id: id }); + return `#/visualize/edit/${encodeURIComponent(id)}`; }; // This behaves similarly to find, except it returns visualizations that are diff --git a/src/legacy/core_plugins/timelion/public/services/saved_sheets.js b/src/legacy/core_plugins/timelion/public/services/saved_sheets.js index d303069e74dea..86cde9e06240a 100644 --- a/src/legacy/core_plugins/timelion/public/services/saved_sheets.js +++ b/src/legacy/core_plugins/timelion/public/services/saved_sheets.js @@ -21,6 +21,7 @@ import { SavedObjectLoader, SavedObjectsClientProvider } from 'ui/saved_objects' import { savedObjectManagementRegistry } from 'plugins/kibana/management/saved_object_registry'; import { uiModules } from 'ui/modules'; import './_saved_sheet.js'; +import { npStart } from '../../../../ui/public/new_platform'; const module = uiModules.get('app/sheet'); @@ -32,9 +33,9 @@ savedObjectManagementRegistry.register({ }); // This is the only thing that gets injected into controllers -module.service('savedSheets', function (Private, SavedSheet, kbnUrl, chrome) { +module.service('savedSheets', function (Private, SavedSheet, kbnUrl) { const savedObjectClient = Private(SavedObjectsClientProvider); - const savedSheetLoader = new SavedObjectLoader(SavedSheet, kbnUrl, chrome, savedObjectClient); + const savedSheetLoader = new SavedObjectLoader(SavedSheet, savedObjectClient, npStart.core.chrome); savedSheetLoader.urlFor = function (id) { return kbnUrl.eval('#/{{id}}', { id: id }); }; diff --git a/src/legacy/ui/public/saved_objects/__tests__/find_object_by_title.js b/src/legacy/ui/public/saved_objects/__tests__/find_object_by_title.js index 766ed44a4c0fe..ccd2ba6a6c510 100644 --- a/src/legacy/ui/public/saved_objects/__tests__/find_object_by_title.js +++ b/src/legacy/ui/public/saved_objects/__tests__/find_object_by_title.js @@ -19,7 +19,7 @@ import sinon from 'sinon'; import expect from '@kbn/expect'; -import { findObjectByTitle } from '../find_object_by_title'; +import { findObjectByTitle } from '../helpers/find_object_by_title'; import { SimpleSavedObject } from '../../../../../core/public'; describe('findObjectByTitle', () => { diff --git a/src/legacy/ui/public/saved_objects/__tests__/saved_object.js b/src/legacy/ui/public/saved_objects/__tests__/saved_object.js index 56124a047ba6d..55d0f800ae7ff 100644 --- a/src/legacy/ui/public/saved_objects/__tests__/saved_object.js +++ b/src/legacy/ui/public/saved_objects/__tests__/saved_object.js @@ -22,7 +22,7 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import Bluebird from 'bluebird'; -import { SavedObjectProvider } from '../saved_object'; +import { createSavedObjectClass } from '../saved_object'; import StubIndexPattern from 'test_utils/stub_index_pattern'; import { SavedObjectsClientProvider } from '../saved_objects_client_provider'; import { InvalidJSONProperty } from '../../../../../plugins/kibana_utils/public'; @@ -97,9 +97,9 @@ describe('Saved Object', function () { ); beforeEach(ngMock.inject(function (es, Private, $window) { - SavedObject = Private(SavedObjectProvider); - esDataStub = es; savedObjectsClientStub = Private(SavedObjectsClientProvider); + SavedObject = createSavedObjectClass({ savedObjectsClient: savedObjectsClientStub }); + esDataStub = es; window = $window; })); @@ -110,66 +110,6 @@ describe('Saved Object', function () { sinon.stub(esDataStub, 'create').returns(Bluebird.reject(mock409FetchError)); } - describe('when true', function () { - it('requests confirmation and updates on yes response', function () { - stubESResponse(getMockedDocResponse('myId')); - return createInitializedSavedObject({ type: 'dashboard', id: 'myId' }).then(savedObject => { - const createStub = sinon.stub(savedObjectsClientStub, 'create'); - createStub.onFirstCall().returns(Bluebird.reject(mock409FetchError)); - createStub.onSecondCall().returns(Bluebird.resolve({ id: 'myId' })); - - stubConfirmOverwrite(); - - savedObject.lastSavedTitle = 'original title'; - savedObject.title = 'new title'; - return savedObject.save({ confirmOverwrite: true }) - .then(() => { - expect(window.confirm.called).to.be(true); - expect(savedObject.id).to.be('myId'); - expect(savedObject.isSaving).to.be(false); - expect(savedObject.lastSavedTitle).to.be('new title'); - expect(savedObject.title).to.be('new title'); - }); - }); - }); - - it('does not update on no response', function () { - stubESResponse(getMockedDocResponse('HI')); - return createInitializedSavedObject({ type: 'dashboard', id: 'HI' }).then(savedObject => { - window.confirm = sinon.stub().returns(false); - - sinon.stub(savedObjectsClientStub, 'create').returns(Bluebird.reject(mock409FetchError)); - - savedObject.lastSavedTitle = 'original title'; - savedObject.title = 'new title'; - return savedObject.save({ confirmOverwrite: true }) - .then(() => { - expect(savedObject.id).to.be('HI'); - expect(savedObject.isSaving).to.be(false); - expect(savedObject.lastSavedTitle).to.be('original title'); - expect(savedObject.title).to.be('new title'); - }); - }); - }); - - it('handles create failures', function () { - stubESResponse(getMockedDocResponse('myId')); - return createInitializedSavedObject({ type: 'dashboard', id: 'myId' }).then(savedObject => { - stubConfirmOverwrite(); - - sinon.stub(savedObjectsClientStub, 'create').returns(Bluebird.reject(mock409FetchError)); - - return savedObject.save({ confirmOverwrite: true }) - .then(() => { - expect(true).to.be(false); // Force failure, the save should not succeed. - }) - .catch(() => { - expect(window.confirm.called).to.be(true); - }); - }); - }); - }); - it('when false does not request overwrite', function () { const mockDocResponse = getMockedDocResponse('myId'); stubESResponse(mockDocResponse); @@ -691,18 +631,6 @@ describe('Saved Object', function () { }); }); - it('init is called', function () { - const initCallback = sinon.spy(); - const config = { - type: 'dashboard', - init: initCallback - }; - - return createInitializedSavedObject(config).then(() => { - expect(initCallback.called).to.be(true); - }); - }); - describe('searchSource', function () { it('when true, creates index', function () { const indexPatternId = 'testIndexPattern'; diff --git a/src/legacy/ui/public/saved_objects/saved_object.d.ts b/src/legacy/ui/public/saved_objects/constants.ts similarity index 55% rename from src/legacy/ui/public/saved_objects/saved_object.d.ts rename to src/legacy/ui/public/saved_objects/constants.ts index dc6496eacfcbe..e1684aa1d19d5 100644 --- a/src/legacy/ui/public/saved_objects/saved_object.d.ts +++ b/src/legacy/ui/public/saved_objects/constants.ts @@ -16,15 +16,25 @@ * specific language governing permissions and limitations * under the License. */ +import { i18n } from '@kbn/i18n'; -export interface SaveOptions { - confirmOverwrite: boolean; - isTitleDuplicateConfirmed: boolean; - onTitleDuplicate: () => void; -} - -export interface SavedObject { - save: (saveOptions: SaveOptions) => Promise; - copyOnSave: boolean; - id?: string; -} +/** + * An error message to be used when the user rejects a confirm overwrite. + * @type {string} + */ +export const OVERWRITE_REJECTED = i18n.translate( + 'common.ui.savedObjects.overwriteRejectedDescription', + { + defaultMessage: 'Overwrite confirmation was rejected', + } +); +/** + * An error message to be used when the user rejects a confirm save with duplicate title. + * @type {string} + */ +export const SAVE_DUPLICATE_REJECTED = i18n.translate( + 'common.ui.savedObjects.saveDuplicateRejectedDescription', + { + defaultMessage: 'Save with duplicate title confirmation was rejected', + } +); diff --git a/src/legacy/ui/public/saved_objects/helpers/apply_es_resp.ts b/src/legacy/ui/public/saved_objects/helpers/apply_es_resp.ts new file mode 100644 index 0000000000000..77f504d108076 --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/apply_es_resp.ts @@ -0,0 +1,82 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import _ from 'lodash'; +import { EsResponse, SavedObject, SavedObjectConfig } from 'ui/saved_objects/types'; +import { parseSearchSource } from 'ui/saved_objects/helpers/parse_search_source'; +import { expandShorthand, SavedObjectNotFound } from '../../../../../plugins/kibana_utils/public'; +import { IndexPattern } from '../../../../core_plugins/data/public'; + +/** + * A given response of and ElasticSearch containing a plain saved object is applied to the given + * savedObject + */ +export async function applyESResp( + resp: EsResponse, + savedObject: SavedObject, + config: SavedObjectConfig +) { + const mapping = expandShorthand(config.mapping); + const esType = config.type || ''; + savedObject._source = _.cloneDeep(resp._source); + const injectReferences = config.injectReferences; + const hydrateIndexPattern = savedObject.hydrateIndexPattern!; + if (typeof resp.found === 'boolean' && !resp.found) { + throw new SavedObjectNotFound(esType, savedObject.id || ''); + } + + const meta = resp._source.kibanaSavedObjectMeta || {}; + delete resp._source.kibanaSavedObjectMeta; + + if (!config.indexPattern && savedObject._source.indexPattern) { + config.indexPattern = savedObject._source.indexPattern as IndexPattern; + delete savedObject._source.indexPattern; + } + + // assign the defaults to the response + _.defaults(savedObject._source, savedObject.defaults); + + // transform the source using _deserializers + _.forOwn(mapping, (fieldMapping, fieldName) => { + if (fieldMapping._deserialize && typeof fieldName === 'string') { + savedObject._source[fieldName] = fieldMapping._deserialize( + savedObject._source[fieldName] as string + ); + } + }); + + // Give obj all of the values in _source.fields + _.assign(savedObject, savedObject._source); + savedObject.lastSavedTitle = savedObject.title; + + try { + await parseSearchSource(savedObject, esType, meta.searchSourceJSON, resp.references); + await hydrateIndexPattern(); + if (injectReferences && resp.references && resp.references.length > 0) { + injectReferences(savedObject, resp.references); + } + if (typeof config.afterESResp === 'function') { + await config.afterESResp.call(savedObject); + } + return savedObject; + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + throw e; + } +} diff --git a/src/legacy/ui/public/saved_objects/helpers/build_saved_object.ts b/src/legacy/ui/public/saved_objects/helpers/build_saved_object.ts new file mode 100644 index 0000000000000..a436f70f31ffe --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/build_saved_object.ts @@ -0,0 +1,126 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import _ from 'lodash'; +import { SearchSource } from 'ui/courier'; +import { hydrateIndexPattern } from './hydrate_index_pattern'; +import { intializeSavedObject } from './initialize_saved_object'; +import { serializeSavedObject } from './serialize_saved_object'; + +import { + EsResponse, + SavedObject, + SavedObjectConfig, + SavedObjectKibanaServices, + SavedObjectSaveOpts, +} from '../types'; +import { applyESResp } from './apply_es_resp'; +import { saveSavedObject } from './save_saved_object'; + +export function buildSavedObject( + savedObject: SavedObject, + config: SavedObjectConfig = {}, + services: SavedObjectKibanaServices +) { + const { indexPatterns, savedObjectsClient } = services; + // type name for this object, used as the ES-type + const esType = config.type || ''; + + savedObject.getDisplayName = () => esType; + + // NOTE: this.type (not set in this file, but somewhere else) is the sub type, e.g. 'area' or + // 'data table', while esType is the more generic type - e.g. 'visualization' or 'saved search'. + savedObject.getEsType = () => esType; + + /** + * Flips to true during a save operation, and back to false once the save operation + * completes. + * @type {boolean} + */ + savedObject.isSaving = false; + savedObject.defaults = config.defaults || {}; + // optional search source which this object configures + savedObject.searchSource = config.searchSource ? new SearchSource() : undefined; + // the id of the document + savedObject.id = config.id || void 0; + // the migration version of the document, should only be set on imports + savedObject.migrationVersion = config.migrationVersion; + // Whether to create a copy when the object is saved. This should eventually go away + // in favor of a better rename/save flow. + savedObject.copyOnSave = false; + + /** + * After creation or fetching from ES, ensure that the searchSources index indexPattern + * is an bonafide IndexPattern object. + * + * @return {Promise} + */ + savedObject.hydrateIndexPattern = (id?: string) => + hydrateIndexPattern(id || '', savedObject, indexPatterns, config); + /** + * Asynchronously initialize this object - will only run + * once even if called multiple times. + * + * @return {Promise} + * @resolved {SavedObject} + */ + savedObject.init = _.once(() => intializeSavedObject(savedObject, savedObjectsClient, config)); + + savedObject.applyESResp = (resp: EsResponse) => applyESResp(resp, savedObject, config); + + /** + * Serialize this object + * @return {Object} + */ + savedObject._serialize = () => serializeSavedObject(savedObject, config); + + /** + * Returns true if the object's original title has been changed. New objects return false. + * @return {boolean} + */ + savedObject.isTitleChanged = () => + savedObject._source && savedObject._source.title !== savedObject.title; + + savedObject.creationOpts = (opts: Record = {}) => ({ + id: savedObject.id, + migrationVersion: savedObject.migrationVersion, + ...opts, + }); + + savedObject.save = async (opts: SavedObjectSaveOpts) => { + try { + const result = await saveSavedObject(savedObject, config, opts, services); + return Promise.resolve(result); + } catch (e) { + return Promise.reject(e); + } + }; + + savedObject.destroy = () => {}; + + /** + * Delete this object from Elasticsearch + * @return {promise} + */ + savedObject.delete = () => { + if (!savedObject.id) { + return Promise.reject(new Error('Deleting a saved Object requires type and id')); + } + return savedObjectsClient.delete(esType, savedObject.id); + }; +} diff --git a/src/legacy/ui/public/saved_objects/helpers/check_for_duplicate_title.ts b/src/legacy/ui/public/saved_objects/helpers/check_for_duplicate_title.ts new file mode 100644 index 0000000000000..5c1c1d0d9a851 --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/check_for_duplicate_title.ts @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { SavedObject, SavedObjectKibanaServices } from '../types'; +import { findObjectByTitle } from './find_object_by_title'; +import { SAVE_DUPLICATE_REJECTED } from '../constants'; +import { displayDuplicateTitleConfirmModal } from './display_duplicate_title_confirm_modal'; + +/** + * check for an existing SavedObject with the same title in ES + * returns Promise when it's no duplicate, or the modal displaying the warning + * that's there's a duplicate is confirmed, else it returns a rejected Promise + * @param savedObject + * @param isTitleDuplicateConfirmed + * @param onTitleDuplicate + * @param services + */ +export async function checkForDuplicateTitle( + savedObject: SavedObject, + isTitleDuplicateConfirmed: boolean, + onTitleDuplicate: (() => void) | undefined, + services: SavedObjectKibanaServices +): Promise { + const { savedObjectsClient, overlays } = services; + // Don't check for duplicates if user has already confirmed save with duplicate title + if (isTitleDuplicateConfirmed) { + return true; + } + + // Don't check if the user isn't updating the title, otherwise that would become very annoying to have + // to confirm the save every time, except when copyOnSave is true, then we do want to check. + if (savedObject.title === savedObject.lastSavedTitle && !savedObject.copyOnSave) { + return true; + } + + const duplicate = await findObjectByTitle( + savedObjectsClient, + savedObject.getEsType(), + savedObject.title + ); + + if (!duplicate || duplicate.id === savedObject.id) { + return true; + } + + if (onTitleDuplicate) { + onTitleDuplicate(); + return Promise.reject(new Error(SAVE_DUPLICATE_REJECTED)); + } + + // TODO: make onTitleDuplicate a required prop and remove UI components from this class + // Need to leave here until all users pass onTitleDuplicate. + return displayDuplicateTitleConfirmModal(savedObject, overlays); +} diff --git a/src/legacy/ui/public/saved_objects/helpers/confirm_modal_promise.tsx b/src/legacy/ui/public/saved_objects/helpers/confirm_modal_promise.tsx new file mode 100644 index 0000000000000..1e0a4f4ebe47f --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/confirm_modal_promise.tsx @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { OverlayStart } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { EuiConfirmModal } from '@elastic/eui'; +import { toMountPoint } from '../../../../../plugins/kibana_react/public'; + +export function confirmModalPromise( + message = '', + title = '', + confirmBtnText = '', + overlays: OverlayStart +): Promise { + return new Promise((resolve, reject) => { + const cancelButtonText = i18n.translate( + 'common.ui.savedObjects.confirmModal.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } + ); + + const modal = overlays.openModal( + toMountPoint( + { + modal.close(); + reject(); + }} + onConfirm={() => { + modal.close(); + resolve(true); + }} + confirmButtonText={confirmBtnText} + cancelButtonText={cancelButtonText} + title={title} + > + {message} + + ) + ); + }); +} diff --git a/src/legacy/ui/public/saved_objects/helpers/create_source.ts b/src/legacy/ui/public/saved_objects/helpers/create_source.ts new file mode 100644 index 0000000000000..1818671cecebe --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/create_source.ts @@ -0,0 +1,85 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { SavedObject, SavedObjectKibanaServices } from 'ui/saved_objects/types'; +import { SavedObjectAttributes } from 'kibana/public'; +import { OVERWRITE_REJECTED } from 'ui/saved_objects/constants'; +import { confirmModalPromise } from './confirm_modal_promise'; + +/** + * Attempts to create the current object using the serialized source. If an object already + * exists, a warning message requests an overwrite confirmation. + * @param source - serialized version of this object (return value from this._serialize()) + * What will be indexed into elasticsearch. + * @param savedObject - savedObject + * @param esType - type of the saved object + * @param options - options to pass to the saved object create method + * @param services - provides Kibana services savedObjectsClient and overlays + * @returns {Promise} - A promise that is resolved with the objects id if the object is + * successfully indexed. If the overwrite confirmation was rejected, an error is thrown with + * a confirmRejected = true parameter so that case can be handled differently than + * a create or index error. + * @resolved {SavedObject} + */ +export async function createSource( + source: SavedObjectAttributes, + savedObject: SavedObject, + esType: string, + options = {}, + services: SavedObjectKibanaServices +) { + const { savedObjectsClient, overlays } = services; + try { + return await savedObjectsClient.create(esType, source, options); + } catch (err) { + // record exists, confirm overwriting + if (_.get(err, 'res.status') === 409) { + const confirmMessage = i18n.translate( + 'common.ui.savedObjects.confirmModal.overwriteConfirmationMessage', + { + defaultMessage: 'Are you sure you want to overwrite {title}?', + values: { title: savedObject.title }, + } + ); + + const title = i18n.translate('common.ui.savedObjects.confirmModal.overwriteTitle', { + defaultMessage: 'Overwrite {name}?', + values: { name: savedObject.getDisplayName() }, + }); + const confirmButtonText = i18n.translate( + 'common.ui.savedObjects.confirmModal.overwriteButtonLabel', + { + defaultMessage: 'Overwrite', + } + ); + + return confirmModalPromise(confirmMessage, title, confirmButtonText, overlays) + .then(() => + savedObjectsClient.create( + esType, + source, + savedObject.creationOpts({ overwrite: true, ...options }) + ) + ) + .catch(() => Promise.reject(new Error(OVERWRITE_REJECTED))); + } + return await Promise.reject(err); + } +} diff --git a/src/legacy/ui/public/saved_objects/helpers/display_duplicate_title_confirm_modal.ts b/src/legacy/ui/public/saved_objects/helpers/display_duplicate_title_confirm_modal.ts new file mode 100644 index 0000000000000..36882db72d56c --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/display_duplicate_title_confirm_modal.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { i18n } from '@kbn/i18n'; +import { OverlayStart } from 'kibana/public'; +import { SAVE_DUPLICATE_REJECTED } from '../constants'; +import { confirmModalPromise } from './confirm_modal_promise'; +import { SavedObject } from '../types'; + +export function displayDuplicateTitleConfirmModal( + savedObject: SavedObject, + overlays: OverlayStart +): Promise { + const confirmMessage = i18n.translate( + 'common.ui.savedObjects.confirmModal.saveDuplicateConfirmationMessage', + { + defaultMessage: `A {name} with the title '{title}' already exists. Would you like to save anyway?`, + values: { title: savedObject.title, name: savedObject.getDisplayName() }, + } + ); + + const confirmButtonText = i18n.translate( + 'common.ui.savedObjects.confirmModal.saveDuplicateButtonLabel', + { + defaultMessage: 'Save {name}', + values: { name: savedObject.getDisplayName() }, + } + ); + try { + return confirmModalPromise(confirmMessage, '', confirmButtonText, overlays); + } catch (_) { + return Promise.reject(new Error(SAVE_DUPLICATE_REJECTED)); + } +} diff --git a/src/legacy/ui/public/saved_objects/find_object_by_title.ts b/src/legacy/ui/public/saved_objects/helpers/find_object_by_title.ts similarity index 75% rename from src/legacy/ui/public/saved_objects/find_object_by_title.ts rename to src/legacy/ui/public/saved_objects/helpers/find_object_by_title.ts index d6f11bcb80956..373800f576627 100644 --- a/src/legacy/ui/public/saved_objects/find_object_by_title.ts +++ b/src/legacy/ui/public/saved_objects/helpers/find_object_by_title.ts @@ -17,7 +17,6 @@ * under the License. */ -import { find } from 'lodash'; import { SavedObjectAttributes } from 'src/core/server'; import { SavedObjectsClientContract } from 'src/core/public'; import { SimpleSavedObject } from 'src/core/public'; @@ -30,30 +29,23 @@ import { SimpleSavedObject } from 'src/core/public'; * @param title {string} * @returns {Promise} */ -export function findObjectByTitle( +export async function findObjectByTitle( savedObjectsClient: SavedObjectsClientContract, type: string, title: string ): Promise | void> { if (!title) { - return Promise.resolve(); + return; } // Elastic search will return the most relevant results first, which means exact matches should come // first, and so we shouldn't need to request everything. Using 10 just to be on the safe side. - return savedObjectsClient - .find({ - type, - perPage: 10, - search: `"${title}"`, - searchFields: ['title'], - fields: ['title'], - }) - .then(response => { - const match = find(response.savedObjects, obj => { - return obj.get('title').toLowerCase() === title.toLowerCase(); - }); - - return match; - }); + const response = await savedObjectsClient.find({ + type, + perPage: 10, + search: `"${title}"`, + searchFields: ['title'], + fields: ['title'], + }); + return response.savedObjects.find(obj => obj.get('title').toLowerCase() === title.toLowerCase()); } diff --git a/src/legacy/ui/public/saved_objects/helpers/hydrate_index_pattern.ts b/src/legacy/ui/public/saved_objects/helpers/hydrate_index_pattern.ts new file mode 100644 index 0000000000000..a78b3f97e884b --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/hydrate_index_pattern.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { SavedObject, SavedObjectConfig } from '../types'; +import { IndexPatternsContract } from '../../../../../plugins/data/public'; + +/** + * After creation or fetching from ES, ensure that the searchSources index indexPattern + * is an bonafide IndexPattern object. + * + * @return {Promise} + */ +export async function hydrateIndexPattern( + id: string, + savedObject: SavedObject, + indexPatterns: IndexPatternsContract, + config: SavedObjectConfig +) { + const clearSavedIndexPattern = !!config.clearSavedIndexPattern; + const indexPattern = config.indexPattern; + + if (!savedObject.searchSource) { + return null; + } + + if (clearSavedIndexPattern) { + savedObject.searchSource!.setField('index', undefined); + return null; + } + + const index = id || indexPattern || savedObject.searchSource!.getOwnField('index'); + + if (typeof index !== 'string' || !index) { + return null; + } + + const indexObj = await indexPatterns.get(index); + savedObject.searchSource!.setField('index', indexObj); + return indexObj; +} diff --git a/src/legacy/ui/public/saved_objects/helpers/initialize_saved_object.ts b/src/legacy/ui/public/saved_objects/helpers/initialize_saved_object.ts new file mode 100644 index 0000000000000..c5ea31e0784aa --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/initialize_saved_object.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import _ from 'lodash'; +import { SavedObjectsClientContract } from 'kibana/public'; +import { SavedObject, SavedObjectConfig } from '../types'; + +/** + * Initialize saved object + */ +export async function intializeSavedObject( + savedObject: SavedObject, + savedObjectsClient: SavedObjectsClientContract, + config: SavedObjectConfig +) { + const esType = config.type; + // ensure that the esType is defined + if (!esType) throw new Error('You must define a type name to use SavedObject objects.'); + + if (!savedObject.id) { + // just assign the defaults and be done + _.assign(savedObject, savedObject.defaults); + await savedObject.hydrateIndexPattern!(); + if (typeof config.afterESResp === 'function') { + await config.afterESResp.call(savedObject); + } + return savedObject; + } + + const resp = await savedObjectsClient.get(esType, savedObject.id); + const respMapped = { + _id: resp.id, + _type: resp.type, + _source: _.cloneDeep(resp.attributes), + references: resp.references, + found: !!resp._version, + }; + await savedObject.applyESResp(respMapped); + if (typeof config.init === 'function') { + await config.init.call(savedObject); + } + + return savedObject; +} diff --git a/src/legacy/ui/public/saved_objects/helpers/parse_search_source.ts b/src/legacy/ui/public/saved_objects/helpers/parse_search_source.ts new file mode 100644 index 0000000000000..8c52b7cfa0dbf --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/parse_search_source.ts @@ -0,0 +1,97 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import _ from 'lodash'; +import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; +import { SavedObject } from '../types'; +import { InvalidJSONProperty } from '../../../../../plugins/kibana_utils/public'; + +export function parseSearchSource( + savedObject: SavedObject, + esType: string, + searchSourceJson: string, + references: any[] +) { + if (!savedObject.searchSource) return; + + // if we have a searchSource, set its values based on the searchSourceJson field + let searchSourceValues: Record; + try { + searchSourceValues = JSON.parse(searchSourceJson); + } catch (e) { + throw new InvalidJSONProperty( + `Invalid JSON in ${esType} "${savedObject.id}". ${e.message} JSON: ${searchSourceJson}` + ); + } + + // This detects a scenario where documents with invalid JSON properties have been imported into the saved object index. + // (This happened in issue #20308) + if (!searchSourceValues || typeof searchSourceValues !== 'object') { + throw new InvalidJSONProperty(`Invalid searchSourceJSON in ${esType} "${savedObject.id}".`); + } + + // Inject index id if a reference is saved + if (searchSourceValues.indexRefName) { + const reference = references.find( + (ref: Record) => ref.name === searchSourceValues.indexRefName + ); + if (!reference) { + throw new Error( + `Could not find reference for ${ + searchSourceValues.indexRefName + } on ${savedObject.getEsType()} ${savedObject.id}` + ); + } + searchSourceValues.index = reference.id; + delete searchSourceValues.indexRefName; + } + + if (searchSourceValues.filter) { + searchSourceValues.filter.forEach((filterRow: any) => { + if (!filterRow.meta || !filterRow.meta.indexRefName) { + return; + } + const reference = references.find((ref: any) => ref.name === filterRow.meta.indexRefName); + if (!reference) { + throw new Error( + `Could not find reference for ${ + filterRow.meta.indexRefName + } on ${savedObject.getEsType()}` + ); + } + filterRow.meta.index = reference.id; + delete filterRow.meta.indexRefName; + }); + } + + const searchSourceFields = savedObject.searchSource.getFields(); + const fnProps = _.transform( + searchSourceFields, + function(dynamic: Record, val: any, name: string | undefined) { + if (_.isFunction(val) && name) dynamic[name] = val; + }, + {} + ); + + savedObject.searchSource.setFields(_.defaults(searchSourceValues, fnProps)); + const query = savedObject.searchSource.getOwnField('query'); + + if (typeof query !== 'undefined') { + savedObject.searchSource.setField('query', migrateLegacyQuery(query)); + } +} diff --git a/src/legacy/ui/public/saved_objects/helpers/save_saved_object.ts b/src/legacy/ui/public/saved_objects/helpers/save_saved_object.ts new file mode 100644 index 0000000000000..bd6daa1832a25 --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/save_saved_object.ts @@ -0,0 +1,128 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { + SavedObject, + SavedObjectConfig, + SavedObjectKibanaServices, + SavedObjectSaveOpts, +} from '../types'; +import { OVERWRITE_REJECTED, SAVE_DUPLICATE_REJECTED } from '../constants'; +import { createSource } from './create_source'; +import { checkForDuplicateTitle } from './check_for_duplicate_title'; + +/** + * @param error {Error} the error + * @return {boolean} + */ +function isErrorNonFatal(error: { message: string }) { + if (!error) return false; + return error.message === OVERWRITE_REJECTED || error.message === SAVE_DUPLICATE_REJECTED; +} + +/** + * Saves this object. + * + * @param {string} [esType] + * @param {SavedObject} [savedObject] + * @param {SavedObjectConfig} [config] + * @param {object} [options={}] + * @property {boolean} [options.confirmOverwrite=false] - If true, attempts to create the source so it + * can confirm an overwrite if a document with the id already exists. + * @property {boolean} [options.isTitleDuplicateConfirmed=false] - If true, save allowed with duplicate title + * @property {func} [options.onTitleDuplicate] - function called if duplicate title exists. + * When not provided, confirm modal will be displayed asking user to confirm or cancel save. + * @param {SavedObjectKibanaServices} [services] + * @return {Promise} + * @resolved {String} - The id of the doc + */ +export async function saveSavedObject( + savedObject: SavedObject, + config: SavedObjectConfig, + { + confirmOverwrite = false, + isTitleDuplicateConfirmed = false, + onTitleDuplicate, + }: SavedObjectSaveOpts = {}, + services: SavedObjectKibanaServices +): Promise { + const { savedObjectsClient, chrome } = services; + + const esType = config.type || ''; + const extractReferences = config.extractReferences; + // Save the original id in case the save fails. + const originalId = savedObject.id; + // Read https://github.com/elastic/kibana/issues/9056 and + // https://github.com/elastic/kibana/issues/9012 for some background into why this copyOnSave variable + // exists. + // The goal is to move towards a better rename flow, but since our users have been conditioned + // to expect a 'save as' flow during a rename, we are keeping the logic the same until a better + // UI/UX can be worked out. + if (savedObject.copyOnSave) { + delete savedObject.id; + } + + // Here we want to extract references and set them within "references" attribute + let { attributes, references } = savedObject._serialize(); + if (extractReferences) { + ({ attributes, references } = extractReferences({ attributes, references })); + } + if (!references) throw new Error('References not returned from extractReferences'); + + try { + await checkForDuplicateTitle( + savedObject, + isTitleDuplicateConfirmed, + onTitleDuplicate, + services + ); + savedObject.isSaving = true; + const resp = confirmOverwrite + ? await createSource( + attributes, + savedObject, + esType, + savedObject.creationOpts({ references }), + services + ) + : await savedObjectsClient.create( + esType, + attributes, + savedObject.creationOpts({ references, overwrite: true }) + ); + + savedObject.id = resp.id; + if (savedObject.showInRecentlyAccessed && savedObject.getFullPath) { + chrome.recentlyAccessed.add( + savedObject.getFullPath(), + savedObject.title, + String(savedObject.id) + ); + } + savedObject.isSaving = false; + savedObject.lastSavedTitle = savedObject.title; + return savedObject.id; + } catch (err) { + savedObject.isSaving = false; + savedObject.id = originalId; + if (isErrorNonFatal(err)) { + return ''; + } + return Promise.reject(err); + } +} diff --git a/src/legacy/ui/public/saved_objects/helpers/serialize_saved_object.ts b/src/legacy/ui/public/saved_objects/helpers/serialize_saved_object.ts new file mode 100644 index 0000000000000..ca780f8f9584d --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/serialize_saved_object.ts @@ -0,0 +1,98 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import _ from 'lodash'; +import angular from 'angular'; +import { SavedObject, SavedObjectConfig } from '../types'; +import { expandShorthand } from '../../../../../plugins/kibana_utils/public'; + +export function serializeSavedObject(savedObject: SavedObject, config: SavedObjectConfig) { + // mapping definition for the fields that this object will expose + const mapping = expandShorthand(config.mapping); + const attributes = {} as Record; + const references = []; + + _.forOwn(mapping, (fieldMapping, fieldName) => { + if (typeof fieldName !== 'string') { + return; + } + // @ts-ignore + const savedObjectFieldVal = savedObject[fieldName]; + if (savedObjectFieldVal != null) { + attributes[fieldName] = fieldMapping._serialize + ? fieldMapping._serialize(savedObjectFieldVal) + : savedObjectFieldVal; + } + }); + + if (savedObject.searchSource) { + let searchSourceFields: Record = _.omit(savedObject.searchSource.getFields(), [ + 'sort', + 'size', + ]); + if (searchSourceFields.index) { + // searchSourceFields.index will normally be an IndexPattern, but can be a string in two scenarios: + // (1) `init()` (and by extension `hydrateIndexPattern()`) hasn't been called on Saved Object + // (2) The IndexPattern doesn't exist, so we fail to resolve it in `hydrateIndexPattern()` + const indexId = + typeof searchSourceFields.index === 'string' + ? searchSourceFields.index + : searchSourceFields.index.id; + const refName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; + references.push({ + name: refName, + type: 'index-pattern', + id: indexId, + }); + searchSourceFields = { + ...searchSourceFields, + indexRefName: refName, + index: undefined, + }; + } + if (searchSourceFields.filter) { + searchSourceFields = { + ...searchSourceFields, + filter: searchSourceFields.filter.map((filterRow: any, i: number) => { + if (!filterRow.meta || !filterRow.meta.index) { + return filterRow; + } + const refName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; + references.push({ + name: refName, + type: 'index-pattern', + id: filterRow.meta.index, + }); + return { + ...filterRow, + meta: { + ...filterRow.meta, + indexRefName: refName, + index: undefined, + }, + }; + }), + }; + } + attributes.kibanaSavedObjectMeta = { + searchSourceJSON: angular.toJson(searchSourceFields), + }; + } + + return { attributes, references }; +} diff --git a/src/legacy/ui/public/saved_objects/index.ts b/src/legacy/ui/public/saved_objects/index.ts index 8076213f62e9a..3c77a02c608c6 100644 --- a/src/legacy/ui/public/saved_objects/index.ts +++ b/src/legacy/ui/public/saved_objects/index.ts @@ -19,6 +19,5 @@ export { SavedObjectRegistryProvider } from './saved_object_registry'; export { SavedObjectsClientProvider } from './saved_objects_client_provider'; -// @ts-ignore export { SavedObjectLoader } from './saved_object_loader'; -export { findObjectByTitle } from './find_object_by_title'; +export { findObjectByTitle } from './helpers/find_object_by_title'; diff --git a/src/legacy/ui/public/saved_objects/saved_object.js b/src/legacy/ui/public/saved_objects/saved_object.js deleted file mode 100644 index 1db651ad9308f..0000000000000 --- a/src/legacy/ui/public/saved_objects/saved_object.js +++ /dev/null @@ -1,541 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * @name SavedObject - * - * NOTE: SavedObject seems to track a reference to an object in ES, - * and surface methods for CRUD functionality (save and delete). This seems - * similar to how Backbone Models work. - * - * This class seems to interface with ES primarily through the es Angular - * service and the saved object api. - */ - -import angular from 'angular'; -import _ from 'lodash'; - - -import { InvalidJSONProperty, SavedObjectNotFound, expandShorthand } from '../../../../plugins/kibana_utils/public'; - -import { SearchSource } from '../courier'; -import { findObjectByTitle } from './find_object_by_title'; -import { SavedObjectsClientProvider } from './saved_objects_client_provider'; -import { migrateLegacyQuery } from '../utils/migrate_legacy_query'; -import { npStart } from 'ui/new_platform'; -import { i18n } from '@kbn/i18n'; - -/** - * An error message to be used when the user rejects a confirm overwrite. - * @type {string} - */ -const OVERWRITE_REJECTED = i18n.translate('common.ui.savedObjects.overwriteRejectedDescription', { - defaultMessage: 'Overwrite confirmation was rejected' -}); - -/** - * An error message to be used when the user rejects a confirm save with duplicate title. - * @type {string} - */ -const SAVE_DUPLICATE_REJECTED = i18n.translate('common.ui.savedObjects.saveDuplicateRejectedDescription', { - defaultMessage: 'Save with duplicate title confirmation was rejected' -}); - -/** - * @param error {Error} the error - * @return {boolean} - */ -function isErrorNonFatal(error) { - if (!error) return false; - return error.message === OVERWRITE_REJECTED || error.message === SAVE_DUPLICATE_REJECTED; -} - -export function SavedObjectProvider(Promise, Private, confirmModalPromise, indexPatterns) { - const savedObjectsClient = Private(SavedObjectsClientProvider); - - /** - * The SavedObject class is a base class for saved objects loaded from the server and - * provides additional functionality besides loading/saving/deleting/etc. - * - * It is overloaded and configured to provide type-aware functionality. - * To just retrieve the attributes of saved objects, it is recommended to use SavedObjectLoader - * which returns instances of SimpleSavedObject which don't introduce additional type-specific complexity. - * @param {*} config - */ - function SavedObject(config) { - if (!_.isObject(config)) config = {}; - - /************ - * Initialize config vars - ************/ - - // type name for this object, used as the ES-type - const esType = config.type; - - this.getDisplayName = function () { - return esType; - }; - - // NOTE: this.type (not set in this file, but somewhere else) is the sub type, e.g. 'area' or - // 'data table', while esType is the more generic type - e.g. 'visualization' or 'saved search'. - this.getEsType = function () { - return esType; - }; - - /** - * Flips to true during a save operation, and back to false once the save operation - * completes. - * @type {boolean} - */ - this.isSaving = false; - this.defaults = config.defaults || {}; - - // mapping definition for the fields that this object will expose - const mapping = expandShorthand(config.mapping); - - const afterESResp = config.afterESResp || _.noop; - const customInit = config.init || _.noop; - const extractReferences = config.extractReferences; - const injectReferences = config.injectReferences; - - // optional search source which this object configures - this.searchSource = config.searchSource ? new SearchSource() : undefined; - - // the id of the document - this.id = config.id || void 0; - - // the migration version of the document, should only be set on imports - this.migrationVersion = config.migrationVersion; - - // Whether to create a copy when the object is saved. This should eventually go away - // in favor of a better rename/save flow. - this.copyOnSave = false; - - const parseSearchSource = (searchSourceJson, references) => { - if (!this.searchSource) return; - - // if we have a searchSource, set its values based on the searchSourceJson field - let searchSourceValues; - try { - searchSourceValues = JSON.parse(searchSourceJson); - } catch (e) { - throw new InvalidJSONProperty( - `Invalid JSON in ${esType} "${this.id}". ${e.message} JSON: ${searchSourceJson}` - ); - } - - // This detects a scenario where documents with invalid JSON properties have been imported into the saved object index. - // (This happened in issue #20308) - if (!searchSourceValues || typeof searchSourceValues !== 'object') { - throw new InvalidJSONProperty(`Invalid searchSourceJSON in ${esType} "${this.id}".`); - } - - // Inject index id if a reference is saved - if (searchSourceValues.indexRefName) { - const reference = references.find(reference => reference.name === searchSourceValues.indexRefName); - if (!reference) { - throw new Error(`Could not find reference for ${searchSourceValues.indexRefName} on ${this.getEsType()} ${this.id}`); - } - searchSourceValues.index = reference.id; - delete searchSourceValues.indexRefName; - } - - if (searchSourceValues.filter) { - searchSourceValues.filter.forEach((filterRow) => { - if (!filterRow.meta || !filterRow.meta.indexRefName) { - return; - } - const reference = references.find(reference => reference.name === filterRow.meta.indexRefName); - if (!reference) { - throw new Error(`Could not find reference for ${filterRow.meta.indexRefName} on ${this.getEsType()}`); - } - filterRow.meta.index = reference.id; - delete filterRow.meta.indexRefName; - }); - } - - const searchSourceFields = this.searchSource.getFields(); - const fnProps = _.transform(searchSourceFields, function (dynamic, val, name) { - if (_.isFunction(val)) dynamic[name] = val; - }, {}); - - this.searchSource.setFields(_.defaults(searchSourceValues, fnProps)); - - if (!_.isUndefined(this.searchSource.getOwnField('query'))) { - this.searchSource.setField('query', migrateLegacyQuery(this.searchSource.getOwnField('query'))); - } - }; - - /** - * After creation or fetching from ES, ensure that the searchSources index indexPattern - * is an bonafide IndexPattern object. - * - * @return {Promise} - */ - this.hydrateIndexPattern = (id) => { - if (!this.searchSource) { - return Promise.resolve(null); - } - - if (config.clearSavedIndexPattern) { - this.searchSource.setField('index', null); - return Promise.resolve(null); - } - - let index = id || config.indexPattern || this.searchSource.getOwnField('index'); - - if (!index) { - return Promise.resolve(null); - } - - // If index is not an IndexPattern object at this point, then it's a string id of an index. - if (typeof index === 'string') { - index = indexPatterns.get(index); - } - - // At this point index will either be an IndexPattern, if cached, or a promise that - // will return an IndexPattern, if not cached. - return Promise.resolve(index).then(indexPattern => { - this.searchSource.setField('index', indexPattern); - }); - }; - - /** - * Asynchronously initialize this object - will only run - * once even if called multiple times. - * - * @return {Promise} - * @resolved {SavedObject} - */ - this.init = _.once(() => { - // ensure that the esType is defined - if (!esType) throw new Error('You must define a type name to use SavedObject objects.'); - - return Promise.resolve() - .then(() => { - // If there is not id, then there is no document to fetch from elasticsearch - if (!this.id) { - // just assign the defaults and be done - _.assign(this, this.defaults); - return this.hydrateIndexPattern().then(() => { - return afterESResp.call(this); - }); - } - - // fetch the object from ES - return savedObjectsClient.get(esType, this.id) - .then(resp => { - // temporary compatability for savedObjectsClient - return { - _id: resp.id, - _type: resp.type, - _source: _.cloneDeep(resp.attributes), - references: resp.references, - found: resp._version ? true : false - }; - }) - .then(this.applyESResp) - .catch(this.applyEsResp); - }) - .then(() => customInit.call(this)) - .then(() => this); - }); - - - this.applyESResp = (resp) => { - this._source = _.cloneDeep(resp._source); - - if (resp.found != null && !resp.found) { - throw new SavedObjectNotFound(esType, this.id); - } - - const meta = resp._source.kibanaSavedObjectMeta || {}; - delete resp._source.kibanaSavedObjectMeta; - - if (!config.indexPattern && this._source.indexPattern) { - config.indexPattern = this._source.indexPattern; - delete this._source.indexPattern; - } - - // assign the defaults to the response - _.defaults(this._source, this.defaults); - - // transform the source using _deserializers - _.forOwn(mapping, (fieldMapping, fieldName) => { - if (fieldMapping._deserialize) { - this._source[fieldName] = fieldMapping._deserialize(this._source[fieldName], resp, fieldName, fieldMapping); - } - }); - - // Give obj all of the values in _source.fields - _.assign(this, this._source); - this.lastSavedTitle = this.title; - - return Promise.try(() => { - parseSearchSource(meta.searchSourceJSON, resp.references); - return this.hydrateIndexPattern(); - }).then(() => { - if (injectReferences && resp.references && resp.references.length > 0) { - injectReferences(this, resp.references); - } - return this; - }).then(() => { - return Promise.cast(afterESResp.call(this, resp)); - }); - }; - - /** - * Serialize this object - * - * @return {Object} - */ - this._serialize = () => { - const attributes = {}; - const references = []; - - _.forOwn(mapping, (fieldMapping, fieldName) => { - if (this[fieldName] != null) { - attributes[fieldName] = (fieldMapping._serialize) - ? fieldMapping._serialize(this[fieldName]) - : this[fieldName]; - } - }); - - if (this.searchSource) { - let searchSourceFields = _.omit(this.searchSource.getFields(), ['sort', 'size']); - if (searchSourceFields.index) { - // searchSourceFields.index will normally be an IndexPattern, but can be a string in two scenarios: - // (1) `init()` (and by extension `hydrateIndexPattern()`) hasn't been called on this Saved Object - // (2) The IndexPattern doesn't exist, so we fail to resolve it in `hydrateIndexPattern()` - const indexId = typeof (searchSourceFields.index) === 'string' ? searchSourceFields.index : searchSourceFields.index.id; - const refName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; - references.push({ - name: refName, - type: 'index-pattern', - id: indexId, - }); - searchSourceFields = { - ...searchSourceFields, - indexRefName: refName, - index: undefined, - }; - } - if (searchSourceFields.filter) { - searchSourceFields = { - ...searchSourceFields, - filter: searchSourceFields.filter.map((filterRow, i) => { - if (!filterRow.meta || !filterRow.meta.index) { - return filterRow; - } - const refName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; - references.push({ - name: refName, - type: 'index-pattern', - id: filterRow.meta.index, - }); - return { - ...filterRow, - meta: { - ...filterRow.meta, - indexRefName: refName, - index: undefined, - } - }; - }), - }; - } - attributes.kibanaSavedObjectMeta = { - searchSourceJSON: angular.toJson(searchSourceFields) - }; - } - - return { attributes, references }; - }; - - /** - * Returns true if the object's original title has been changed. New objects return false. - * @return {boolean} - */ - this.isTitleChanged = () => { - return this._source && this._source.title !== this.title; - }; - - this.creationOpts = (opts = {}) => ({ - id: this.id, - migrationVersion: this.migrationVersion, - ...opts, - }); - - /** - * Attempts to create the current object using the serialized source. If an object already - * exists, a warning message requests an overwrite confirmation. - * @param source - serialized version of this object (return value from this._serialize()) - * What will be indexed into elasticsearch. - * @param options - options to pass to the saved object create method - * @returns {Promise} - A promise that is resolved with the objects id if the object is - * successfully indexed. If the overwrite confirmation was rejected, an error is thrown with - * a confirmRejected = true parameter so that case can be handled differently than - * a create or index error. - * @resolved {SavedObject} - */ - const createSource = (source, options = {}) => { - return savedObjectsClient.create(esType, source, options) - .catch(err => { - // record exists, confirm overwriting - if (_.get(err, 'res.status') === 409) { - const confirmMessage = i18n.translate('common.ui.savedObjects.confirmModal.overwriteConfirmationMessage', { - defaultMessage: 'Are you sure you want to overwrite {title}?', - values: { title: this.title } - }); - - return confirmModalPromise(confirmMessage, { - confirmButtonText: i18n.translate('common.ui.savedObjects.confirmModal.overwriteButtonLabel', { - defaultMessage: 'Overwrite', - }), - title: i18n.translate('common.ui.savedObjects.confirmModal.overwriteTitle', { - defaultMessage: 'Overwrite {name}?', - values: { name: this.getDisplayName() } - }), - }) - .then(() => savedObjectsClient.create(esType, source, this.creationOpts({ overwrite: true, ...options }))) - .catch(() => Promise.reject(new Error(OVERWRITE_REJECTED))); - } - return Promise.reject(err); - }); - }; - - const displayDuplicateTitleConfirmModal = () => { - const confirmMessage = i18n.translate('common.ui.savedObjects.confirmModal.saveDuplicateConfirmationMessage', { - defaultMessage: `A {name} with the title '{title}' already exists. Would you like to save anyway?`, - values: { title: this.title, name: this.getDisplayName() } - }); - - return confirmModalPromise(confirmMessage, { - confirmButtonText: i18n.translate('common.ui.savedObjects.confirmModal.saveDuplicateButtonLabel', { - defaultMessage: 'Save {name}', - values: { name: this.getDisplayName() } - }) - }) - .catch(() => Promise.reject(new Error(SAVE_DUPLICATE_REJECTED))); - }; - - const checkForDuplicateTitle = (isTitleDuplicateConfirmed, onTitleDuplicate) => { - // Don't check for duplicates if user has already confirmed save with duplicate title - if (isTitleDuplicateConfirmed) { - return Promise.resolve(); - } - - // Don't check if the user isn't updating the title, otherwise that would become very annoying to have - // to confirm the save every time, except when copyOnSave is true, then we do want to check. - if (this.title === this.lastSavedTitle && !this.copyOnSave) { - return Promise.resolve(); - } - - return findObjectByTitle(savedObjectsClient, this.getEsType(), this.title) - .then(duplicate => { - if (!duplicate) return true; - if (duplicate.id === this.id) return true; - - if (onTitleDuplicate) { - onTitleDuplicate(); - return Promise.reject(new Error(SAVE_DUPLICATE_REJECTED)); - } - - // TODO: make onTitleDuplicate a required prop and remove UI components from this class - // Need to leave here until all users pass onTitleDuplicate. - return displayDuplicateTitleConfirmModal(); - }); - }; - - /** - * Saves this object. - * - * @param {object} [options={}] - * @property {boolean} [options.confirmOverwrite=false] - If true, attempts to create the source so it - * can confirm an overwrite if a document with the id already exists. - * @property {boolean} [options.isTitleDuplicateConfirmed=false] - If true, save allowed with duplicate title - * @property {func} [options.onTitleDuplicate] - function called if duplicate title exists. - * When not provided, confirm modal will be displayed asking user to confirm or cancel save. - * @return {Promise} - * @resolved {String} - The id of the doc - */ - this.save = ({ confirmOverwrite = false, isTitleDuplicateConfirmed = false, onTitleDuplicate } = {}) => { - // Save the original id in case the save fails. - const originalId = this.id; - // Read https://github.com/elastic/kibana/issues/9056 and - // https://github.com/elastic/kibana/issues/9012 for some background into why this copyOnSave variable - // exists. - // The goal is to move towards a better rename flow, but since our users have been conditioned - // to expect a 'save as' flow during a rename, we are keeping the logic the same until a better - // UI/UX can be worked out. - if (this.copyOnSave) { - this.id = null; - } - - // Here we want to extract references and set them within "references" attribute - let { attributes, references } = this._serialize(); - if (extractReferences) { - ({ attributes, references } = extractReferences({ attributes, references })); - } - if (!references) throw new Error('References not returned from extractReferences'); - - this.isSaving = true; - - return checkForDuplicateTitle(isTitleDuplicateConfirmed, onTitleDuplicate) - .then(() => { - if (confirmOverwrite) { - return createSource(attributes, this.creationOpts({ references })); - } else { - return savedObjectsClient.create(esType, attributes, this.creationOpts({ references, overwrite: true })); - } - }) - .then((resp) => { - this.id = resp.id; - }) - .then(() => { - if (this.showInRecentlyAccessed && this.getFullPath) { - npStart.core.chrome.recentlyAccessed.add(this.getFullPath(), this.title, this.id); - } - this.isSaving = false; - this.lastSavedTitle = this.title; - return this.id; - }) - .catch((err) => { - this.isSaving = false; - this.id = originalId; - if (isErrorNonFatal(err)) { - return; - } - return Promise.reject(err); - }); - }; - - this.destroy = () => {}; - - /** - * Delete this object from Elasticsearch - * @return {promise} - */ - this.delete = () => { - return savedObjectsClient.delete(esType, this.id); - }; - } - - return SavedObject; -} diff --git a/src/legacy/ui/public/saved_objects/saved_object.ts b/src/legacy/ui/public/saved_objects/saved_object.ts new file mode 100644 index 0000000000000..91182e67aac0d --- /dev/null +++ b/src/legacy/ui/public/saved_objects/saved_object.ts @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * @name SavedObject + * + * NOTE: SavedObject seems to track a reference to an object in ES, + * and surface methods for CRUD functionality (save and delete). This seems + * similar to how Backbone Models work. + * + * This class seems to interface with ES primarily through the es Angular + * service and the saved object api. + */ +import { npStart } from 'ui/new_platform'; +import { SavedObject, SavedObjectConfig, SavedObjectKibanaServices } from './types'; +import { buildSavedObject } from './helpers/build_saved_object'; + +export function createSavedObjectClass(services: SavedObjectKibanaServices) { + /** + * The SavedObject class is a base class for saved objects loaded from the server and + * provides additional functionality besides loading/saving/deleting/etc. + * + * It is overloaded and configured to provide type-aware functionality. + * To just retrieve the attributes of saved objects, it is recommended to use SavedObjectLoader + * which returns instances of SimpleSavedObject which don't introduce additional type-specific complexity. + * @param {*} config + */ + class SavedObjectClass { + constructor(config: SavedObjectConfig = {}) { + // @ts-ignore + const self: SavedObject = this; + buildSavedObject(self, config, services); + } + } + + return SavedObjectClass as new (config: SavedObjectConfig) => SavedObject; +} +// the old angular way, should be removed once no longer used +export function SavedObjectProvider() { + const services = { + savedObjectsClient: npStart.core.savedObjects.client, + indexPatterns: npStart.plugins.data.indexPatterns, + chrome: npStart.core.chrome, + overlays: npStart.core.overlays, + }; + return createSavedObjectClass(services); +} diff --git a/src/legacy/ui/public/saved_objects/saved_object_loader.js b/src/legacy/ui/public/saved_objects/saved_object_loader.ts similarity index 60% rename from src/legacy/ui/public/saved_objects/saved_object_loader.js rename to src/legacy/ui/public/saved_objects/saved_object_loader.ts index 434ce0d8b0caa..eb880ce5380c0 100644 --- a/src/legacy/ui/public/saved_objects/saved_object_loader.js +++ b/src/legacy/ui/public/saved_objects/saved_object_loader.ts @@ -16,7 +16,8 @@ * specific language governing permissions and limitations * under the License. */ - +import { SavedObject } from 'ui/saved_objects/types'; +import { ChromeStart, SavedObjectsClientContract, SavedObjectsFindOptions } from 'kibana/public'; import { StringUtils } from '../utils/string_utils'; /** @@ -28,20 +29,25 @@ import { StringUtils } from '../utils/string_utils'; * to avoid pulling in extra functionality which isn't used. */ export class SavedObjectLoader { - constructor(SavedObjectClass, kbnUrl, chrome, savedObjectClient) { + private readonly Class: (id: string) => SavedObject; + public type: string; + public lowercaseType: string; + public loaderProperties: Record; + + constructor( + SavedObjectClass: any, + private readonly savedObjectsClient: SavedObjectsClientContract, + private readonly chrome: ChromeStart + ) { this.type = SavedObjectClass.type; this.Class = SavedObjectClass; this.lowercaseType = this.type.toLowerCase(); - this.kbnUrl = kbnUrl; - this.chrome = chrome; this.loaderProperties = { - name: `${ this.lowercaseType }s`, + name: `${this.lowercaseType}s`, noun: StringUtils.upperFirst(this.type), - nouns: `${ this.lowercaseType }s`, + nouns: `${this.lowercaseType}s`, }; - - this.savedObjectsClient = savedObjectClient; } /** @@ -50,27 +56,38 @@ export class SavedObjectLoader { * @param id * @returns {Promise} */ - get(id) { - return (new this.Class(id)).init(); + async get(id: string) { + // @ts-ignore + const obj = new this.Class(id); + return obj.init(); } - urlFor(id) { - return this.kbnUrl.eval(`#/${ this.lowercaseType }/{{id}}`, { id: id }); + urlFor(id: string) { + return `#/${this.lowercaseType}/${encodeURIComponent(id)}`; } - delete(ids) { - ids = !Array.isArray(ids) ? [ids] : ids; + async delete(ids: string | string[]) { + const idsUsed = !Array.isArray(ids) ? [ids] : ids; - const deletions = ids.map(id => { + const deletions = idsUsed.map(id => { + // @ts-ignore const savedObject = new this.Class(id); return savedObject.delete(); }); + await Promise.all(deletions); - return Promise.all(deletions).then(() => { - if (this.chrome) { - this.chrome.untrackNavLinksForDeletedSavedObjects(ids); - } - }); + const coreNavLinks = this.chrome.navLinks; + /** + * Modify last url for deleted saved objects to avoid loading pages with "Could not locate..." + */ + coreNavLinks + .getAll() + .filter( + link => + link.linkToLastSubUrl && + idsUsed.find(deletedId => link.url && link.url.includes(deletedId)) !== undefined + ) + .forEach(link => coreNavLinks.update(link.id, { url: link.baseUrl })); } /** @@ -80,7 +97,7 @@ export class SavedObjectLoader { * @param id * @returns {source} The modified source object, with an id and url field. */ - mapHitSource(source, id) { + mapHitSource(source: Record, id: string) { source.id = id; source.url = this.urlFor(id); return source; @@ -92,7 +109,7 @@ export class SavedObjectLoader { * @param hit * @returns {hit.attributes} The modified hit.attributes object, with an id and url field. */ - mapSavedObjectApiHits(hit) { + mapSavedObjectApiHits(hit: { attributes: Record; id: string }) { return this.mapHitSource(hit.attributes, hit.id); } @@ -100,13 +117,14 @@ export class SavedObjectLoader { * TODO: Rather than use a hardcoded limit, implement pagination. See * https://github.com/elastic/kibana/issues/8044 for reference. * - * @param searchString + * @param search * @param size + * @param fields * @returns {Promise} */ - findAll(search = '', size = 100, fields) { - return this.savedObjectsClient.find( - { + findAll(search: string = '', size: number = 100, fields?: string[]) { + return this.savedObjectsClient + .find({ type: this.lowercaseType, search: search ? `${search}*` : undefined, perPage: size, @@ -114,20 +132,20 @@ export class SavedObjectLoader { searchFields: ['title^3', 'description'], defaultSearchOperator: 'AND', fields, - }).then((resp) => { - return { - total: resp.total, - hits: resp.savedObjects - .map((savedObject) => this.mapSavedObjectApiHits(savedObject)) - }; - }); + } as SavedObjectsFindOptions) + .then(resp => { + return { + total: resp.total, + hits: resp.savedObjects.map(savedObject => this.mapSavedObjectApiHits(savedObject)), + }; + }); } - find(search = '', size = 100) { + find(search: string = '', size: number = 100) { return this.findAll(search, size).then(resp => { return { total: resp.total, - hits: resp.hits.filter(savedObject => !savedObject.error) + hits: resp.hits.filter(savedObject => !savedObject.error), }; }); } diff --git a/src/legacy/ui/public/saved_objects/types.ts b/src/legacy/ui/public/saved_objects/types.ts new file mode 100644 index 0000000000000..bccf73917882a --- /dev/null +++ b/src/legacy/ui/public/saved_objects/types.ts @@ -0,0 +1,90 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { ChromeStart, OverlayStart, SavedObjectsClientContract } from 'kibana/public'; +import { SearchSource, SearchSourceContract } from 'ui/courier'; +import { SavedObjectAttributes, SavedObjectReference } from 'kibana/server'; +import { IndexPatternsContract } from '../../../../plugins/data/public'; +import { IndexPattern } from '../../../core_plugins/data/public'; + +export interface SavedObject { + _serialize: () => { attributes: SavedObjectAttributes; references: SavedObjectReference[] }; + _source: Record; + applyESResp: (resp: EsResponse) => Promise; + copyOnSave: boolean; + creationOpts: (opts: SavedObjectCreationOpts) => Record; + defaults: any; + delete?: () => Promise<{}>; + destroy?: () => void; + getDisplayName: () => string; + getEsType: () => string; + getFullPath: () => string; + hydrateIndexPattern?: (id?: string) => Promise; + id?: string; + init?: () => Promise; + isSaving: boolean; + isTitleChanged: () => boolean; + lastSavedTitle: string; + migrationVersion?: Record; + save: (saveOptions: SavedObjectSaveOpts) => Promise; + searchSource?: SearchSourceContract; + showInRecentlyAccessed: boolean; + title: string; +} + +export interface SavedObjectSaveOpts { + confirmOverwrite?: boolean; + isTitleDuplicateConfirmed?: boolean; + onTitleDuplicate?: () => void; +} + +export interface SavedObjectCreationOpts { + references?: SavedObjectReference[]; + overwrite?: boolean; +} + +export interface SavedObjectKibanaServices { + savedObjectsClient: SavedObjectsClientContract; + indexPatterns: IndexPatternsContract; + chrome: ChromeStart; + overlays: OverlayStart; +} + +export interface SavedObjectConfig { + afterESResp?: () => any; + clearSavedIndexPattern?: boolean; + defaults?: any; + extractReferences?: (opts: { + attributes: SavedObjectAttributes; + references: SavedObjectReference[]; + }) => { + attributes: SavedObjectAttributes; + references: SavedObjectReference[]; + }; + id?: string; + init?: () => void; + indexPattern?: IndexPattern; + injectReferences?: any; + mapping?: any; + migrationVersion?: Record; + path?: string; + searchSource?: SearchSource | boolean; + type?: string; +} + +export type EsResponse = Record; diff --git a/x-pack/legacy/plugins/graph/public/types/persistence.ts b/x-pack/legacy/plugins/graph/public/types/persistence.ts index 9c59b7057fe67..7883e81fb9b8e 100644 --- a/x-pack/legacy/plugins/graph/public/types/persistence.ts +++ b/x-pack/legacy/plugins/graph/public/types/persistence.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObject } from 'ui/saved_objects/saved_object'; +import { SavedObject } from 'ui/saved_objects/types'; import { AdvancedSettings, UrlTemplate, WorkspaceField } from './app_state'; import { WorkspaceNode, WorkspaceEdge } from './workspace_state'; diff --git a/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js b/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js index fdf5172fea8ca..cff6fe878c9fc 100644 --- a/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js +++ b/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js @@ -6,12 +6,12 @@ import './saved_gis_map'; import { uiModules } from 'ui/modules'; -import { SavedObjectLoader, SavedObjectsClientProvider } from 'ui/saved_objects'; +import { SavedObjectLoader } from 'ui/saved_objects'; +import { npStart } from '../../../../../../../src/legacy/ui/public/new_platform'; const module = uiModules.get('app/maps'); // This is the only thing that gets injected into controllers -module.service('gisMapSavedObjectLoader', function (Private, SavedGisMap, kbnUrl, chrome) { - const savedObjectClient = Private(SavedObjectsClientProvider); - return new SavedObjectLoader(SavedGisMap, kbnUrl, chrome, savedObjectClient); +module.service('gisMapSavedObjectLoader', function (SavedGisMap) { + return new SavedObjectLoader(SavedGisMap, npStart.core.savedObjects.client, npStart.core.chrome); }); From c95fa975d82f47c4eee9718b8d73189caa6335f6 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 12 Dec 2019 14:23:47 -0700 Subject: [PATCH 19/36] =?UTF-8?q?[7.x]=20require=20yarn=201.21.1=20to=20av?= =?UTF-8?q?oid=20binary=20planting=20vuln=20(#5289=E2=80=A6=20(#52923)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- x-pack/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4dd5922d3565f..d03f4b7059833 100644 --- a/package.json +++ b/package.json @@ -463,6 +463,6 @@ }, "engines": { "node": "10.15.2", - "yarn": "^1.10.1" + "yarn": "^1.21.1" } } diff --git a/x-pack/package.json b/x-pack/package.json index 870c5f741ab6d..1356bf4b7edd6 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -344,7 +344,7 @@ "xregexp": "4.2.4" }, "engines": { - "yarn": "^1.10.1" + "yarn": "^1.21.1" }, "workspaces": { "nohoist": [ From b184978f1ff592b0105c305a4fcbb7e6e7ec6f17 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Thu, 12 Dec 2019 14:24:17 -0700 Subject: [PATCH 20/36] [Reporting/Screenshots] add error for no shared items found on the page (#52022) (#52946) * [Reporting/Screenshots] add error for no shared items container found on the page * wording adjustment --- .../common/lib/screenshots/get_element_position_data.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts index 927cf9ec7d801..ce51dc2317c79 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts @@ -47,5 +47,11 @@ export const getElementPositionAndAttributes = async ( args: [layout.selectors.screenshot, { title: 'data-title', description: 'data-description' }], }); + if (elementsPositionAndAttributes.length === 0) { + throw new Error( + `No shared items containers were found on the page! Reporting requires a container element with the '${layout.selectors.screenshot}' attribute on the page.` + ); + } + return elementsPositionAndAttributes; }; From 12153e5733e0a7a2216c40ac871698121381db52 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Thu, 12 Dec 2019 16:00:14 -0600 Subject: [PATCH 21/36] [7.x] Add getStartServices API (#50231) (#52943) --- .../core/public/kibana-plugin-public.app.md | 2 +- .../public/kibana-plugin-public.app.mount.md | 9 ++- .../kibana-plugin-public.applicationsetup.md | 2 +- ...c.applicationsetup.registermountcontext.md | 10 ++- .../kibana-plugin-public.applicationstart.md | 2 +- ...c.applicationstart.registermountcontext.md | 10 ++- .../public/kibana-plugin-public.appmount.md | 13 ++++ .../kibana-plugin-public.appmountcontext.md | 6 +- ...kibana-plugin-public.appmountdeprecated.md | 22 ++++++ .../kibana-plugin-public.coresetup.context.md | 4 ++ ...lugin-public.coresetup.getstartservices.md | 17 +++++ .../public/kibana-plugin-public.coresetup.md | 8 ++- .../kibana-plugin-public.legacycoresetup.md | 2 +- .../core/public/kibana-plugin-public.md | 4 +- .../kibana-plugin-public.plugin.setup.md | 4 +- .../application/application_service.test.tsx | 36 +++++++--- .../application/application_service.tsx | 35 +++++++--- src/core/public/application/index.ts | 2 + .../integration_tests/router.test.tsx | 4 +- src/core/public/application/types.ts | 69 ++++++++++++++----- .../public/application/ui/app_container.tsx | 4 +- src/core/public/application/ui/app_router.tsx | 4 +- src/core/public/core_system.ts | 4 +- src/core/public/index.ts | 30 ++++++-- src/core/public/legacy/legacy_service.test.ts | 14 ++++ src/core/public/legacy/legacy_service.ts | 7 ++ src/core/public/mocks.ts | 4 ++ src/core/public/plugins/plugin.test.ts | 27 ++++++++ src/core/public/plugins/plugin.ts | 15 +++- src/core/public/plugins/plugin_context.ts | 1 + .../public/plugins/plugins_service.test.ts | 1 + src/core/public/public.api.md | 23 +++++-- .../local_application_service.ts | 12 +++- .../public/new_platform/new_platform.test.ts | 20 ++++++ .../ui/public/new_platform/new_platform.ts | 15 ++-- src/plugins/dev_tools/public/application.tsx | 15 ++-- .../plugins/core_plugin_b/public/plugin.tsx | 4 ++ .../core_plugin_legacy/public/index.ts | 4 +- .../test_suites/core_plugins/ui_plugins.ts | 17 ++++- 39 files changed, 388 insertions(+), 94 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-public.appmount.md create mode 100644 docs/development/core/public/kibana-plugin-public.appmountdeprecated.md create mode 100644 docs/development/core/public/kibana-plugin-public.coresetup.getstartservices.md diff --git a/docs/development/core/public/kibana-plugin-public.app.md b/docs/development/core/public/kibana-plugin-public.app.md index c500c080a5feb..edab4f88497f6 100644 --- a/docs/development/core/public/kibana-plugin-public.app.md +++ b/docs/development/core/public/kibana-plugin-public.app.md @@ -17,5 +17,5 @@ export interface App extends AppBase | Property | Type | Description | | --- | --- | --- | | [chromeless](./kibana-plugin-public.app.chromeless.md) | boolean | Hide the UI chrome when the application is mounted. Defaults to false. Takes precedence over chrome service visibility settings. | -| [mount](./kibana-plugin-public.app.mount.md) | (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise<AppUnmount> | A mount function called when the user navigates to this app's route. | +| [mount](./kibana-plugin-public.app.mount.md) | AppMount | AppMountDeprecated | A mount function called when the user navigates to this app's route. May have signature of [AppMount](./kibana-plugin-public.appmount.md) or [AppMountDeprecated](./kibana-plugin-public.appmountdeprecated.md). | diff --git a/docs/development/core/public/kibana-plugin-public.app.mount.md b/docs/development/core/public/kibana-plugin-public.app.mount.md index dda06b035db4a..151fb7baeb138 100644 --- a/docs/development/core/public/kibana-plugin-public.app.mount.md +++ b/docs/development/core/public/kibana-plugin-public.app.mount.md @@ -4,10 +4,15 @@ ## App.mount property -A mount function called when the user navigates to this app's route. +A mount function called when the user navigates to this app's route. May have signature of [AppMount](./kibana-plugin-public.appmount.md) or [AppMountDeprecated](./kibana-plugin-public.appmountdeprecated.md). Signature: ```typescript -mount: (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise; +mount: AppMount | AppMountDeprecated; ``` + +## Remarks + +When function has two arguments, it will be called with a [context](./kibana-plugin-public.appmountcontext.md) as the first argument. This behavior is \*\*deprecated\*\*, and consumers should instead use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). + diff --git a/docs/development/core/public/kibana-plugin-public.applicationsetup.md b/docs/development/core/public/kibana-plugin-public.applicationsetup.md index b53873bc0fb8a..a63de399c2ecb 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationsetup.md +++ b/docs/development/core/public/kibana-plugin-public.applicationsetup.md @@ -16,5 +16,5 @@ export interface ApplicationSetup | Method | Description | | --- | --- | | [register(app)](./kibana-plugin-public.applicationsetup.register.md) | Register an mountable application to the system. | -| [registerMountContext(contextName, provider)](./kibana-plugin-public.applicationsetup.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. | +| [registerMountContext(contextName, provider)](./kibana-plugin-public.applicationsetup.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). | diff --git a/docs/development/core/public/kibana-plugin-public.applicationsetup.registermountcontext.md b/docs/development/core/public/kibana-plugin-public.applicationsetup.registermountcontext.md index f264ba500ed6e..275ba431bc7e7 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationsetup.registermountcontext.md +++ b/docs/development/core/public/kibana-plugin-public.applicationsetup.registermountcontext.md @@ -4,12 +4,16 @@ ## ApplicationSetup.registerMountContext() method -Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. +> Warning: This API is now obsolete. +> +> + +Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). Signature: ```typescript -registerMountContext(contextName: T, provider: IContextProvider): void; +registerMountContext(contextName: T, provider: IContextProvider): void; ``` ## Parameters @@ -17,7 +21,7 @@ registerMountContext(contextName: T, provider: | Parameter | Type | Description | | --- | --- | --- | | contextName | T | The key of [AppMountContext](./kibana-plugin-public.appmountcontext.md) this provider's return value should be attached to. | -| provider | IContextProvider<App['mount'], T> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function | +| provider | IContextProvider<AppMountDeprecated, T> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function | Returns: diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.md b/docs/development/core/public/kibana-plugin-public.applicationstart.md index 2a60ff449e44e..4baa4565ff7b0 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.md @@ -23,5 +23,5 @@ export interface ApplicationStart | --- | --- | | [getUrlForApp(appId, options)](./kibana-plugin-public.applicationstart.geturlforapp.md) | Returns a relative URL to a given app, including the global base path. | | [navigateToApp(appId, options)](./kibana-plugin-public.applicationstart.navigatetoapp.md) | Navigiate to a given app | -| [registerMountContext(contextName, provider)](./kibana-plugin-public.applicationstart.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. | +| [registerMountContext(contextName, provider)](./kibana-plugin-public.applicationstart.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). | diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.registermountcontext.md b/docs/development/core/public/kibana-plugin-public.applicationstart.registermountcontext.md index 62821fcbb92ba..c15a23fe82b21 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.registermountcontext.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.registermountcontext.md @@ -4,12 +4,16 @@ ## ApplicationStart.registerMountContext() method -Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. +> Warning: This API is now obsolete. +> +> + +Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). Signature: ```typescript -registerMountContext(contextName: T, provider: IContextProvider): void; +registerMountContext(contextName: T, provider: IContextProvider): void; ``` ## Parameters @@ -17,7 +21,7 @@ registerMountContext(contextName: T, provider: | Parameter | Type | Description | | --- | --- | --- | | contextName | T | The key of [AppMountContext](./kibana-plugin-public.appmountcontext.md) this provider's return value should be attached to. | -| provider | IContextProvider<App['mount'], T> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function | +| provider | IContextProvider<AppMountDeprecated, T> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function | Returns: diff --git a/docs/development/core/public/kibana-plugin-public.appmount.md b/docs/development/core/public/kibana-plugin-public.appmount.md new file mode 100644 index 0000000000000..25faa7be30b68 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appmount.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppMount](./kibana-plugin-public.appmount.md) + +## AppMount type + +A mount function called when the user navigates to this app's route. + +Signature: + +```typescript +export declare type AppMount = (params: AppMountParameters) => AppUnmount | Promise; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appmountcontext.md b/docs/development/core/public/kibana-plugin-public.appmountcontext.md index 68a1c27b11836..2f8c0553d0b38 100644 --- a/docs/development/core/public/kibana-plugin-public.appmountcontext.md +++ b/docs/development/core/public/kibana-plugin-public.appmountcontext.md @@ -4,7 +4,11 @@ ## AppMountContext interface -The context object received when applications are mounted to the DOM. +> Warning: This API is now obsolete. +> +> + +The context object received when applications are mounted to the DOM. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). Signature: diff --git a/docs/development/core/public/kibana-plugin-public.appmountdeprecated.md b/docs/development/core/public/kibana-plugin-public.appmountdeprecated.md new file mode 100644 index 0000000000000..936642abcc97a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appmountdeprecated.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppMountDeprecated](./kibana-plugin-public.appmountdeprecated.md) + +## AppMountDeprecated type + +> Warning: This API is now obsolete. +> +> + +A mount function called when the user navigates to this app's route. + +Signature: + +```typescript +export declare type AppMountDeprecated = (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise; +``` + +## Remarks + +When function has two arguments, it will be called with a [context](./kibana-plugin-public.appmountcontext.md) as the first argument. This behavior is \*\*deprecated\*\*, and consumers should instead use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). + diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.context.md b/docs/development/core/public/kibana-plugin-public.coresetup.context.md index e56ecb92074c4..f2a891c6c674e 100644 --- a/docs/development/core/public/kibana-plugin-public.coresetup.context.md +++ b/docs/development/core/public/kibana-plugin-public.coresetup.context.md @@ -4,6 +4,10 @@ ## CoreSetup.context property +> Warning: This API is now obsolete. +> +> + [ContextSetup](./kibana-plugin-public.contextsetup.md) Signature: diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.getstartservices.md b/docs/development/core/public/kibana-plugin-public.coresetup.getstartservices.md new file mode 100644 index 0000000000000..b89d98b0a9ed5 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.coresetup.getstartservices.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [getStartServices](./kibana-plugin-public.coresetup.getstartservices.md) + +## CoreSetup.getStartServices() method + +Allows plugins to get access to APIs available in start inside async handlers, such as [App.mount](./kibana-plugin-public.app.mount.md). Promise will not resolve until Core and plugin dependencies have completed `start`. + +Signature: + +```typescript +getStartServices(): Promise<[CoreStart, TPluginsStart]>; +``` +Returns: + +`Promise<[CoreStart, TPluginsStart]>` + diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.md b/docs/development/core/public/kibana-plugin-public.coresetup.md index 8314bde7b95f0..7d75782df2e32 100644 --- a/docs/development/core/public/kibana-plugin-public.coresetup.md +++ b/docs/development/core/public/kibana-plugin-public.coresetup.md @@ -9,7 +9,7 @@ Core services exposed to the `Plugin` setup lifecycle Signature: ```typescript -export interface CoreSetup +export interface CoreSetup ``` ## Properties @@ -24,3 +24,9 @@ export interface CoreSetup | [notifications](./kibana-plugin-public.coresetup.notifications.md) | NotificationsSetup | [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | | [uiSettings](./kibana-plugin-public.coresetup.uisettings.md) | IUiSettingsClient | [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) | +## Methods + +| Method | Description | +| --- | --- | +| [getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md) | Allows plugins to get access to APIs available in start inside async handlers, such as [App.mount](./kibana-plugin-public.app.mount.md). Promise will not resolve until Core and plugin dependencies have completed start. | + diff --git a/docs/development/core/public/kibana-plugin-public.legacycoresetup.md b/docs/development/core/public/kibana-plugin-public.legacycoresetup.md index f704bc65d12a5..a753300437c1c 100644 --- a/docs/development/core/public/kibana-plugin-public.legacycoresetup.md +++ b/docs/development/core/public/kibana-plugin-public.legacycoresetup.md @@ -13,7 +13,7 @@ Setup interface exposed to the legacy platform via the `ui/new_platform` module. Signature: ```typescript -export interface LegacyCoreSetup extends CoreSetup +export interface LegacyCoreSetup extends CoreSetup ``` ## Properties diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index 22b6f7faf2daa..c599e1eaa14fe 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -26,7 +26,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AppBase](./kibana-plugin-public.appbase.md) | | | [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) | | | [ApplicationStart](./kibana-plugin-public.applicationstart.md) | | -| [AppMountContext](./kibana-plugin-public.appmountcontext.md) | The context object received when applications are mounted to the DOM. | +| [AppMountContext](./kibana-plugin-public.appmountcontext.md) | The context object received when applications are mounted to the DOM. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). | | [AppMountParameters](./kibana-plugin-public.appmountparameters.md) | | | [Capabilities](./kibana-plugin-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-public.chromebadge.md) | | @@ -98,6 +98,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | Type Alias | Description | | --- | --- | +| [AppMount](./kibana-plugin-public.appmount.md) | A mount function called when the user navigates to this app's route. | +| [AppMountDeprecated](./kibana-plugin-public.appmountdeprecated.md) | A mount function called when the user navigates to this app's route. | | [AppUnmount](./kibana-plugin-public.appunmount.md) | A function called when an application should be unmounted from the page. This function should be synchronous. | | [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) | | | [ChromeHelpExtensionMenuCustomLink](./kibana-plugin-public.chromehelpextensionmenucustomlink.md) | | diff --git a/docs/development/core/public/kibana-plugin-public.plugin.setup.md b/docs/development/core/public/kibana-plugin-public.plugin.setup.md index 56855b02cfbad..f058bc8d86fbc 100644 --- a/docs/development/core/public/kibana-plugin-public.plugin.setup.md +++ b/docs/development/core/public/kibana-plugin-public.plugin.setup.md @@ -7,14 +7,14 @@ Signature: ```typescript -setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| core | CoreSetup | | +| core | CoreSetup<TPluginsStart> | | | plugins | TPluginsSetup | | Returns: diff --git a/src/core/public/application/application_service.test.tsx b/src/core/public/application/application_service.test.tsx index 19a208aeefb37..32634572466a6 100644 --- a/src/core/public/application/application_service.test.tsx +++ b/src/core/public/application/application_service.test.tsx @@ -32,9 +32,9 @@ describe('#setup()', () => { const service = new ApplicationService(); const context = contextServiceMock.createSetupContract(); const setup = service.setup({ context }); - setup.register(Symbol(), { id: 'app1' } as any); + setup.register(Symbol(), { id: 'app1', mount: jest.fn() } as any); expect(() => - setup.register(Symbol(), { id: 'app1' } as any) + setup.register(Symbol(), { id: 'app1', mount: jest.fn() } as any) ).toThrowErrorMatchingInlineSnapshot( `"An application is already registered with the id \\"app1\\""` ); @@ -51,6 +51,18 @@ describe('#setup()', () => { setup.register(Symbol(), { id: 'app1' } as any) ).toThrowErrorMatchingInlineSnapshot(`"Applications cannot be registered after \\"setup\\""`); }); + + it('logs a warning when registering a deprecated app mount', async () => { + const consoleWarnSpy = jest.spyOn(console, 'warn'); + const service = new ApplicationService(); + const context = contextServiceMock.createSetupContract(); + const setup = service.setup({ context }); + setup.register(Symbol(), { id: 'app1', mount: (ctx: any, params: any) => {} } as any); + expect(consoleWarnSpy).toHaveBeenCalledWith( + `App [app1] is using deprecated mount context. Use core.getStartServices() instead.` + ); + consoleWarnSpy.mockRestore(); + }); }); describe('registerLegacyApp', () => { @@ -100,7 +112,7 @@ describe('#start()', () => { const service = new ApplicationService(); const context = contextServiceMock.createSetupContract(); const setup = service.setup({ context }); - setup.register(Symbol(), { id: 'app1' } as any); + setup.register(Symbol(), { id: 'app1', mount: jest.fn() } as any); setup.registerLegacyApp({ id: 'app2' } as any); const http = httpServiceMock.createStartContract(); @@ -108,12 +120,13 @@ describe('#start()', () => { const startContract = await service.start({ http, injectedMetadata }); expect(startContract.availableApps).toMatchInlineSnapshot(` - Map { - "app1" => Object { - "id": "app1", - }, - } - `); + Map { + "app1" => Object { + "id": "app1", + "mount": [MockFunction], + }, + } + `); expect(startContract.availableLegacyApps).toMatchInlineSnapshot(` Map { "app2" => Object { @@ -127,14 +140,15 @@ describe('#start()', () => { const service = new ApplicationService(); const context = contextServiceMock.createSetupContract(); const setup = service.setup({ context }); - setup.register(Symbol(), { id: 'app1' } as any); + const app1 = { id: 'app1', mount: jest.fn() }; + setup.register(Symbol(), app1 as any); const http = httpServiceMock.createStartContract(); const injectedMetadata = injectedMetadataServiceMock.createStartContract(); await service.start({ http, injectedMetadata }); expect(MockCapabilitiesService.start).toHaveBeenCalledWith({ - apps: new Map([['app1', { id: 'app1' }]]), + apps: new Map([['app1', app1]]), legacyApps: new Map(), http, }); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 45ca7f3fe7f7b..df00c84028e6f 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -29,7 +29,8 @@ import { ContextSetup, IContextContainer } from '../context'; import { App, LegacyApp, - AppMounter, + AppMount, + AppMountDeprecated, InternalApplicationSetup, InternalApplicationStart, } from './types'; @@ -50,7 +51,7 @@ interface StartDeps { interface AppBox { app: App; - mount: AppMounter; + mount: AppMount; } /** @@ -61,7 +62,7 @@ export class ApplicationService { private readonly apps$ = new BehaviorSubject>(new Map()); private readonly legacyApps$ = new BehaviorSubject>(new Map()); private readonly capabilities = new CapabilitiesService(); - private mountContext?: IContextContainer; + private mountContext?: IContextContainer; public setup({ context }: SetupDeps): InternalApplicationSetup { this.mountContext = context.createContextContainer(); @@ -75,10 +76,21 @@ export class ApplicationService { throw new Error(`Applications cannot be registered after "setup"`); } - const appBox: AppBox = { - app, - mount: this.mountContext!.createHandler(plugin, app.mount), - }; + let appBox: AppBox; + if (isAppMountDeprecated(app.mount)) { + // eslint-disable-next-line no-console + console.warn( + `App [${app.id}] is using deprecated mount context. Use core.getStartServices() instead.` + ); + + appBox = { + app, + mount: this.mountContext!.createHandler(plugin, app.mount), + }; + } else { + appBox = { app, mount: app.mount }; + } + this.apps$.next(new Map([...this.apps$.value.entries(), [app.id, appBox]])); }, registerLegacyApp: (app: LegacyApp) => { @@ -146,7 +158,7 @@ export class ApplicationService { } // Filter only available apps and map to just the mount function. - const appMounters = new Map( + const appMounts = new Map( [...this.apps$.value] .filter(([id]) => availableApps.has(id)) .map(([id, { mount }]) => [id, mount]) @@ -154,7 +166,7 @@ export class ApplicationService { return ( path ? `/app/${appId}/${path.replace(/^\//, '')}` // Remove preceding slash from path if present : `/app/${appId}`; + +function isAppMountDeprecated(mount: (...args: any[]) => any): mount is AppMountDeprecated { + // Mount functions with two arguments are assumed to expect deprecated `context` object. + return mount.length === 2; +} diff --git a/src/core/public/application/index.ts b/src/core/public/application/index.ts index ae25b54cf07a8..9c4427c772a5e 100644 --- a/src/core/public/application/index.ts +++ b/src/core/public/application/index.ts @@ -22,6 +22,8 @@ export { Capabilities } from './capabilities'; export { App, AppBase, + AppMount, + AppMountDeprecated, AppUnmount, AppMountContext, AppMountParameters, diff --git a/src/core/public/application/integration_tests/router.test.tsx b/src/core/public/application/integration_tests/router.test.tsx index 593858851d387..81aef5204c7e2 100644 --- a/src/core/public/application/integration_tests/router.test.tsx +++ b/src/core/public/application/integration_tests/router.test.tsx @@ -24,7 +24,7 @@ import { BehaviorSubject } from 'rxjs'; import { I18nProvider } from '@kbn/i18n/react'; -import { AppMounter, LegacyApp, AppMountParameters } from '../types'; +import { AppMount, LegacyApp, AppMountParameters } from '../types'; import { httpServiceMock } from '../../http/http_service.mock'; import { AppRouter, AppNotFound } from '../ui'; @@ -35,7 +35,7 @@ const createMountHandler = (htmlString: string) => }); describe('AppContainer', () => { - let apps: Map, Parameters>>; + let apps: Map, Parameters>>; let legacyApps: Map; let history: History; let router: ReactWrapper; diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index a031ab6070413..72460b07900da 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -75,12 +75,14 @@ export interface AppBase { */ export interface App extends AppBase { /** - * A mount function called when the user navigates to this app's route. - * @param context The mount context for this app. - * @param targetDomElement An HTMLElement to mount the application onto. - * @returns An unmounting function that will be called to unmount the application. + * A mount function called when the user navigates to this app's route. May have signature of {@link AppMount} or + * {@link AppMountDeprecated}. + * + * @remarks + * When function has two arguments, it will be called with a {@link AppMountContext | context} as the first argument. + * This behavior is **deprecated**, and consumers should instead use {@link CoreSetup.getStartServices}. */ - mount: (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise; + mount: AppMount | AppMountDeprecated; /** * Hide the UI chrome when the application is mounted. Defaults to `false`. @@ -97,7 +99,39 @@ export interface LegacyApp extends AppBase { } /** - * The context object received when applications are mounted to the DOM. + * A mount function called when the user navigates to this app's route. + * + * @param params {@link AppMountParameters} + * @returns An unmounting function that will be called to unmount the application. See {@link AppUnmount}. + * + * @public + */ +export type AppMount = (params: AppMountParameters) => AppUnmount | Promise; + +/** + * A mount function called when the user navigates to this app's route. + * + * @remarks + * When function has two arguments, it will be called with a {@link AppMountContext | context} as the first argument. + * This behavior is **deprecated**, and consumers should instead use {@link CoreSetup.getStartServices}. + * + * @param context The mount context for this app. Deprecated, use {@link CoreSetup.getStartServices}. + * @param params {@link AppMountParameters} + * @returns An unmounting function that will be called to unmount the application. See {@link AppUnmount}. + * + * @deprecated + * @public + */ +export type AppMountDeprecated = ( + context: AppMountContext, + params: AppMountParameters +) => AppUnmount | Promise; + +/** + * The context object received when applications are mounted to the DOM. Deprecated, use + * {@link CoreSetup.getStartServices}. + * + * @deprecated * @public */ export interface AppMountContext { @@ -192,9 +226,6 @@ export interface AppMountParameters { */ export type AppUnmount = () => void; -/** @internal */ -export type AppMounter = (params: AppMountParameters) => Promise; - /** @public */ export interface ApplicationSetup { /** @@ -205,14 +236,15 @@ export interface ApplicationSetup { /** * Register a context provider for application mounting. Will only be available to applications that depend on the - * plugin that registered this context. + * plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}. * + * @deprecated * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. * @param provider - A {@link IContextProvider} function */ registerMountContext( contextName: T, - provider: IContextProvider + provider: IContextProvider ): void; } @@ -234,8 +266,9 @@ export interface InternalApplicationSetup { /** * Register a context provider for application mounting. Will only be available to applications that depend on the - * plugin that registered this context. + * plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}. * + * @deprecated * @param pluginOpaqueId - The opaque ID of the plugin that is registering the context. * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. * @param provider - A {@link IContextProvider} function @@ -243,7 +276,7 @@ export interface InternalApplicationSetup { registerMountContext( pluginOpaqueId: PluginOpaqueId, contextName: T, - provider: IContextProvider + provider: IContextProvider ): void; } @@ -272,15 +305,16 @@ export interface ApplicationStart { /** * Register a context provider for application mounting. Will only be available to applications that depend on the - * plugin that registered this context. + * plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}. * + * @deprecated * @param pluginOpaqueId - The opaque ID of the plugin that is registering the context. * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. * @param provider - A {@link IContextProvider} function */ registerMountContext( contextName: T, - provider: IContextProvider + provider: IContextProvider ): void; } @@ -301,8 +335,9 @@ export interface InternalApplicationStart /** * Register a context provider for application mounting. Will only be available to applications that depend on the - * plugin that registered this context. + * plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}. * + * @deprecated * @param pluginOpaqueId - The opaque ID of the plugin that is registering the context. * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. * @param provider - A {@link IContextProvider} function @@ -310,7 +345,7 @@ export interface InternalApplicationStart registerMountContext( pluginOpaqueId: PluginOpaqueId, contextName: T, - provider: IContextProvider + provider: IContextProvider ): void; // Internal APIs diff --git a/src/core/public/application/ui/app_container.tsx b/src/core/public/application/ui/app_container.tsx index 876cd3aa3a3d3..9c2bb30e79503 100644 --- a/src/core/public/application/ui/app_container.tsx +++ b/src/core/public/application/ui/app_container.tsx @@ -21,12 +21,12 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { Subject } from 'rxjs'; -import { LegacyApp, AppMounter, AppUnmount } from '../types'; +import { LegacyApp, AppMount, AppUnmount } from '../types'; import { HttpStart } from '../../http'; import { AppNotFound } from './app_not_found_screen'; interface Props extends RouteComponentProps<{ appId: string }> { - apps: ReadonlyMap; + apps: ReadonlyMap; legacyApps: ReadonlyMap; basePath: HttpStart['basePath']; currentAppId$: Subject; diff --git a/src/core/public/application/ui/app_router.tsx b/src/core/public/application/ui/app_router.tsx index b574bf16278e2..67701a33dabf4 100644 --- a/src/core/public/application/ui/app_router.tsx +++ b/src/core/public/application/ui/app_router.tsx @@ -22,12 +22,12 @@ import React from 'react'; import { Router, Route } from 'react-router-dom'; import { Subject } from 'rxjs'; -import { LegacyApp, AppMounter } from '../types'; +import { LegacyApp, AppMount } from '../types'; import { AppContainer } from './app_container'; import { HttpStart } from '../../http'; interface Props { - apps: ReadonlyMap; + apps: ReadonlyMap; legacyApps: ReadonlyMap; basePath: HttpStart['basePath']; currentAppId$: Subject; diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 4818484b00819..abc4c144356e8 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -64,7 +64,7 @@ export interface CoreContext { } /** @internal */ -export interface InternalCoreSetup extends Omit { +export interface InternalCoreSetup extends Omit { application: InternalApplicationSetup; injectedMetadata: InjectedMetadataSetup; } @@ -253,11 +253,11 @@ export class CoreSystem { docLinks, http, i18n, + injectedMetadata: pick(injectedMetadata, ['getInjectedVar']), notifications, overlays, savedObjects, uiSettings, - injectedMetadata: pick(injectedMetadata, ['getInjectedVar']), })); const core: InternalCoreStart = { diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 5e1d4f7a26bca..1af02e6966d13 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -80,7 +80,17 @@ import { export { CoreContext, CoreSystem } from './core_system'; export { RecursiveReadonly } from '../utils'; -export { App, AppBase, AppUnmount, AppMountContext, AppMountParameters } from './application'; +export { + ApplicationSetup, + ApplicationStart, + App, + AppBase, + AppMount, + AppMountDeprecated, + AppUnmount, + AppMountContext, + AppMountParameters, +} from './application'; export { SavedObjectsBatchResponse, @@ -146,10 +156,13 @@ export { MountPoint, UnmountCallback } from './types'; * navigation in the generated docs until there's a fix for * https://github.com/Microsoft/web-build-tools/issues/1237 */ -export interface CoreSetup { +export interface CoreSetup { /** {@link ApplicationSetup} */ application: ApplicationSetup; - /** {@link ContextSetup} */ + /** + * {@link ContextSetup} + * @deprecated + */ context: ContextSetup; /** {@link FatalErrorsSetup} */ fatalErrors: FatalErrorsSetup; @@ -168,6 +181,13 @@ export interface CoreSetup { injectedMetadata: { getInjectedVar: (name: string, defaultValue?: any) => unknown; }; + + /** + * Allows plugins to get access to APIs available in start inside async + * handlers, such as {@link App.mount}. Promise will not resolve until Core + * and plugin dependencies have completed `start`. + */ + getStartServices(): Promise<[CoreStart, TPluginsStart]>; } /** @@ -219,7 +239,7 @@ export interface CoreStart { * @public * @deprecated */ -export interface LegacyCoreSetup extends CoreSetup { +export interface LegacyCoreSetup extends CoreSetup { /** @deprecated */ injectedMetadata: InjectedMetadataSetup; } @@ -240,8 +260,6 @@ export interface LegacyCoreStart extends CoreStart { } export { - ApplicationSetup, - ApplicationStart, Capabilities, ChromeBadge, ChromeBrand, diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts index 37e07af0a7da5..9dd24f9e4a7a3 100644 --- a/src/core/public/legacy/legacy_service.test.ts +++ b/src/core/public/legacy/legacy_service.test.ts @@ -169,6 +169,20 @@ describe('#start()', () => { expect(mockUiNewPlatformStart).toHaveBeenCalledWith(expect.any(Object), {}); }); + it('resolves getStartServices with core and plugin APIs', async () => { + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + }); + + legacyPlatform.setup(defaultSetupDeps); + legacyPlatform.start(defaultStartDeps); + + const { getStartServices } = mockUiNewPlatformSetup.mock.calls[0][0]; + const [coreStart, pluginsStart] = await getStartServices(); + expect(coreStart).toEqual(expect.any(Object)); + expect(pluginsStart).toBe(defaultStartDeps.plugins); + }); + describe('useLegacyTestHarness = false', () => { it('passes the targetDomElement to ui/chrome', () => { const legacyPlatform = new LegacyPlatformService({ diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index 22e315f9e1b03..a4fdd86de5311 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -18,6 +18,8 @@ */ import angular from 'angular'; +import { first } from 'rxjs/operators'; +import { Subject } from 'rxjs'; import { InternalCoreSetup, InternalCoreStart } from '../core_system'; import { LegacyCoreSetup, LegacyCoreStart, MountPoint } from '../'; @@ -55,6 +57,8 @@ export class LegacyPlatformService { public readonly legacyId = Symbol(); private bootstrapModule?: BootstrapModule; private targetDomElement?: HTMLElement; + private readonly startDependencies$ = new Subject<[LegacyCoreStart, object]>(); + private readonly startDependencies = this.startDependencies$.pipe(first()).toPromise(); constructor(private readonly params: LegacyPlatformParams) {} @@ -75,6 +79,7 @@ export class LegacyPlatformService { const legacyCore: LegacyCoreSetup = { ...core, + getStartServices: () => this.startDependencies, application: { register: notSupported(`core.application.register()`), registerMountContext: notSupported(`core.application.registerMountContext()`), @@ -120,6 +125,8 @@ export class LegacyPlatformService { }, }; + this.startDependencies$.next([legacyCore, plugins]); + // Inject parts of the new platform into parts of the legacy platform // so that legacy APIs/modules can mimic their new platform counterparts require('ui/new_platform').__start__(legacyCore, plugins); diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 644df259b8e24..43c8aa6f1d6b9 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -46,6 +46,9 @@ function createCoreSetupMock({ basePath = '' } = {}) { application: applicationServiceMock.createSetupContract(), context: contextServiceMock.createSetupContract(), fatalErrors: fatalErrorsServiceMock.createSetupContract(), + getStartServices: jest.fn, object]>, []>(() => + Promise.resolve([createCoreStartMock({ basePath }), {}]) + ), http: httpServiceMock.createSetupContract({ basePath }), notifications: notificationServiceMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), @@ -75,6 +78,7 @@ function createCoreStartMock({ basePath = '' } = {}) { return mock; } + function pluginInitializerContextMock() { const mock: PluginInitializerContext = { opaqueId: Symbol(), diff --git a/src/core/public/plugins/plugin.test.ts b/src/core/public/plugins/plugin.test.ts index 85de5c6620cc1..111ee93dd699b 100644 --- a/src/core/public/plugins/plugin.test.ts +++ b/src/core/public/plugins/plugin.test.ts @@ -106,6 +106,33 @@ describe('PluginWrapper', () => { expect(mockPlugin.start).toHaveBeenCalledWith(context, deps); }); + test("`start` resolves `startDependencies` Promise after plugin's start", async () => { + expect.assertions(2); + + let startDependenciesResolved = false; + mockPluginLoader.mockResolvedValueOnce(() => ({ + setup: jest.fn(), + start: async () => { + // Add small delay to ensure startDependencies is not resolved until after the plugin instance's start resolves. + await new Promise(resolve => setTimeout(resolve, 10)); + expect(startDependenciesResolved).toBe(false); + }, + })); + await plugin.load(addBasePath); + await plugin.setup({} as any, {} as any); + const context = { any: 'thing' } as any; + const deps = { otherDep: 'value' }; + + // Add promise callback prior to calling `start` to ensure calls in `setup` will not resolve before `start` is + // called. + const startDependenciesCheck = plugin.startDependencies.then(res => { + startDependenciesResolved = true; + expect(res).toEqual([context, deps]); + }); + await plugin.start(context, deps); + await startDependenciesCheck; + }); + test('`stop` fails if plugin is not setup up', async () => { expect(() => plugin.stop()).toThrowErrorMatchingInlineSnapshot( `"Plugin \\"plugin-a\\" can't be stopped since it isn't set up."` diff --git a/src/core/public/plugins/plugin.ts b/src/core/public/plugins/plugin.ts index 05268bbfcdd05..e880627e352c8 100644 --- a/src/core/public/plugins/plugin.ts +++ b/src/core/public/plugins/plugin.ts @@ -17,6 +17,8 @@ * under the License. */ +import { Subject } from 'rxjs'; +import { first } from 'rxjs/operators'; import { DiscoveredPlugin, PluginOpaqueId } from '../../server'; import { PluginInitializerContext } from './plugin_context'; import { loadPluginBundle } from './plugin_loader'; @@ -33,7 +35,7 @@ export interface Plugin< TPluginsSetup extends object = object, TPluginsStart extends object = object > { - setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; stop?(): void; } @@ -70,6 +72,9 @@ export class PluginWrapper< private initializer?: PluginInitializer; private instance?: Plugin; + private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart]>(); + public readonly startDependencies = this.startDependencies$.pipe(first()).toPromise(); + constructor( public readonly discoveredPlugin: DiscoveredPlugin, public readonly opaqueId: PluginOpaqueId, @@ -100,7 +105,7 @@ 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 async setup(setupContext: CoreSetup, plugins: TPluginsSetup) { this.instance = await this.createPluginInstance(); return await this.instance.setup(setupContext, plugins); @@ -118,7 +123,11 @@ export class PluginWrapper< throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`); } - return await this.instance.start(startContext, plugins); + const startContract = await this.instance.start(startContext, plugins); + + this.startDependencies$.next([startContext, plugins]); + + return startContract; } /** diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index f77ddd8f2f696..848f46605d4de 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -107,6 +107,7 @@ export function createPluginSetupContext< injectedMetadata: { getInjectedVar: deps.injectedMetadata.getInjectedVar, }, + getStartServices: () => plugin.startDependencies, }; } diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index 2983d7583cb49..281778f9420dd 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -98,6 +98,7 @@ describe('PluginsService', () => { mockSetupContext = { ...mockSetupDeps, application: expect.any(Object), + getStartServices: expect.any(Function), }; mockStartDeps = { application: applicationServiceMock.createInternalStartContract(), diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 157f0bab466b0..b745c23d52212 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -19,7 +19,7 @@ import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/type // @public export interface App extends AppBase { chromeless?: boolean; - mount: (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise; + mount: AppMount | AppMountDeprecated; } // @public (undocumented) @@ -37,7 +37,8 @@ export interface AppBase { // @public (undocumented) export interface ApplicationSetup { register(app: App): void; - registerMountContext(contextName: T, provider: IContextProvider): void; + // @deprecated + registerMountContext(contextName: T, provider: IContextProvider): void; } // @public (undocumented) @@ -50,10 +51,14 @@ export interface ApplicationStart { path?: string; state?: any; }): void; - registerMountContext(contextName: T, provider: IContextProvider): void; + // @deprecated + registerMountContext(contextName: T, provider: IContextProvider): void; } // @public +export type AppMount = (params: AppMountParameters) => AppUnmount | Promise; + +// @public @deprecated export interface AppMountContext { core: { application: Pick; @@ -71,6 +76,9 @@ export interface AppMountContext { }; } +// @public @deprecated +export type AppMountDeprecated = (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise; + // @public (undocumented) export interface AppMountParameters { appBasePath: string; @@ -275,13 +283,14 @@ export interface CoreContext { } // @public -export interface CoreSetup { +export interface CoreSetup { // (undocumented) application: ApplicationSetup; - // (undocumented) + // @deprecated (undocumented) context: ContextSetup; // (undocumented) fatalErrors: FatalErrorsSetup; + getStartServices(): Promise<[CoreStart, TPluginsStart]>; // (undocumented) http: HttpSetup; // @deprecated @@ -653,7 +662,7 @@ export interface IUiSettingsClient { } // @public @deprecated -export interface LegacyCoreSetup extends CoreSetup { +export interface LegacyCoreSetup extends CoreSetup { // Warning: (ae-forgotten-export) The symbol "InjectedMetadataSetup" needs to be exported by the entry point index.d.ts // // @deprecated (undocumented) @@ -749,7 +758,7 @@ export interface PackageInfo { // @public export interface Plugin { // (undocumented) - setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; // (undocumented) start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; // (undocumented) diff --git a/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts index e5bfd88ea7637..cd3e0d2fd9f89 100644 --- a/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts +++ b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { App, AppUnmount } from 'kibana/public'; +import { App, AppUnmount, AppMountDeprecated } from 'kibana/public'; import { UIRoutes } from 'ui/routes'; import { ILocationService, IScope } from 'angular'; import { npStart } from 'ui/new_platform'; @@ -68,7 +68,10 @@ export class LocalApplicationService { isUnmounted = true; }); (async () => { - unmountHandler = await app.mount({ core: npStart.core }, { element, appBasePath: '' }); + const params = { element, appBasePath: '' }; + unmountHandler = isAppMountDeprecated(app.mount) + ? await app.mount({ core: npStart.core }, params) + : await app.mount(params); // immediately unmount app if scope got destroyed in the meantime if (isUnmounted) { unmountHandler(); @@ -90,3 +93,8 @@ export class LocalApplicationService { } export const localApplicationService = new LocalApplicationService(); + +function isAppMountDeprecated(mount: (...args: any[]) => any): mount is AppMountDeprecated { + // Mount functions with two arguments are assumed to expect deprecated `context` object. + return mount.length === 2; +} diff --git a/src/legacy/ui/public/new_platform/new_platform.test.ts b/src/legacy/ui/public/new_platform/new_platform.test.ts index e5d5cd0a87776..cd1af311d4eff 100644 --- a/src/legacy/ui/public/new_platform/new_platform.test.ts +++ b/src/legacy/ui/public/new_platform/new_platform.test.ts @@ -58,6 +58,26 @@ describe('ui/new_platform', () => { const scopeMock = { $on: jest.fn() }; const elementMock = [document.createElement('div')]; + controller(scopeMock, elementMock); + expect(mountMock).toHaveBeenCalledWith({ + element: elementMock[0], + appBasePath: '/test/base/path/app/test', + }); + }); + + test('controller calls deprecated context app.mount when invoked', () => { + const unmountMock = jest.fn(); + // Two arguments changes how this is called. + const mountMock = jest.fn((context, params) => unmountMock); + legacyAppRegister({ + id: 'test', + title: 'Test', + mount: mountMock, + }); + const controller = setRootControllerMock.mock.calls[0][1]; + const scopeMock = { $on: jest.fn() }; + const elementMock = [document.createElement('div')]; + controller(scopeMock, elementMock); expect(mountMock).toHaveBeenCalledWith(expect.any(Object), { element: elementMock[0], diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index c0b2d6d913257..d80d11e1b1bdd 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -20,7 +20,7 @@ import { IScope } from 'angular'; import { IUiActionsStart, IUiActionsSetup } from 'src/plugins/ui_actions/public'; import { IEmbeddableStart, IEmbeddableSetup } from 'src/plugins/embeddable/public'; -import { LegacyCoreSetup, LegacyCoreStart, App } from '../../../../core/public'; +import { LegacyCoreSetup, LegacyCoreStart, App, AppMountDeprecated } from '../../../../core/public'; import { Plugin as DataPlugin } from '../../../../plugins/data/public'; import { Plugin as ExpressionsPlugin } from '../../../../plugins/expressions/public'; import { @@ -111,13 +111,18 @@ export const legacyAppRegister = (app: App) => { // Root controller cannot return a Promise so use an internal async function and call it immediately (async () => { - const unmount = await app.mount( - { core: npStart.core }, - { element, appBasePath: npSetup.core.http.basePath.prepend(`/app/${app.id}`) } - ); + const params = { element, appBasePath: npSetup.core.http.basePath.prepend(`/app/${app.id}`) }; + const unmount = isAppMountDeprecated(app.mount) + ? await app.mount({ core: npStart.core }, params) + : await app.mount(params); $scope.$on('$destroy', () => { unmount(); }); })(); }); }; + +function isAppMountDeprecated(mount: (...args: any[]) => any): mount is AppMountDeprecated { + // Mount functions with two arguments are assumed to expect deprecated `context` object. + return mount.length === 2; +} diff --git a/src/plugins/dev_tools/public/application.tsx b/src/plugins/dev_tools/public/application.tsx index b3c6bb592f378..be142f2cc74e6 100644 --- a/src/plugins/dev_tools/public/application.tsx +++ b/src/plugins/dev_tools/public/application.tsx @@ -25,7 +25,7 @@ import * as React from 'react'; import ReactDOM from 'react-dom'; import { useEffect, useRef } from 'react'; -import { AppMountContext } from 'kibana/public'; +import { AppMountContext, AppMountDeprecated } from 'kibana/public'; import { DevTool } from './plugin'; interface DevToolsWrapperProps { @@ -91,10 +91,10 @@ function DevToolsWrapper({ if (mountedTool.current) { mountedTool.current.unmountHandler(); } - const unmountHandler = await activeDevTool.mount(appMountContext, { - element, - appBasePath: '', - }); + const params = { element, appBasePath: '' }; + const unmountHandler = isAppMountDeprecated(activeDevTool.mount) + ? await activeDevTool.mount(appMountContext, params) + : await activeDevTool.mount(params); mountedTool.current = { devTool: activeDevTool, mountpoint: element, @@ -182,3 +182,8 @@ export function renderApp( return () => ReactDOM.unmountComponentAtNode(element); } + +function isAppMountDeprecated(mount: (...args: any[]) => any): mount is AppMountDeprecated { + // Mount functions with two arguments are assumed to expect deprecated `context` object. + return mount.length === 2; +} 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 56cc1cb4ab425..5c8e1d03d5a4a 100644 --- a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx +++ b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx @@ -24,6 +24,7 @@ declare global { interface Window { corePluginB?: string; hasAccessToInjectedMetadata?: boolean; + receivedStartServices?: boolean; env?: PluginInitializerContext['env']; } } @@ -40,6 +41,9 @@ export class CorePluginBPlugin public setup(core: CoreSetup, deps: CorePluginBDeps) { window.corePluginB = `Plugin A said: ${deps.core_plugin_a.getGreeting()}`; window.hasAccessToInjectedMetadata = 'getInjectedVar' in core.injectedMetadata; + core.getStartServices().then(([coreStart, plugins]) => { + window.receivedStartServices = 'overlays' in coreStart; + }); core.application.register({ id: 'bar', diff --git a/test/plugin_functional/plugins/core_plugin_legacy/public/index.ts b/test/plugin_functional/plugins/core_plugin_legacy/public/index.ts index 6988ed82f34a7..51b5d2aaf3587 100644 --- a/test/plugin_functional/plugins/core_plugin_legacy/public/index.ts +++ b/test/plugin_functional/plugins/core_plugin_legacy/public/index.ts @@ -22,8 +22,8 @@ import { npSetup } from 'ui/new_platform'; npSetup.core.application.register({ id: 'core_legacy_compat', title: 'Core Legacy Compat', - async mount(...args) { + async mount(context, params) { const { renderApp } = await import('./application'); - return renderApp(...args); + return renderApp(context, params); }, }); diff --git a/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts b/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts index a971921ad3ed8..ff53583546487 100644 --- a/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts +++ b/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts @@ -36,28 +36,41 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider expect(corePluginB).to.equal(`Plugin A said: Hello from Plugin A!`); }); }); + describe('have injectedMetadata service provided', function describeIndexTests() { before(async () => { await PageObjects.common.navigateToApp('bar'); }); - it('should attach string to window.corePluginB', async () => { + it('should attach boolean to window.hasAccessToInjectedMetadata', async () => { const hasAccessToInjectedMetadata = await browser.execute( 'return window.hasAccessToInjectedMetadata' ); expect(hasAccessToInjectedMetadata).to.equal(true); }); }); + describe('have env data provided', function describeIndexTests() { before(async () => { await PageObjects.common.navigateToApp('bar'); }); - it('should attach pluginContext to window.corePluginB', async () => { + it('should attach pluginContext to window.env', async () => { const envData: any = await browser.execute('return window.env'); expect(envData.mode.dev).to.be(true); expect(envData.packageInfo.version).to.be.a('string'); }); }); + + describe('have access to start services via coreSetup.getStartServices', function describeIndexTests() { + before(async () => { + await PageObjects.common.navigateToApp('bar'); + }); + + it('should attach boolean to window.receivedStartServices', async () => { + const receivedStartServices = await browser.execute('return window.receivedStartServices'); + expect(receivedStartServices).to.equal(true); + }); + }); }); } From da06a9d646cbd5e2a088b80947d675e16fff3b26 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 12 Dec 2019 15:59:49 -0700 Subject: [PATCH 22/36] [SIEM][Detection Engine] Adds a tags service and optimizes alert_id lookups (#52952) ## Summary * Adds a tags services for use by UI's that want to get a list of all the unique tags that are on all of the rules just like an aggregation * Removes the horribly inefficient `alert_id` look up that was a full alert scan and instead uses an internal structure that it augments to the tags for fast `alert_id` look ups. * Adds unit tests for the tags and internal structure tags * Updates other unit tests Usage for the UI: ```sh GET /api/detection_engine/tags ``` or shell script: ```sh ./get_tags.sh ``` Returns: ```sh [ "tag_1", "tag_2" ] ``` Testing: Ensure that the internal structure does not leak when doing any of these script/API calls * ./get_tags.sh * ./post_rule.sh ./rules/queries/query_with_tags.json * ./update_rule.sh ./rules/queries/query_with_tags.json * ./delete_rule.sh * ./find_rules.sh * ./find_rule_by_filter.sh "alert.attributes.enabled:%20true" * ./find_rule_by_filter.sh "alert.attributes.tags:tag_1" Caveat: You can do filter searches against tags that have the double underscore such as: ```sh ./find_rule_by_filter.sh "alert.attributes.tags:%20__*" ``` But that shouldn't be a big problem and more than likely no one will be naming something with double underscores. ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. ~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~ ~~- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~~ ~~- [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~~ - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ~~- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~~ ### For maintainers ~~- [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~ - [x] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) --- .../legacy/plugins/siem/common/constants.ts | 8 + .../plugins/siem/server/kibana.index.ts | 3 + .../routes/__mocks__/request_responses.ts | 3 +- .../routes/rules/utils.test.ts | 66 ++++ .../detection_engine/routes/rules/utils.ts | 7 +- .../routes/tags/read_tags_route.ts | 46 +++ .../rules/add_rule_id_to_tags.test.ts | 35 ++ .../rules/add_rule_id_to_tags.ts | 15 + .../detection_engine/rules/create_rules.ts | 3 +- .../detection_engine/rules/read_rules.test.ts | 146 +------ .../lib/detection_engine/rules/read_rules.ts | 84 ++-- .../lib/detection_engine/rules/types.ts | 9 - .../detection_engine/rules/update_rules.ts | 3 +- .../rules/update_tags.test.ts | 35 ++ .../lib/detection_engine/rules/update_tags.ts | 16 + .../lib/detection_engine/scripts/get_tags.sh | 15 + .../detection_engine/scripts/post_x_rules.sh | 1 + .../detection_engine/tags/read_tags.test.ts | 364 ++++++++++++++++++ .../lib/detection_engine/tags/read_tags.ts | 80 ++++ 19 files changed, 727 insertions(+), 212 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/add_rule_id_to_tags.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/add_rule_id_to_tags.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_tags.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_tags.ts create mode 100755 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_tags.sh create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts index f08a6e66c3400..7b5015c34de14 100644 --- a/x-pack/legacy/plugins/siem/common/constants.ts +++ b/x-pack/legacy/plugins/siem/common/constants.ts @@ -40,6 +40,13 @@ export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges'; */ export const SIGNALS_ID = `${APP_ID}.signals`; +/** + * Special internal structure for tags for signals. This is used + * to filter out tags that have internal structures within them. + */ +export const INTERNAL_IDENTIFIER = '__internal'; +export const INTERNAL_RULE_ID_KEY = `${INTERNAL_IDENTIFIER}_rule_id`; + /** * Detection engine routes */ @@ -47,6 +54,7 @@ export const DETECTION_ENGINE_URL = '/api/detection_engine'; export const DETECTION_ENGINE_RULES_URL = `${DETECTION_ENGINE_URL}/rules`; export const DETECTION_ENGINE_PRIVILEGES_URL = `${DETECTION_ENGINE_URL}/privileges`; export const DETECTION_ENGINE_INDEX_URL = `${DETECTION_ENGINE_URL}/index`; +export const DETECTION_ENGINE_TAGS_URL = `${DETECTION_ENGINE_URL}/tags`; /** * Default signals index key for kibana.dev.yml diff --git a/x-pack/legacy/plugins/siem/server/kibana.index.ts b/x-pack/legacy/plugins/siem/server/kibana.index.ts index 219c59dbf11a3..e90e6366dd9ec 100644 --- a/x-pack/legacy/plugins/siem/server/kibana.index.ts +++ b/x-pack/legacy/plugins/siem/server/kibana.index.ts @@ -19,6 +19,7 @@ import { querySignalsRoute } from './lib/detection_engine/routes/signals/query_s import { ServerFacade } from './types'; import { deleteIndexRoute } from './lib/detection_engine/routes/index/delete_index_route'; import { isAlertExecutor } from './lib/detection_engine/signals/types'; +import { readTagsRoute } from './lib/detection_engine/routes/tags/read_tags_route'; import { readPrivilegesRoute } from './lib/detection_engine/routes/privileges/read_privileges_route'; const APP_ID = 'siem'; @@ -54,6 +55,8 @@ export const initServerWithKibana = (context: PluginInitializerContext, __legacy readIndexRoute(__legacy); deleteIndexRoute(__legacy); + // Detection Engine tags routes that have the REST endpoints of /api/detection_engine/tags + readTagsRoute(__legacy); // Privileges API to get the generic user privileges readPrivilegesRoute(__legacy); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index cb24b0d0c89b1..d9dd7bb1ff7d0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -12,6 +12,7 @@ import { DETECTION_ENGINE_SIGNALS_STATUS_URL, DETECTION_ENGINE_PRIVILEGES_URL, DETECTION_ENGINE_QUERY_SIGNALS_URL, + INTERNAL_RULE_ID_KEY, } from '../../../../../common/constants'; import { RuleAlertType } from '../../rules/types'; import { RuleAlertParamsRest } from '../../types'; @@ -171,7 +172,7 @@ export const createActionResult = (): ActionResult => ({ export const getResult = (): RuleAlertType => ({ id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', name: 'Detect Root/Admin Users', - tags: [], + tags: [`${INTERNAL_RULE_ID_KEY}:rule-1`], alertTypeId: 'siem.signals', params: { description: 'Detecting root and admin users', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts index d4e129f543ccf..a2312ce25e72a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -11,8 +11,10 @@ import { getIdError, transformFindAlertsOrError, transformOrError, + transformTags, } from './utils'; import { getResult } from '../__mocks__/request_responses'; +import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; describe('utils', () => { describe('transformAlertToRule', () => { @@ -335,6 +337,53 @@ describe('utils', () => { type: 'query', }); }); + + test('should work with tags but filter out any internal tags', () => { + const fullRule = getResult(); + fullRule.tags = ['tag 1', 'tag 2', `${INTERNAL_IDENTIFIER}_some_other_value`]; + const rule = transformAlertToRule(fullRule); + expect(rule).toEqual({ + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + risk_score: 50, + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: ['tag 1', 'tag 2'], + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + techniques: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + to: 'now', + type: 'query', + }); + }); }); describe('getIdError', () => { @@ -493,4 +542,21 @@ describe('utils', () => { expect((output as Boom).message).toEqual('Internal error transforming'); }); }); + + describe('transformTags', () => { + test('it returns tags that have no internal structures', () => { + expect(transformTags(['tag 1', 'tag 2'])).toEqual(['tag 1', 'tag 2']); + }); + + test('it returns empty tags given empty tags', () => { + expect(transformTags([])).toEqual([]); + }); + + test('it returns tags with internal tags stripped out', () => { + expect(transformTags(['tag 1', `${INTERNAL_IDENTIFIER}_some_value`, 'tag 2'])).toEqual([ + 'tag 1', + 'tag 2', + ]); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts index c9ae3abdfdc6b..ff06b63a034b8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -6,6 +6,7 @@ import Boom from 'boom'; import { pickBy } from 'lodash/fp'; +import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; import { RuleAlertType, isAlertType, isAlertTypes } from '../../rules/types'; import { OutputRuleAlertRest } from '../../types'; @@ -25,6 +26,10 @@ export const getIdError = ({ } }; +export const transformTags = (tags: string[]): string[] => { + return tags.filter(tag => !tag.startsWith(INTERNAL_IDENTIFIER)); +}; + // Transforms the data but will remove any null or undefined it encounters and not include // those on the export export const transformAlertToRule = (alert: RuleAlertType): Partial => { @@ -51,7 +56,7 @@ export const transformAlertToRule = (alert: RuleAlertType): Partial { + server.route(createReadTagsRoute); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/add_rule_id_to_tags.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/add_rule_id_to_tags.test.ts new file mode 100644 index 0000000000000..5a92c8ef42ed7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/add_rule_id_to_tags.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { addRuleIdToTags } from './add_rule_id_to_tags'; +import { INTERNAL_RULE_ID_KEY } from '../../../../common/constants'; + +describe('add_rule_id_to_tags', () => { + test('it should add a rule id as an internal structure to a single tag', () => { + const tags = addRuleIdToTags(['tag 1'], 'rule-1'); + expect(tags).toEqual(['tag 1', `${INTERNAL_RULE_ID_KEY}:rule-1`]); + }); + + test('it should add a rule id as an internal structure to two tags', () => { + const tags = addRuleIdToTags(['tag 1', 'tag 2'], 'rule-1'); + expect(tags).toEqual(['tag 1', 'tag 2', `${INTERNAL_RULE_ID_KEY}:rule-1`]); + }); + + test('it should add a rule id as an internal structure with empty tags', () => { + const tags = addRuleIdToTags([], 'rule-1'); + expect(tags).toEqual([`${INTERNAL_RULE_ID_KEY}:rule-1`]); + }); + + test('it should add not add an internal structure if rule id is undefined', () => { + const tags = addRuleIdToTags(['tag 1'], undefined); + expect(tags).toEqual(['tag 1']); + }); + + test('it should add not add an internal structure if rule id is null', () => { + const tags = addRuleIdToTags(['tag 1'], null); + expect(tags).toEqual(['tag 1']); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/add_rule_id_to_tags.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/add_rule_id_to_tags.ts new file mode 100644 index 0000000000000..1cf97881d514b --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/add_rule_id_to_tags.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { INTERNAL_RULE_ID_KEY } from '../../../../common/constants'; + +export const addRuleIdToTags = (tags: string[], ruleId: string | null | undefined): string[] => { + if (ruleId == null) { + return tags; + } else { + return [...tags, `${INTERNAL_RULE_ID_KEY}:${ruleId}`]; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts index 4cbf3756f58ac..c4c31190e1e83 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts @@ -6,6 +6,7 @@ import { SIGNALS_ID } from '../../../../common/constants'; import { RuleParams } from './types'; +import { addRuleIdToTags } from './add_rule_id_to_tags'; export const createRules = async ({ alertsClient, @@ -37,7 +38,7 @@ export const createRules = async ({ return alertsClient.create({ data: { name, - tags, + tags: addRuleIdToTags(tags, ruleId), alertTypeId: SIGNALS_ID, params: { description, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts index b3d7ab1322775..6ba0aa95bdd7b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts @@ -5,14 +5,9 @@ */ import { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; -import { readRules, readRuleByRuleId, findRuleInArrayByRuleId } from './read_rules'; +import { readRules } from './read_rules'; import { AlertsClient } from '../../../../../alerting'; -import { - getResult, - getFindResultWithSingleHit, - getFindResultWithMultiHits, -} from '../routes/__mocks__/request_responses'; -import { SIGNALS_ID } from '../../../../common/constants'; +import { getResult, getFindResultWithSingleHit } from '../routes/__mocks__/request_responses'; describe('read_rules', () => { describe('readRules', () => { @@ -98,141 +93,4 @@ describe('read_rules', () => { expect(rule).toEqual(null); }); }); - - describe('readRuleByRuleId', () => { - test('should return a single value if the rule id matches', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rule = await readRuleByRuleId({ - alertsClient: unsafeCast, - ruleId: 'rule-1', - }); - expect(rule).toEqual(getResult()); - }); - - test('should not return a single value if the rule id does not match', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rule = await readRuleByRuleId({ - alertsClient: unsafeCast, - ruleId: 'rule-that-should-not-match-anything', - }); - expect(rule).toEqual(null); - }); - - test('should return a single value of rule-1 with multiple values', async () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rule = await readRuleByRuleId({ - alertsClient: unsafeCast, - ruleId: 'rule-1', - }); - expect(rule).toEqual(result1); - }); - - test('should return a single value of rule-2 with multiple values', async () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rule = await readRuleByRuleId({ - alertsClient: unsafeCast, - ruleId: 'rule-2', - }); - expect(rule).toEqual(result2); - }); - - test('should return null for a made up value with multiple values', async () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rule = await readRuleByRuleId({ - alertsClient: unsafeCast, - ruleId: 'rule-that-should-not-match-anything', - }); - expect(rule).toEqual(null); - }); - }); - - describe('findRuleInArrayByRuleId', () => { - test('returns null if the objects are not of a signal rule type', () => { - const rule = findRuleInArrayByRuleId( - [ - { alertTypeId: 'made up 1', params: { ruleId: '123' } }, - { alertTypeId: 'made up 2', params: { ruleId: '456' } }, - ], - '123' - ); - expect(rule).toEqual(null); - }); - - test('returns correct type if the objects are of a signal rule type', () => { - const rule = findRuleInArrayByRuleId( - [ - { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, - { alertTypeId: 'made up 2', params: { ruleId: '456' } }, - ], - '123' - ); - expect(rule).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '123' } }); - }); - - test('returns second correct type if the objects are of a signal rule type', () => { - const rule = findRuleInArrayByRuleId( - [ - { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, - { alertTypeId: SIGNALS_ID, params: { ruleId: '456' } }, - ], - '456' - ); - expect(rule).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '456' } }); - }); - - test('returns null with correct types but data does not exist', () => { - const rule = findRuleInArrayByRuleId( - [ - { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, - { alertTypeId: SIGNALS_ID, params: { ruleId: '456' } }, - ], - '892' - ); - expect(rule).toEqual(null); - }); - }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts index 5c33526329016..9c83ae924486d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts @@ -4,66 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ +import { INTERNAL_RULE_ID_KEY } from '../../../../common/constants'; import { findRules } from './find_rules'; -import { RuleAlertType, isAlertTypeArray, ReadRuleParams, ReadRuleByRuleId } from './types'; +import { RuleAlertType, ReadRuleParams, isAlertType } from './types'; -export const findRuleInArrayByRuleId = ( - objects: object[], - ruleId: string -): RuleAlertType | null => { - if (isAlertTypeArray(objects)) { - const rules: RuleAlertType[] = objects; - const rule: RuleAlertType[] = rules.filter(datum => { - return datum.params.ruleId === ruleId; - }); - if (rule.length !== 0) { - return rule[0]; - } else { - return null; - } - } else { - return null; - } -}; - -// This an extremely slow and inefficient way of getting a rule by its id. -// I have to manually query every single record since the rule Params are -// not indexed and I cannot push in my own _id when I create an alert at the moment. -// TODO: Once we can directly push in the _id, then we should no longer need this way. -// TODO: This is meant to be _very_ temporary. -export const readRuleByRuleId = async ({ +/** + * This reads the rules through a cascade try of what is fastest to what is slowest. + * @param id - This is the fastest. This is the auto-generated id through the parameter id. + * and the id will either be found through `alertsClient.get({ id })` or it will not + * be returned as a not-found or a thrown error that is not 404. + * @param ruleId - This is a close second to being fast as long as it can find the rule_id from + * a filter query against the tags using `alert.attributes.tags: "__internal:${ruleId}"]` + */ +export const readRules = async ({ alertsClient, + id, ruleId, -}: ReadRuleByRuleId): Promise => { - const firstRules = await findRules({ alertsClient, page: 1 }); - const firstRule = findRuleInArrayByRuleId(firstRules.data, ruleId); - if (firstRule != null) { - return firstRule; - } else { - const totalPages = Math.ceil(firstRules.total / firstRules.perPage); - return Array(totalPages) - .fill({}) - .map((_, page) => { - // page index never starts at zero. It always has to be 1 or greater - return findRules({ alertsClient, page: page + 1 }); - }) - .reduce>(async (accum, findRule) => { - const rules = await findRule; - const rule = findRuleInArrayByRuleId(rules.data, ruleId); - if (rule != null) { - return rule; - } else { - return accum; - } - }, Promise.resolve(null)); - } -}; - -export const readRules = async ({ alertsClient, id, ruleId }: ReadRuleParams) => { +}: ReadRuleParams): Promise => { if (id != null) { try { - const output = await alertsClient.get({ id }); - return output; + const rule = await alertsClient.get({ id }); + if (isAlertType(rule)) { + return rule; + } else { + return null; + } } catch (err) { if (err.output.statusCode === 404) { return null; @@ -73,7 +38,16 @@ export const readRules = async ({ alertsClient, id, ruleId }: ReadRuleParams) => } } } else if (ruleId != null) { - return readRuleByRuleId({ alertsClient, ruleId }); + const ruleFromFind = await findRules({ + alertsClient, + filter: `alert.attributes.tags: "${INTERNAL_RULE_ID_KEY}:${ruleId}"`, + page: 1, + }); + if (ruleFromFind.data.length === 0 || !isAlertType(ruleFromFind.data[0])) { + return null; + } else { + return ruleFromFind.data[0]; + } } else { // should never get here, and yet here we are. return null; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts index 5c0fa76b52620..caeec68c504e6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -84,11 +84,6 @@ export interface ReadRuleParams { ruleId?: string | undefined | null; } -export interface ReadRuleByRuleId { - alertsClient: AlertsClient; - ruleId: string; -} - export const isAlertTypes = (obj: unknown[]): obj is RuleAlertType[] => { return obj.every(rule => isAlertType(rule)); }; @@ -96,7 +91,3 @@ export const isAlertTypes = (obj: unknown[]): obj is RuleAlertType[] => { export const isAlertType = (obj: unknown): obj is RuleAlertType => { return get('alertTypeId', obj) === SIGNALS_ID; }; - -export const isAlertTypeArray = (objArray: unknown[]): objArray is RuleAlertType[] => { - return objArray.length === 0 || isAlertType(objArray[0]); -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts index 2eaa05ae2fa6a..b37e9f6cb8538 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts @@ -8,6 +8,7 @@ import { defaults } from 'lodash/fp'; import { AlertAction } from '../../../../../alerting/server/types'; import { readRules } from './read_rules'; import { UpdateRuleParams } from './types'; +import { updateTags } from './update_tags'; export const calculateInterval = ( interval: string | undefined, @@ -114,7 +115,7 @@ export const updateRules = async ({ return alertsClient.update({ id: rule.id, data: { - tags: tags != null ? tags : [], + tags: updateTags(rule.tags, tags), name: calculateName({ updatedName: name, originalName: rule.name }), interval: calculateInterval(interval, rule.interval), actions, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_tags.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_tags.test.ts new file mode 100644 index 0000000000000..cca937d73bd74 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_tags.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { updateTags } from './update_tags'; +import { INTERNAL_IDENTIFIER } from '../../../../common/constants'; + +describe('update_tags', () => { + test('it should copy internal structures but not any other tags when updating', () => { + const tags = updateTags(['tag 2', `${INTERNAL_IDENTIFIER}_some_value`, 'tag 3'], ['tag 1']); + expect(tags).toEqual([`${INTERNAL_IDENTIFIER}_some_value`, 'tag 1']); + }); + + test('it should copy internal structures but not any other tags if given an update of empty tags', () => { + const tags = updateTags(['tag 2', `${INTERNAL_IDENTIFIER}_some_value`, 'tag 3'], []); + expect(tags).toEqual([`${INTERNAL_IDENTIFIER}_some_value`]); + }); + + test('it should work like a normal update if there are no internal structures', () => { + const tags = updateTags(['tag 2', 'tag 3'], ['tag 1']); + expect(tags).toEqual(['tag 1']); + }); + + test('it should not perform an update if the nextTags are undefined', () => { + const tags = updateTags(['tag 2', `${INTERNAL_IDENTIFIER}_some_value`, 'tag 3'], undefined); + expect(tags).toEqual(['tag 2', `${INTERNAL_IDENTIFIER}_some_value`, 'tag 3']); + }); + + test('it should not perform an update if the nextTags are null', () => { + const tags = updateTags(['tag 2', `${INTERNAL_IDENTIFIER}_some_value`, 'tag 3'], null); + expect(tags).toEqual(['tag 2', `${INTERNAL_IDENTIFIER}_some_value`, 'tag 3']); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_tags.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_tags.ts new file mode 100644 index 0000000000000..cf1424ea31600 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_tags.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { INTERNAL_IDENTIFIER } from '../../../../common/constants'; + +export const updateTags = (prevTags: string[], nextTags: string[] | undefined | null): string[] => { + if (nextTags == null) { + return prevTags; + } else { + const allInternalStructures = prevTags.filter(tag => tag.startsWith(INTERNAL_IDENTIFIER)); + return [...allInternalStructures, ...nextTags]; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_tags.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_tags.sh new file mode 100755 index 0000000000000..2458c6aeba248 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_tags.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./get_tags.sh +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/tags | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh index 53e7bb504746d..0dd4d85ea9da8 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh @@ -32,6 +32,7 @@ do { \"type\": \"query\", \"from\": \"now-6m\", \"to\": \"now\", + \"enabled\": \"false\", \"query\": \"user.name: root or user.name: admin\", \"language\": \"kuery\" }" \ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts new file mode 100644 index 0000000000000..2d562672a4a63 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts @@ -0,0 +1,364 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; +import { AlertsClient } from '../../../../../alerting'; +import { getResult, getFindResultWithMultiHits } from '../routes/__mocks__/request_responses'; +import { INTERNAL_RULE_ID_KEY, INTERNAL_IDENTIFIER } from '../../../../common/constants'; +import { readRawTags, readTags, convertTagsToSet, convertToTags, isTags } from './read_tags'; + +describe('read_tags', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('readRawTags', () => { + test('it should return the intersection of tags to where none are repeating', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 2', 'tag 3']; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = ['tag 1', 'tag 2', 'tag 3', 'tag 4']; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readRawTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); + }); + + test('it should return the intersection of tags to where some are repeating values', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readRawTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); + }); + + test('it should work with no tags defined between two results', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = []; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = []; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readRawTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual([]); + }); + + test('it should work with a single tag which has repeating values in it', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 1', 'tag 1', 'tag 2']; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readRawTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1', 'tag 2']); + }); + + test('it should work with a single tag which has empty tags', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = []; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readRawTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual([]); + }); + }); + + describe('readTags', () => { + test('it should return the intersection of tags to where none are repeating', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 2', 'tag 3']; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = ['tag 1', 'tag 2', 'tag 3', 'tag 4']; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); + }); + + test('it should return the intersection of tags to where some are repeating values', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); + }); + + test('it should work with no tags defined between two results', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = []; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = []; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual([]); + }); + + test('it should work with a single tag which has repeating values in it', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 1', 'tag 1', 'tag 2']; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1', 'tag 2']); + }); + + test('it should work with a single tag which has empty tags', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = []; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual([]); + }); + + test('it should filter out any __internal tags for things such as alert_id', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = [ + `${INTERNAL_IDENTIFIER}_some_value`, + `${INTERNAL_RULE_ID_KEY}_some_value`, + 'tag 1', + ]; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1']); + }); + + test('it should filter out any __internal tags with two different results', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = [ + `${INTERNAL_IDENTIFIER}_some_value`, + `${INTERNAL_RULE_ID_KEY}_some_value`, + 'tag 1', + 'tag 2', + 'tag 3', + 'tag 4', + 'tag 5', + ]; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = [ + `${INTERNAL_IDENTIFIER}_some_value`, + `${INTERNAL_RULE_ID_KEY}_some_value`, + 'tag 1', + 'tag 2', + 'tag 3', + 'tag 4', + ]; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4', 'tag 5']); + }); + }); + + describe('convertTagsToSet', () => { + test('it should convert the intersection of two tag systems without duplicates', () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; + + const findResult = getFindResultWithMultiHits([result1, result2]); + const set = convertTagsToSet(findResult.data); + expect(Array.from(set)).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); + }); + + test('it should with with an empty array', () => { + const set = convertTagsToSet([]); + expect(Array.from(set)).toEqual([]); + }); + }); + + describe('convertToTags', () => { + test('it should convert the two tag systems together with duplicates', () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; + + const findResult = getFindResultWithMultiHits([result1, result2]); + const tags = convertToTags(findResult.data); + expect(tags).toEqual([ + 'tag 1', + 'tag 2', + 'tag 2', + 'tag 3', + 'tag 1', + 'tag 2', + 'tag 2', + 'tag 3', + 'tag 4', + ]); + }); + + test('it should filter out anything that is not a tag', () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; + + const result2 = getResult(); + result2.id = '99979e67-19a7-455f-b452-8eded6135716'; + result2.params.ruleId = 'rule-2'; + delete result2.tags; + + const result3 = getResult(); + result3.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result3.params.ruleId = 'rule-2'; + result3.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; + + const findResult = getFindResultWithMultiHits([result1, result2, result3]); + const tags = convertToTags(findResult.data); + expect(tags).toEqual([ + 'tag 1', + 'tag 2', + 'tag 2', + 'tag 3', + 'tag 1', + 'tag 2', + 'tag 2', + 'tag 3', + 'tag 4', + ]); + }); + + test('it should with with an empty array', () => { + const tags = convertToTags([]); + expect(tags).toEqual([]); + }); + }); + + describe('isTags', () => { + test('it should return true if the object has a tags on it', () => { + expect(isTags({ tags: [] })).toBe(true); + }); + + test('it should return false if the object does not have a tags on it', () => { + expect(isTags({})).toBe(false); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts new file mode 100644 index 0000000000000..0f973d816917f --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { has } from 'lodash/fp'; +import { INTERNAL_IDENTIFIER } from '../../../../common/constants'; +import { AlertsClient } from '../../../../../alerting'; +import { findRules } from '../rules/find_rules'; + +const DEFAULT_PER_PAGE: number = 1000; + +export interface TagType { + id: string; + tags: string[]; +} + +export const isTags = (obj: object): obj is TagType => { + return has('tags', obj); +}; + +export const convertToTags = (tagObjects: object[]): string[] => { + const tags = tagObjects.reduce((accum, tagObj) => { + if (isTags(tagObj)) { + return [...accum, ...tagObj.tags]; + } else { + return accum; + } + }, []); + return tags; +}; + +export const convertTagsToSet = (tagObjects: object[]): Set => { + return new Set(convertToTags(tagObjects)); +}; + +// Note: This is doing an in-memory aggregation of the tags by calling each of the alerting +// records in batches of this const setting and uses the fields to try to get the least +// amount of data per record back. If saved objects at some point supports aggregations +// then this should be replaced with a an aggregation call. +// Ref: https://www.elastic.co/guide/en/kibana/master/saved-objects-api.html +export const readTags = async ({ + alertsClient, + perPage = DEFAULT_PER_PAGE, +}: { + alertsClient: AlertsClient; + perPage?: number; +}): Promise => { + const tags = await readRawTags({ alertsClient, perPage }); + return tags.filter(tag => !tag.startsWith(INTERNAL_IDENTIFIER)); +}; + +export const readRawTags = async ({ + alertsClient, + perPage = DEFAULT_PER_PAGE, +}: { + alertsClient: AlertsClient; + perPage?: number; +}): Promise => { + const firstTags = await findRules({ alertsClient, fields: ['tags'], perPage, page: 1 }); + const firstSet = convertTagsToSet(firstTags.data); + const totalPages = Math.ceil(firstTags.total / firstTags.perPage); + if (totalPages <= 1) { + return Array.from(firstSet); + } else { + const returnTags = await Array(totalPages - 1) + .fill({}) + .map((_, page) => { + // page index starts at 2 as we already got the first page and we have more pages to go + return findRules({ alertsClient, fields: ['tags'], perPage, page: page + 2 }); + }) + .reduce>>(async (accum, nextTagPage) => { + const tagArray = convertToTags((await nextTagPage).data); + return new Set([...(await accum), ...tagArray]); + }, Promise.resolve(firstSet)); + + return Array.from(returnTags); + } +}; From 2d002a3866007738ef986d9a6e9782e546b78338 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 12 Dec 2019 17:10:55 -0700 Subject: [PATCH 23/36] skip flaky suite (#48236) (cherry picked from commit 5fa9b1f0cc3c98cc4b73765d2dcef3ee2f68935a) --- test/functional/apps/dashboard/empty_dashboard.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/functional/apps/dashboard/empty_dashboard.js b/test/functional/apps/dashboard/empty_dashboard.js index 1614c8ebf49a9..03a55c3e18f44 100644 --- a/test/functional/apps/dashboard/empty_dashboard.js +++ b/test/functional/apps/dashboard/empty_dashboard.js @@ -26,7 +26,8 @@ export default function ({ getService, getPageObjects }) { const dashboardAddPanel = getService('dashboardAddPanel'); const PageObjects = getPageObjects(['common', 'dashboard']); - describe('empty dashboard', () => { + // FLAKY: https://github.com/elastic/kibana/issues/48236 + describe.skip('empty dashboard', () => { before(async () => { await esArchiver.load('dashboard/current/kibana'); await kibanaServer.uiSettings.replace({ @@ -47,7 +48,6 @@ export default function ({ getService, getPageObjects }) { expect(addButtonExists).to.be(true); }); - // Flaky test: https://github.com/elastic/kibana/issues/48236 it.skip('should open add panel when add button is clicked', async () => { await testSubjects.click('emptyDashboardAddPanelButton'); const isAddPanelOpen = await dashboardAddPanel.isAddPanelOpen(); From 730beb38d76a7fa4dc553f801ee2ae7f056b8532 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Thu, 12 Dec 2019 17:15:16 -0700 Subject: [PATCH 24/36] [Metrics UI] Add AWS Metricsets to Inventory Models (#49983) (#52949) * Adding initial code for EC2 * Removing obsolute files; Adding EC2; * Removing currentTimerange and replacing it with currentTime; Timerange will now be calcuated on the server * Fixing AWS.s3 with Metrics Explorer * Auto calculating timerange and interval based on metricset.period * Adding S3 metricset * Inital addition of RDS metrics * Adding SQS and fixing a few things * Fixing typescript error * Adding RDS; Adjusting fields for S3; adding new formatter * Return 60 seconds by detault * Fixing types * Removing i18n * Fixing tests * Fixing translations * Fixes from merge * Removing IDX from code not covered by #52354 * fixing tests * Adding controls for crossliking; consolidating display name * remove obsolete import * Adding drop_last_bucket_support to TSVB models * Changing type * Fixing value per type * remvoing obsolete translation * Removing duplicate lines * Removing icons from switcher * Reducing boilerplate in Toolbar Items * Changing file name --- .../plugins/infra/common/ecs_allowed_list.ts | 6 + .../infra/common/graphql/shared/schema.gql.ts | 4 + .../plugins/infra/common/graphql/types.ts | 38 ++++ .../infra/common/http_api/metadata_api.ts | 11 +- .../common/inventory_models/aws_ec2/index.ts | 30 +++ .../inventory_models/aws_ec2/layout.tsx | 116 +++++++++++ .../inventory_models/aws_ec2/metrics/index.ts | 28 +++ .../aws_ec2/metrics/snapshot/cpu.ts | 27 +++ .../metrics/snapshot/disk_io_read_bytes.ts | 15 ++ .../metrics/snapshot/disk_io_write_bytes.ts | 15 ++ .../aws_ec2/metrics/snapshot/rx.ts | 15 ++ .../aws_ec2/metrics/snapshot/tx.ts | 15 ++ .../metrics/tsvb/aws_ec2_cpu_utilization.ts | 37 ++++ .../metrics/tsvb/aws_ec2_diskio_bytes.ts | 41 ++++ .../metrics/tsvb/aws_ec2_network_traffic.ts | 41 ++++ .../aws_ec2/toolbar_items.tsx | 33 ++++ .../common/inventory_models/aws_rds/index.ts | 35 ++++ .../inventory_models/aws_rds/layout.tsx | 180 ++++++++++++++++++ .../inventory_models/aws_rds/metrics/index.ts | 38 ++++ .../aws_rds/metrics/snapshot/cpu.ts | 27 +++ .../snapshot/rds_active_transactions.ts | 15 ++ .../metrics/snapshot/rds_connections.ts | 15 ++ .../aws_rds/metrics/snapshot/rds_latency.ts | 15 ++ .../metrics/snapshot/rds_queries_executed.ts | 15 ++ .../tsvb/aws_rds_active_transactions.ts | 36 ++++ .../metrics/tsvb/aws_rds_connections.ts | 25 +++ .../aws_rds/metrics/tsvb/aws_rds_cpu_total.ts | 37 ++++ .../aws_rds/metrics/tsvb/aws_rds_latency.ts | 68 +++++++ .../metrics/tsvb/aws_rds_queries_executed.ts | 24 +++ .../aws_rds/toolbar_items.tsx | 32 ++++ .../common/inventory_models/aws_s3/index.ts | 35 ++++ .../common/inventory_models/aws_s3/layout.tsx | 143 ++++++++++++++ .../inventory_models/aws_s3/metrics/index.ts | 38 ++++ .../aws_s3/metrics/snapshot/s3_bucket_size.ts | 15 ++ .../metrics/snapshot/s3_download_bytes.ts | 15 ++ .../metrics/snapshot/s3_number_of_objects.ts | 15 ++ .../metrics/snapshot/s3_total_requests.ts | 15 ++ .../metrics/snapshot/s3_upload_bytes.ts | 15 ++ .../aws_s3/metrics/tsvb/aws_s3_bucket_size.ts | 26 +++ .../metrics/tsvb/aws_s3_download_bytes.ts | 25 +++ .../metrics/tsvb/aws_s3_number_of_objects.ts | 26 +++ .../metrics/tsvb/aws_s3_total_requests.ts | 26 +++ .../metrics/tsvb/aws_s3_upload_bytes.ts | 26 +++ .../inventory_models/aws_s3/toolbar_items.tsx | 28 +++ .../common/inventory_models/aws_sqs/index.ts | 35 ++++ .../inventory_models/aws_sqs/layout.tsx | 143 ++++++++++++++ .../inventory_models/aws_sqs/metrics/index.ts | 38 ++++ .../metrics/snapshot/sqs_messages_delayed.ts | 15 ++ .../metrics/snapshot/sqs_messages_empty.ts | 15 ++ .../metrics/snapshot/sqs_messages_sent.ts | 15 ++ .../metrics/snapshot/sqs_messages_visible.ts | 15 ++ .../metrics/snapshot/sqs_oldest_message.ts | 15 ++ .../metrics/tsvb/aws_sqs_messages_delayed.ts | 26 +++ .../metrics/tsvb/aws_sqs_messages_empty.ts | 26 +++ .../metrics/tsvb/aws_sqs_messages_sent.ts | 26 +++ .../metrics/tsvb/aws_sqs_messages_visible.ts | 26 +++ .../metrics/tsvb/aws_sqs_oldest_message.ts | 26 +++ .../aws_sqs/toolbar_items.tsx | 28 +++ .../inventory_models/container/index.ts | 15 ++ .../container/metrics/index.ts | 1 + .../container/toolbar_items.tsx | 74 +++---- .../inventory_models/create_tsvb_model.ts | 24 +++ .../common/inventory_models/host/index.ts | 15 ++ .../inventory_models/host/metrics/index.ts | 1 + .../inventory_models/host/toolbar_items.tsx | 77 +++----- .../infra/common/inventory_models/index.ts | 41 +++- .../infra/common/inventory_models/layouts.ts | 8 + .../infra/common/inventory_models/metrics.ts | 8 + .../common/inventory_models/pod/index.ts | 15 ++ .../inventory_models/pod/metrics/index.ts | 1 + .../inventory_models/pod/toolbar_items.tsx | 60 ++---- .../metrics_and_groupby_toolbar_items.tsx | 52 +++++ .../inventory_models/shared/metrics/index.ts | 1 + .../infra/common/inventory_models/toolbars.ts | 8 + .../infra/common/inventory_models/types.ts | 52 +++++ .../public/components/inventory/layout.tsx | 7 +- .../inventory/toolbars/toolbar_wrapper.tsx | 128 +++++++++++++ .../components/nodes_overview/index.tsx | 47 ++++- .../components/nodes_overview/table.tsx | 13 +- .../components/waffle/group_of_groups.tsx | 6 +- .../components/waffle/group_of_nodes.tsx | 8 +- .../waffle/lib/field_to_display_name.ts | 42 +++- .../infra/public/components/waffle/map.tsx | 10 +- .../infra/public/components/waffle/node.tsx | 15 +- .../components/waffle/node_context_menu.tsx | 62 +++--- .../waffle/waffle_inventory_switcher.tsx | 105 ++++++---- .../waffle/waffle_metric_controls.tsx | 2 +- .../public/containers/waffle/use_snaphot.ts | 9 +- .../infra/public/graphql/introspection.json | 25 ++- .../plugins/infra/public/graphql/types.ts | 38 ++++ .../infrastructure/snapshot/page_content.tsx | 4 +- .../infra/public/pages/link_to/link_to.tsx | 7 +- .../pages/link_to/redirect_to_node_logs.tsx | 10 +- .../public/utils/formatters/high_precision.ts | 11 ++ .../infra/public/utils/formatters/index.ts | 2 + .../plugins/infra/server/graphql/types.ts | 38 ++++ .../metrics/kibana_metrics_adapter.ts | 13 +- .../plugins/infra/server/lib/constants.ts | 17 -- .../create_timerange_with_interval.ts | 59 ++++++ .../server/lib/snapshot/query_helpers.ts | 23 ++- .../server/lib/snapshot/response_helpers.ts | 17 +- .../infra/server/lib/snapshot/snapshot.ts | 43 +++-- .../infra/server/lib/snapshot/types.ts | 23 +++ .../routes/metadata/lib/get_id_field_name.ts | 18 -- .../metadata/lib/get_metric_metadata.ts | 13 +- .../routes/metadata/lib/get_node_info.ts | 18 +- .../routes/metadata/lib/get_pod_node_name.ts | 5 +- .../routes/metadata/lib/has_apm_data.ts | 9 +- .../infra/server/routes/snapshot/index.ts | 2 +- .../server/utils/calculate_metric_interval.ts | 12 +- .../translations/translations/ja-JP.json | 5 +- .../translations/translations/zh-CN.json | 3 - .../test/api_integration/apis/infra/waffle.ts | 18 +- 113 files changed, 2860 insertions(+), 411 deletions(-) create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/index.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/layout.tsx create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/index.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/cpu.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/disk_io_read_bytes.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/disk_io_write_bytes.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/rx.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/tx.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/tsvb/aws_ec2_cpu_utilization.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/tsvb/aws_ec2_diskio_bytes.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/tsvb/aws_ec2_network_traffic.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/toolbar_items.tsx create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/index.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/layout.tsx create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/index.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/cpu.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_active_transactions.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_connections.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_latency.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_queries_executed.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_active_transactions.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_connections.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_cpu_total.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_latency.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_queries_executed.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/toolbar_items.tsx create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/index.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/layout.tsx create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/index.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_bucket_size.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_download_bytes.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_number_of_objects.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_total_requests.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_upload_bytes.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_bucket_size.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_download_bytes.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_number_of_objects.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_total_requests.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_upload_bytes.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/toolbar_items.tsx create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/index.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/layout.tsx create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/index.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_delayed.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_empty.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_sent.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_visible.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_oldest_message.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_delayed.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_empty.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_sent.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_visible.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_oldest_message.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/toolbar_items.tsx create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/create_tsvb_model.ts create mode 100644 x-pack/legacy/plugins/infra/common/inventory_models/shared/compontents/metrics_and_groupby_toolbar_items.tsx create mode 100644 x-pack/legacy/plugins/infra/public/utils/formatters/high_precision.ts create mode 100644 x-pack/legacy/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts create mode 100644 x-pack/legacy/plugins/infra/server/lib/snapshot/types.ts delete mode 100644 x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_id_field_name.ts diff --git a/x-pack/legacy/plugins/infra/common/ecs_allowed_list.ts b/x-pack/legacy/plugins/infra/common/ecs_allowed_list.ts index 37b037214bb02..1728cd1fa4b45 100644 --- a/x-pack/legacy/plugins/infra/common/ecs_allowed_list.ts +++ b/x-pack/legacy/plugins/infra/common/ecs_allowed_list.ts @@ -45,6 +45,8 @@ export const DOCKER_ALLOWED_LIST = [ 'docker.container.labels', ]; +export const AWS_S3_ALLOWED_LIST = ['aws.s3']; + export const getAllowedListForPrefix = (prefix: string) => { const firstPart = first(prefix.split(/\./)); const defaultAllowedList = prefix ? [...ECS_ALLOWED_LIST, prefix] : ECS_ALLOWED_LIST; @@ -55,6 +57,10 @@ export const getAllowedListForPrefix = (prefix: string) => { return [...defaultAllowedList, ...PROMETHEUS_ALLOWED_LIST]; case 'kubernetes': return [...defaultAllowedList, ...K8S_ALLOWED_LIST]; + case 'aws': + if (prefix === 'aws.s3_daily_storage') { + return [...defaultAllowedList, ...AWS_S3_ALLOWED_LIST]; + } default: return defaultAllowedList; } diff --git a/x-pack/legacy/plugins/infra/common/graphql/shared/schema.gql.ts b/x-pack/legacy/plugins/infra/common/graphql/shared/schema.gql.ts index fd86e605b8747..071313817eff3 100644 --- a/x-pack/legacy/plugins/infra/common/graphql/shared/schema.gql.ts +++ b/x-pack/legacy/plugins/infra/common/graphql/shared/schema.gql.ts @@ -30,5 +30,9 @@ export const sharedSchema = gql` pod container host + awsEC2 + awsS3 + awsRDS + awsSQS } `; diff --git a/x-pack/legacy/plugins/infra/common/graphql/types.ts b/x-pack/legacy/plugins/infra/common/graphql/types.ts index 273cbfc1d4f89..0520409800bce 100644 --- a/x-pack/legacy/plugins/infra/common/graphql/types.ts +++ b/x-pack/legacy/plugins/infra/common/graphql/types.ts @@ -552,6 +552,10 @@ export enum InfraNodeType { pod = 'pod', container = 'container', host = 'host', + awsEC2 = 'awsEC2', + awsS3 = 'awsS3', + awsRDS = 'awsRDS', + awsSQS = 'awsSQS', } export enum InfraSnapshotMetricType { @@ -562,6 +566,22 @@ export enum InfraSnapshotMetricType { tx = 'tx', rx = 'rx', logRate = 'logRate', + diskIOReadBytes = 'diskIOReadBytes', + diskIOWriteBytes = 'diskIOWriteBytes', + s3TotalRequests = 's3TotalRequests', + s3NumberOfObjects = 's3NumberOfObjects', + s3BucketSize = 's3BucketSize', + s3DownloadBytes = 's3DownloadBytes', + s3UploadBytes = 's3UploadBytes', + rdsConnections = 'rdsConnections', + rdsQueriesExecuted = 'rdsQueriesExecuted', + rdsActiveTransactions = 'rdsActiveTransactions', + rdsLatency = 'rdsLatency', + sqsMessagesVisible = 'sqsOldestMessage', + sqsMessagesDelayed = 'sqsMessagesDelayed', + sqsMessagesSent = 'sqsMessagesSent', + sqsMessagesEmpty = 'sqsMessagesEmpty', + sqsOldestMessage = 'sqsOldestMessage', } export enum InfraMetric { @@ -602,6 +622,24 @@ export enum InfraMetric { awsNetworkPackets = 'awsNetworkPackets', awsDiskioBytes = 'awsDiskioBytes', awsDiskioOps = 'awsDiskioOps', + awsEC2CpuUtilization = 'awsEC2CpuUtilization', + awsEC2DiskIOBytes = 'awsEC2DiskIOBytes', + awsEC2NetworkTraffic = 'awsEC2NetworkTraffic', + awsS3TotalRequests = 'awsS3TotalRequests', + awsS3NumberOfObjects = 'awsS3NumberOfObjects', + awsS3BucketSize = 'awsS3BucketSize', + awsS3DownloadBytes = 'awsS3DownloadBytes', + awsS3UploadBytes = 'awsS3UploadBytes', + awsRDSCpuTotal = 'awsRDSCpuTotal', + awsRDSConnections = 'awsRDSConnections', + awsRDSQueriesExecuted = 'awsRDSQueriesExecuted', + awsRDSActiveTransactions = 'awsRDSActiveTransactions', + awsRDSLatency = 'awsRDSLatency', + awsSQSMessagesVisible = 'awsSQSMessagesVisible', + awsSQSMessagesDelayed = 'awsSQSMessagesDelayed', + awsSQSMessagesSent = 'awsSQSMessagesSent', + awsSQSMessagesEmpty = 'awsSQSMessagesEmpty', + awsSQSOldestMessage = 'awsSQSOldestMessage', custom = 'custom', } diff --git a/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts b/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts index ace61e13193c8..7fc3c3e876f08 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts @@ -5,16 +5,11 @@ */ import * as rt from 'io-ts'; - -export const InfraMetadataNodeTypeRT = rt.keyof({ - host: null, - pod: null, - container: null, -}); +import { ItemTypeRT } from '../../common/inventory_models/types'; export const InfraMetadataRequestRT = rt.type({ nodeId: rt.string, - nodeType: InfraMetadataNodeTypeRT, + nodeType: ItemTypeRT, sourceId: rt.string, }); @@ -96,5 +91,3 @@ export type InfraMetadataMachine = rt.TypeOf; export type InfraMetadataHost = rt.TypeOf; export type InfraMetadataOS = rt.TypeOf; - -export type InfraMetadataNodeType = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/index.ts new file mode 100644 index 0000000000000..ba4a6bb22c184 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/index.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { metrics } from './metrics'; +import { InventoryModel } from '../types'; + +export const awsEC2: InventoryModel = { + id: 'awsEC2', + displayName: i18n.translate('xpack.infra.inventoryModels.awsEC2.displayName', { + defaultMessage: 'EC2 Instances', + }), + requiredModules: ['aws'], + crosslinkSupport: { + details: true, + logs: true, + apm: true, + uptime: true, + }, + metrics, + fields: { + id: 'cloud.instance.id', + name: 'cloud.instance.name', + ip: 'aws.ec2.instance.public.ip', + }, + requiredMetrics: ['awsEC2CpuUtilization', 'awsEC2NetworkTraffic', 'awsEC2DiskIOBytes'], +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/layout.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/layout.tsx new file mode 100644 index 0000000000000..01009b478951a --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/layout.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; +import { Section } from '../../../public/pages/metrics/components/section'; +import { SubSection } from '../../../public/pages/metrics/components/sub_section'; +import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; +import { withTheme } from '../../../../../common/eui_styled_components'; + +export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( + +
+ + + + + + + + + +
+
+)); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/index.ts new file mode 100644 index 0000000000000..18b7cca2048a5 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { cpu } from './snapshot/cpu'; +import { rx } from './snapshot/rx'; +import { tx } from './snapshot/tx'; +import { diskIOReadBytes } from './snapshot/disk_io_read_bytes'; +import { diskIOWriteBytes } from './snapshot/disk_io_write_bytes'; + +import { awsEC2CpuUtilization } from './tsvb/aws_ec2_cpu_utilization'; +import { awsEC2NetworkTraffic } from './tsvb/aws_ec2_network_traffic'; +import { awsEC2DiskIOBytes } from './tsvb/aws_ec2_diskio_bytes'; + +import { InventoryMetrics } from '../../types'; + +export const metrics: InventoryMetrics = { + tsvb: { + awsEC2CpuUtilization, + awsEC2NetworkTraffic, + awsEC2DiskIOBytes, + }, + snapshot: { cpu, rx, tx, diskIOReadBytes, diskIOWriteBytes }, + defaultSnapshot: 'cpu', + defaultTimeRangeInSeconds: 14400, // 4 hours +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/cpu.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/cpu.ts new file mode 100644 index 0000000000000..483d9de784919 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/cpu.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SnapshotModel } from '../../../types'; + +export const cpu: SnapshotModel = { + cpu_avg: { + avg: { + field: 'aws.ec2.cpu.total.pct', + }, + }, + cpu: { + bucket_script: { + buckets_path: { + cpu: 'cpu_avg', + }, + script: { + source: 'params.cpu / 100', + lang: 'painless', + }, + gap_policy: 'skip', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/disk_io_read_bytes.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/disk_io_read_bytes.ts new file mode 100644 index 0000000000000..48e4a9eb59fad --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/disk_io_read_bytes.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SnapshotModel } from '../../../types'; + +export const diskIOReadBytes: SnapshotModel = { + diskIOReadBytes: { + avg: { + field: 'aws.ec2.diskio.read.bytes_per_sec', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/disk_io_write_bytes.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/disk_io_write_bytes.ts new file mode 100644 index 0000000000000..deadaa8c4a776 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/disk_io_write_bytes.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SnapshotModel } from '../../../types'; + +export const diskIOWriteBytes: SnapshotModel = { + diskIOWriteBytes: { + avg: { + field: 'aws.ec2.diskio.write.bytes_per_sec', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/rx.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/rx.ts new file mode 100644 index 0000000000000..2b857ce9b338a --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/rx.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SnapshotModel } from '../../../types'; + +export const rx: SnapshotModel = { + rx: { + avg: { + field: 'aws.ec2.network.in.bytes_per_sec', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/tx.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/tx.ts new file mode 100644 index 0000000000000..63c9da8ea1888 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/tx.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SnapshotModel } from '../../../types'; + +export const tx: SnapshotModel = { + tx: { + avg: { + field: 'aws.ec2.network.in.bytes_per_sec', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/tsvb/aws_ec2_cpu_utilization.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/tsvb/aws_ec2_cpu_utilization.ts new file mode 100644 index 0000000000000..a7a06ef1cfc1d --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/tsvb/aws_ec2_cpu_utilization.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsEC2CpuUtilization = createTSVBModel( + 'awsEC2CpuUtilization', + ['aws.ec2'], + [ + { + id: 'total', + split_mode: 'everything', + metrics: [ + { + field: 'aws.ec2.cpu.total.pct', + id: 'avg-cpu', + type: 'avg', + }, + { + id: 'convert-to-percent', + script: 'params.avg / 100', + type: 'calculation', + variables: [ + { + field: 'avg-cpu', + id: 'var-avg', + name: 'avg', + }, + ], + }, + ], + }, + ] +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/tsvb/aws_ec2_diskio_bytes.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/tsvb/aws_ec2_diskio_bytes.ts new file mode 100644 index 0000000000000..35d165936211a --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/tsvb/aws_ec2_diskio_bytes.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createTSVBModel } from '../../../create_tsvb_model'; +export const awsEC2DiskIOBytes = createTSVBModel( + 'awsEC2DiskIOBytes', + ['aws.ec2'], + [ + { + id: 'write', + split_mode: 'everything', + metrics: [ + { + field: 'aws.ec2.diskio.write.bytes_per_sec', + id: 'avg-write', + type: 'avg', + }, + ], + }, + { + id: 'read', + split_mode: 'everything', + metrics: [ + { + field: 'aws.ec2.diskio.read.bytes_per_sec', + id: 'avg-read', + type: 'avg', + }, + { + id: 'calculation-rate', + type: 'calculation', + variables: [{ id: 'rate-var', name: 'rate', field: 'avg-read' }], + script: 'params.rate * -1', + }, + ], + }, + ] +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/tsvb/aws_ec2_network_traffic.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/tsvb/aws_ec2_network_traffic.ts new file mode 100644 index 0000000000000..ea4b41d0bcd68 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/tsvb/aws_ec2_network_traffic.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createTSVBModel } from '../../../create_tsvb_model'; +export const awsEC2NetworkTraffic = createTSVBModel( + 'awsEC2NetworkTraffic', + ['aws.ec2'], + [ + { + id: 'tx', + split_mode: 'everything', + metrics: [ + { + field: 'aws.ec2.network.out.bytes_per_sec', + id: 'avg-tx', + type: 'avg', + }, + ], + }, + { + id: 'rx', + split_mode: 'everything', + metrics: [ + { + field: 'aws.ec2.network.in.bytes_per_sec', + id: 'avg-rx', + type: 'avg', + }, + { + id: 'calculation-rate', + type: 'calculation', + variables: [{ id: 'rate-var', name: 'rate', field: 'avg-rx' }], + script: 'params.rate * -1', + }, + ], + }, + ] +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/toolbar_items.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/toolbar_items.tsx new file mode 100644 index 0000000000000..fc09f0761a522 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/toolbar_items.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; +import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; +import { InfraSnapshotMetricType } from '../../graphql/types'; + +export const AwsEC2ToolbarItems = (props: ToolbarProps) => { + const metricTypes = [ + InfraSnapshotMetricType.cpu, + InfraSnapshotMetricType.rx, + InfraSnapshotMetricType.tx, + InfraSnapshotMetricType.diskIOReadBytes, + InfraSnapshotMetricType.diskIOWriteBytes, + ]; + const groupByFields = [ + 'cloud.availability_zone', + 'cloud.machine.type', + 'aws.ec2.instance.image.id', + 'aws.ec2.instance.state.name', + ]; + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/index.ts new file mode 100644 index 0000000000000..e81dee504b064 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/index.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { metrics } from './metrics'; +import { InventoryModel } from '../types'; + +export const awsRDS: InventoryModel = { + id: 'awsRDS', + displayName: i18n.translate('xpack.infra.inventoryModels.awsRDS.displayName', { + defaultMessage: 'RDS Databases', + }), + requiredModules: ['aws'], + crosslinkSupport: { + details: true, + logs: true, + apm: false, + uptime: false, + }, + metrics, + fields: { + id: 'aws.rds.db_instance.arn', + name: 'aws.rds.db_instance.identifier', + }, + requiredMetrics: [ + 'awsRDSCpuTotal', + 'awsRDSConnections', + 'awsRDSQueriesExecuted', + 'awsRDSActiveTransactions', + 'awsRDSLatency', + ], +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/layout.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/layout.tsx new file mode 100644 index 0000000000000..5f1185666a35d --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/layout.tsx @@ -0,0 +1,180 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; +import { Section } from '../../../public/pages/metrics/components/section'; +import { SubSection } from '../../../public/pages/metrics/components/sub_section'; +import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; +import { withTheme } from '../../../../../common/eui_styled_components'; + +export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( + +
+ + + + + + + + + + + + + + + +
+
+)); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/index.ts new file mode 100644 index 0000000000000..eaded5d8df223 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/index.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InventoryMetrics } from '../../types'; + +import { cpu } from './snapshot/cpu'; +import { rdsLatency } from './snapshot/rds_latency'; +import { rdsConnections } from './snapshot/rds_connections'; +import { rdsQueriesExecuted } from './snapshot/rds_queries_executed'; +import { rdsActiveTransactions } from './snapshot/rds_active_transactions'; + +import { awsRDSLatency } from './tsvb/aws_rds_latency'; +import { awsRDSConnections } from './tsvb/aws_rds_connections'; +import { awsRDSCpuTotal } from './tsvb/aws_rds_cpu_total'; +import { awsRDSQueriesExecuted } from './tsvb/aws_rds_queries_executed'; +import { awsRDSActiveTransactions } from './tsvb/aws_rds_active_transactions'; + +export const metrics: InventoryMetrics = { + tsvb: { + awsRDSLatency, + awsRDSConnections, + awsRDSCpuTotal, + awsRDSQueriesExecuted, + awsRDSActiveTransactions, + }, + snapshot: { + cpu, + rdsLatency, + rdsConnections, + rdsQueriesExecuted, + rdsActiveTransactions, + }, + defaultSnapshot: 'cpu', + defaultTimeRangeInSeconds: 14400, // 4 hours +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/cpu.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/cpu.ts new file mode 100644 index 0000000000000..e277b3b11958b --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/cpu.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SnapshotModel } from '../../../types'; + +export const cpu: SnapshotModel = { + cpu_avg: { + avg: { + field: 'aws.rds.cpu.total.pct', + }, + }, + cpu: { + bucket_script: { + buckets_path: { + cpu: 'cpu_avg', + }, + script: { + source: 'params.cpu / 100', + lang: 'painless', + }, + gap_policy: 'skip', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_active_transactions.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_active_transactions.ts new file mode 100644 index 0000000000000..be3dba100ba29 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_active_transactions.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SnapshotModel } from '../../../types'; + +export const rdsActiveTransactions: SnapshotModel = { + rdsActiveTransactions: { + avg: { + field: 'aws.rds.transactions.active', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_connections.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_connections.ts new file mode 100644 index 0000000000000..c7855d5548eea --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_connections.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SnapshotModel } from '../../../types'; + +export const rdsConnections: SnapshotModel = { + rdsConnections: { + avg: { + field: 'aws.rds.database_connections', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_latency.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_latency.ts new file mode 100644 index 0000000000000..2997b54d2f92e --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_latency.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SnapshotModel } from '../../../types'; + +export const rdsLatency: SnapshotModel = { + rdsLatency: { + avg: { + field: 'aws.rds.latency.dml', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_queries_executed.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_queries_executed.ts new file mode 100644 index 0000000000000..18e6538fb1e1e --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_queries_executed.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SnapshotModel } from '../../../types'; + +export const rdsQueriesExecuted: SnapshotModel = { + rdsQueriesExecuted: { + avg: { + field: 'aws.rds.queries', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_active_transactions.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_active_transactions.ts new file mode 100644 index 0000000000000..026cdeac40c36 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_active_transactions.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsRDSActiveTransactions = createTSVBModel( + 'awsRDSActiveTransactions', + ['aws.rds'], + [ + { + id: 'active', + split_mode: 'everything', + metrics: [ + { + field: 'aws.rds.transactions.active', + id: 'avg', + type: 'avg', + }, + ], + }, + { + id: 'blocked', + split_mode: 'everything', + metrics: [ + { + field: 'aws.rds.transactions.blocked', + id: 'avg', + type: 'avg', + }, + ], + }, + ] +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_connections.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_connections.ts new file mode 100644 index 0000000000000..145cc758e4a5b --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_connections.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsRDSConnections = createTSVBModel( + 'awsRDSConnections', + ['aws.rds'], + [ + { + id: 'connections', + split_mode: 'everything', + metrics: [ + { + field: 'aws.rds.database_connections', + id: 'avg-conns', + type: 'avg', + }, + ], + }, + ] +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_cpu_total.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_cpu_total.ts new file mode 100644 index 0000000000000..9a8eefc859bb0 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_cpu_total.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsRDSCpuTotal = createTSVBModel( + 'awsRDSCpuTotal', + ['aws.rds'], + [ + { + id: 'cpu', + split_mode: 'everything', + metrics: [ + { + field: 'aws.rds.cpu.total.pct', + id: 'avg-cpu', + type: 'avg', + }, + { + id: 'convert-to-percent', + script: 'params.avg / 100', + type: 'calculation', + variables: [ + { + field: 'avg-cpu', + id: 'var-avg', + name: 'avg', + }, + ], + }, + ], + }, + ] +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_latency.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_latency.ts new file mode 100644 index 0000000000000..80dffeeb717c6 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_latency.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsRDSLatency = createTSVBModel( + 'awsRDSLatency', + ['aws.rds'], + [ + { + id: 'read', + split_mode: 'everything', + metrics: [ + { + field: 'aws.rds.latency.read', + id: 'avg', + type: 'avg', + }, + ], + }, + { + id: 'write', + split_mode: 'everything', + metrics: [ + { + field: 'aws.rds.latency.write', + id: 'avg', + type: 'avg', + }, + ], + }, + { + id: 'insert', + split_mode: 'everything', + metrics: [ + { + field: 'aws.rds.latency.insert', + id: 'avg', + type: 'avg', + }, + ], + }, + { + id: 'update', + split_mode: 'everything', + metrics: [ + { + field: 'aws.rds.latency.update', + id: 'avg', + type: 'avg', + }, + ], + }, + { + id: 'commit', + split_mode: 'everything', + metrics: [ + { + field: 'aws.rds.latency.commit', + id: 'avg', + type: 'avg', + }, + ], + }, + ] +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_queries_executed.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_queries_executed.ts new file mode 100644 index 0000000000000..4dd1a1e89a21a --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_queries_executed.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createTSVBModel } from '../../../create_tsvb_model'; +export const awsRDSQueriesExecuted = createTSVBModel( + 'awsRDSQueriesExecuted', + ['aws.rds'], + [ + { + id: 'queries', + split_mode: 'everything', + metrics: [ + { + field: 'aws.rds.queries', + id: 'avg-queries', + type: 'avg', + }, + ], + }, + ] +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/toolbar_items.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/toolbar_items.tsx new file mode 100644 index 0000000000000..b60d6292c9c0f --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/toolbar_items.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; +import { InfraSnapshotMetricType } from '../../../public/graphql/types'; +import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; + +export const AwsRDSToolbarItems = (props: ToolbarProps) => { + const metricTypes = [ + InfraSnapshotMetricType.cpu, + InfraSnapshotMetricType.rdsConnections, + InfraSnapshotMetricType.rdsQueriesExecuted, + InfraSnapshotMetricType.rdsActiveTransactions, + InfraSnapshotMetricType.rdsLatency, + ]; + const groupByFields = [ + 'cloud.availability_zone', + 'aws.rds.db_instance.class', + 'aws.rds.db_instance.status', + ]; + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/index.ts new file mode 100644 index 0000000000000..c5de4ed80a1cb --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/index.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { metrics } from './metrics'; +import { InventoryModel } from '../types'; + +export const awsS3: InventoryModel = { + id: 'awsS3', + displayName: i18n.translate('xpack.infra.inventoryModels.awsS3.displayName', { + defaultMessage: 'S3 Buckets', + }), + requiredModules: ['aws'], + crosslinkSupport: { + details: true, + logs: true, + apm: false, + uptime: false, + }, + metrics, + fields: { + id: 'aws.s3.bucket.name', + name: 'aws.s3.bucket.name', + }, + requiredMetrics: [ + 'awsS3BucketSize', + 'awsS3NumberOfObjects', + 'awsS3TotalRequests', + 'awsS3DownloadBytes', + 'awsS3UploadBytes', + ], +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/layout.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/layout.tsx new file mode 100644 index 0000000000000..80089f15b04b2 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/layout.tsx @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; +import { Section } from '../../../public/pages/metrics/components/section'; +import { SubSection } from '../../../public/pages/metrics/components/sub_section'; +import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; +import { withTheme } from '../../../../../common/eui_styled_components'; + +export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( + +
+ + + + + + + + + + + + + + + +
+
+)); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/index.ts new file mode 100644 index 0000000000000..5aa974c16feec --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/index.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InventoryMetrics } from '../../types'; + +import { awsS3BucketSize } from './tsvb/aws_s3_bucket_size'; +import { awsS3TotalRequests } from './tsvb/aws_s3_total_requests'; +import { awsS3NumberOfObjects } from './tsvb/aws_s3_number_of_objects'; +import { awsS3DownloadBytes } from './tsvb/aws_s3_download_bytes'; +import { awsS3UploadBytes } from './tsvb/aws_s3_upload_bytes'; + +import { s3BucketSize } from './snapshot/s3_bucket_size'; +import { s3TotalRequests } from './snapshot/s3_total_requests'; +import { s3NumberOfObjects } from './snapshot/s3_number_of_objects'; +import { s3DownloadBytes } from './snapshot/s3_download_bytes'; +import { s3UploadBytes } from './snapshot/s3_upload_bytes'; + +export const metrics: InventoryMetrics = { + tsvb: { + awsS3BucketSize, + awsS3TotalRequests, + awsS3NumberOfObjects, + awsS3DownloadBytes, + awsS3UploadBytes, + }, + snapshot: { + s3BucketSize, + s3NumberOfObjects, + s3TotalRequests, + s3UploadBytes, + s3DownloadBytes, + }, + defaultSnapshot: 's3BucketSize', + defaultTimeRangeInSeconds: 86400 * 7, // 7 days +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_bucket_size.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_bucket_size.ts new file mode 100644 index 0000000000000..a99753a39c97c --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_bucket_size.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SnapshotModel } from '../../../types'; + +export const s3BucketSize: SnapshotModel = { + s3BucketSize: { + max: { + field: 'aws.s3_daily_storage.bucket.size.bytes', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_download_bytes.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_download_bytes.ts new file mode 100644 index 0000000000000..a0b23dadee37a --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_download_bytes.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SnapshotModel } from '../../../types'; + +export const s3DownloadBytes: SnapshotModel = { + s3DownloadBytes: { + max: { + field: 'aws.s3_request.downloaded.bytes', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_number_of_objects.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_number_of_objects.ts new file mode 100644 index 0000000000000..29162a59db47a --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_number_of_objects.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SnapshotModel } from '../../../types'; + +export const s3NumberOfObjects: SnapshotModel = { + s3NumberOfObjects: { + max: { + field: 'aws.s3_daily_storage.number_of_objects', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_total_requests.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_total_requests.ts new file mode 100644 index 0000000000000..bc57c6eb38234 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_total_requests.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SnapshotModel } from '../../../types'; + +export const s3TotalRequests: SnapshotModel = { + s3TotalRequests: { + max: { + field: 'aws.s3_request.requests.total', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_upload_bytes.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_upload_bytes.ts new file mode 100644 index 0000000000000..977d73254c3cd --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_upload_bytes.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SnapshotModel } from '../../../types'; + +export const s3UploadBytes: SnapshotModel = { + s3UploadBytes: { + max: { + field: 'aws.s3_request.uploaded.bytes', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_bucket_size.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_bucket_size.ts new file mode 100644 index 0000000000000..216f98b9e16b4 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_bucket_size.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsS3BucketSize = createTSVBModel( + 'awsS3BucketSize', + ['aws.s3_daily_storage'], + [ + { + id: 'bytes', + split_mode: 'everything', + metrics: [ + { + field: 'aws.s3_daily_storage.bucket.size.bytes', + id: 'max-bytes', + type: 'max', + }, + ], + }, + ], + '>=86400s', + false +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_download_bytes.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_download_bytes.ts new file mode 100644 index 0000000000000..15eb3130a5e23 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_download_bytes.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsS3DownloadBytes = createTSVBModel( + 'awsS3DownloadBytes', + ['aws.s3_request'], + [ + { + id: 'bytes', + split_mode: 'everything', + metrics: [ + { + field: 'aws.s3_request.downloaded.bytes', + id: 'max-bytes', + type: 'max', + }, + ], + }, + ], + '>=300s' +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_number_of_objects.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_number_of_objects.ts new file mode 100644 index 0000000000000..c108735bc0efd --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_number_of_objects.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsS3NumberOfObjects = createTSVBModel( + 'awsS3NumberOfObjects', + ['aws.s3_daily_storage'], + [ + { + id: 'objects', + split_mode: 'everything', + metrics: [ + { + field: 'aws.s3_daily_storage.number_of_objects', + id: 'max-size', + type: 'max', + }, + ], + }, + ], + '>=86400s', + false +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_total_requests.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_total_requests.ts new file mode 100644 index 0000000000000..311067fd96b47 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_total_requests.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsS3TotalRequests = createTSVBModel( + 'awsS3TotalRequests', + ['aws.s3_request'], + [ + { + id: 'total', + split_mode: 'everything', + metrics: [ + { + field: 'aws.s3_request.requests.total', + id: 'max-size', + type: 'max', + }, + ], + }, + ], + '>=300s' +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_upload_bytes.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_upload_bytes.ts new file mode 100644 index 0000000000000..ab66b47cfa781 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_upload_bytes.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsS3UploadBytes = createTSVBModel( + 'awsS3UploadBytes', + ['aws.s3_request'], + [ + { + id: 'bytes', + split_mode: 'everything', + metrics: [ + { + field: 'aws.s3_request.uploaded.bytes', + id: 'max-bytes', + type: 'max', + }, + ], + }, + ], + '>=300s' +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/toolbar_items.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/toolbar_items.tsx new file mode 100644 index 0000000000000..6764de237118a --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/toolbar_items.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; +import { InfraSnapshotMetricType } from '../../../public/graphql/types'; +import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; + +export const AwsS3ToolbarItems = (props: ToolbarProps) => { + const metricTypes = [ + InfraSnapshotMetricType.s3BucketSize, + InfraSnapshotMetricType.s3NumberOfObjects, + InfraSnapshotMetricType.s3TotalRequests, + InfraSnapshotMetricType.s3DownloadBytes, + InfraSnapshotMetricType.s3UploadBytes, + ]; + const groupByFields = ['cloud.region']; + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/index.ts new file mode 100644 index 0000000000000..d7fb7c7a615b1 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/index.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { metrics } from './metrics'; +import { InventoryModel } from '../types'; + +export const awsSQS: InventoryModel = { + id: 'awsSQS', + displayName: i18n.translate('xpack.infra.inventoryModels.awsSQS.displayName', { + defaultMessage: 'SQS Queues', + }), + requiredModules: ['aws'], + crosslinkSupport: { + details: true, + logs: true, + apm: false, + uptime: false, + }, + metrics, + fields: { + id: 'aws.sqs.queue.name', + name: 'aws.sqs.queue.name', + }, + requiredMetrics: [ + 'awsSQSMessagesVisible', + 'awsSQSMessagesDelayed', + 'awsSQSMessagesSent', + 'awsSQSMessagesEmpty', + 'awsSQSOldestMessage', + ], +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/layout.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/layout.tsx new file mode 100644 index 0000000000000..40cb0a64d83cc --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/layout.tsx @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; +import { Section } from '../../../public/pages/metrics/components/section'; +import { SubSection } from '../../../public/pages/metrics/components/sub_section'; +import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; +import { withTheme } from '../../../../../common/eui_styled_components'; + +export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( + +
+ + + + + + + + + + + + + + + +
+
+)); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/index.ts new file mode 100644 index 0000000000000..7bc593cc22035 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/index.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InventoryMetrics } from '../../types'; + +import { sqsMessagesVisible } from './snapshot/sqs_messages_visible'; +import { sqsMessagesDelayed } from './snapshot/sqs_messages_delayed'; +import { sqsMessagesEmpty } from './snapshot/sqs_messages_empty'; +import { sqsMessagesSent } from './snapshot/sqs_messages_sent'; +import { sqsOldestMessage } from './snapshot/sqs_oldest_message'; + +import { awsSQSMessagesVisible } from './tsvb/aws_sqs_messages_visible'; +import { awsSQSMessagesDelayed } from './tsvb/aws_sqs_messages_delayed'; +import { awsSQSMessagesSent } from './tsvb/aws_sqs_messages_sent'; +import { awsSQSMessagesEmpty } from './tsvb/aws_sqs_messages_empty'; +import { awsSQSOldestMessage } from './tsvb/aws_sqs_oldest_message'; + +export const metrics: InventoryMetrics = { + tsvb: { + awsSQSMessagesVisible, + awsSQSMessagesDelayed, + awsSQSMessagesSent, + awsSQSMessagesEmpty, + awsSQSOldestMessage, + }, + snapshot: { + sqsMessagesVisible, + sqsMessagesDelayed, + sqsMessagesEmpty, + sqsMessagesSent, + sqsOldestMessage, + }, + defaultSnapshot: 'sqsMessagesVisible', + defaultTimeRangeInSeconds: 14400, // 4 hours +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_delayed.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_delayed.ts new file mode 100644 index 0000000000000..679f86671725e --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_delayed.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SnapshotModel } from '../../../types'; + +export const sqsMessagesDelayed: SnapshotModel = { + sqsMessagesDelayed: { + max: { + field: 'aws.sqs.messages.delayed', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_empty.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_empty.ts new file mode 100644 index 0000000000000..d80a3f3451e1d --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_empty.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SnapshotModel } from '../../../types'; + +export const sqsMessagesEmpty: SnapshotModel = { + sqsMessagesEmpty: { + max: { + field: 'aws.sqs.messages.not_visible', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_sent.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_sent.ts new file mode 100644 index 0000000000000..3d6934bf3da85 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_sent.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SnapshotModel } from '../../../types'; + +export const sqsMessagesSent: SnapshotModel = { + sqsMessagesSent: { + max: { + field: 'aws.sqs.messages.sent', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_visible.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_visible.ts new file mode 100644 index 0000000000000..1a78c50cd7949 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_visible.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SnapshotModel } from '../../../types'; + +export const sqsMessagesVisible: SnapshotModel = { + sqsMessagesVisible: { + avg: { + field: 'aws.sqs.messages.visible', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_oldest_message.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_oldest_message.ts new file mode 100644 index 0000000000000..ae780069c8ca1 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_oldest_message.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SnapshotModel } from '../../../types'; + +export const sqsOldestMessage: SnapshotModel = { + sqsOldestMessage: { + max: { + field: 'aws.sqs.oldest_message_age.sec', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_delayed.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_delayed.ts new file mode 100644 index 0000000000000..469b9ddd33953 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_delayed.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsSQSMessagesDelayed = createTSVBModel( + 'awsSQSMessagesDelayed', + ['aws.sqs'], + [ + { + id: 'delayed', + split_mode: 'everything', + metrics: [ + { + field: 'aws.sqs.messages.delayed', + id: 'avg-delayed', + type: 'avg', + }, + ], + }, + ], + '>=300s' +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_empty.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_empty.ts new file mode 100644 index 0000000000000..54c9e503a8c8c --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_empty.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsSQSMessagesEmpty = createTSVBModel( + 'awsSQSMessagesEmpty', + ['aws.sqs'], + [ + { + id: 'empty', + split_mode: 'everything', + metrics: [ + { + field: 'aws.sqs.messages.not_visible', + id: 'avg-empty', + type: 'avg', + }, + ], + }, + ], + '>=300s' +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_sent.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_sent.ts new file mode 100644 index 0000000000000..98389ef22fbe8 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_sent.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsSQSMessagesSent = createTSVBModel( + 'awsSQSMessagesSent', + ['aws.sqs'], + [ + { + id: 'sent', + split_mode: 'everything', + metrics: [ + { + field: 'aws.sqs.messages.sent', + id: 'avg-sent', + type: 'avg', + }, + ], + }, + ], + '>=300s' +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_visible.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_visible.ts new file mode 100644 index 0000000000000..c96ab07e4ae75 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_visible.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsSQSMessagesVisible = createTSVBModel( + 'awsSQSMessagesVisible', + ['aws.sqs'], + [ + { + id: 'visible', + split_mode: 'everything', + metrics: [ + { + field: 'aws.sqs.messages.visible', + id: 'avg-visible', + type: 'avg', + }, + ], + }, + ], + '>=300s' +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_oldest_message.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_oldest_message.ts new file mode 100644 index 0000000000000..812906386fb67 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_oldest_message.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsSQSOldestMessage = createTSVBModel( + 'awsSQSOldestMessage', + ['aws.sqs'], + [ + { + id: 'oldest', + split_mode: 'everything', + metrics: [ + { + field: 'aws.sqs.oldest_message_age.sec', + id: 'max-oldest', + type: 'max', + }, + ], + }, + ], + '>=300s' +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/toolbar_items.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/toolbar_items.tsx new file mode 100644 index 0000000000000..89d372d6ac21c --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/toolbar_items.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; +import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; +import { InfraSnapshotMetricType } from '../../graphql/types'; + +export const AwsSQSToolbarItems = (props: ToolbarProps) => { + const metricTypes = [ + InfraSnapshotMetricType.sqsMessagesVisible, + InfraSnapshotMetricType.sqsMessagesDelayed, + InfraSnapshotMetricType.sqsMessagesSent, + InfraSnapshotMetricType.sqsMessagesEmpty, + InfraSnapshotMetricType.sqsOldestMessage, + ]; + const groupByFields = ['cloud.region']; + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/container/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/container/index.ts index 54fe938528d19..af7b6058ff174 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/container/index.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/container/index.ts @@ -4,12 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { metrics } from './metrics'; import { InventoryModel } from '../types'; export const container: InventoryModel = { id: 'container', + displayName: i18n.translate('xpack.infra.inventoryModel.container.displayName', { + defaultMessage: 'Docker Containers', + }), requiredModules: ['docker'], + crosslinkSupport: { + details: true, + logs: true, + apm: true, + uptime: true, + }, + fields: { + id: 'container.id', + name: 'container.name', + ip: 'continaer.ip_address', + }, metrics, requiredMetrics: [ 'containerOverview', diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/container/metrics/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/container/metrics/index.ts index 9e0153c5d6ea6..73a10cbadb66d 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/container/metrics/index.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/container/metrics/index.ts @@ -30,4 +30,5 @@ export const metrics: InventoryMetrics = { }, snapshot: { cpu, memory, rx, tx }, defaultSnapshot: 'cpu', + defaultTimeRangeInSeconds: 3600, // 1 hour }; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/container/toolbar_items.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/container/toolbar_items.tsx index ddb3c0491f164..9ed2cbe6dea08 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/container/toolbar_items.tsx +++ b/x-pack/legacy/plugins/infra/common/inventory_models/container/toolbar_items.tsx @@ -4,61 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; -import { EuiFlexItem } from '@elastic/eui'; +import React from 'react'; import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; -import { WaffleMetricControls } from '../../../public/components/waffle/waffle_metric_controls'; -import { WaffleGroupByControls } from '../../../public/components/waffle/waffle_group_by_controls'; -import { InfraSnapshotMetricType } from '../../../public/graphql/types'; -import { - toMetricOpt, - toGroupByOpt, -} from '../../../public/components/inventory/toolbars/toolbar_wrapper'; +import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; +import { InfraSnapshotMetricType } from '../../graphql/types'; export const ContainerToolbarItems = (props: ToolbarProps) => { - const options = useMemo( - () => - [ - InfraSnapshotMetricType.cpu, - InfraSnapshotMetricType.memory, - InfraSnapshotMetricType.rx, - InfraSnapshotMetricType.tx, - ].map(toMetricOpt), - [] - ); - - const groupByOptions = useMemo( - () => - [ - 'host.name', - 'cloud.availability_zone', - 'cloud.machine.type', - 'cloud.project.id', - 'cloud.provider', - 'service.type', - ].map(toGroupByOpt), - [] - ); + const metricTypes = [ + InfraSnapshotMetricType.cpu, + InfraSnapshotMetricType.memory, + InfraSnapshotMetricType.rx, + InfraSnapshotMetricType.tx, + ]; + const groupByFields = [ + 'host.name', + 'cloud.availability_zone', + 'cloud.machine.type', + 'cloud.project.id', + 'cloud.provider', + 'service.type', + ]; return ( - <> - - - - - - - + ); }; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/create_tsvb_model.ts b/x-pack/legacy/plugins/infra/common/inventory_models/create_tsvb_model.ts new file mode 100644 index 0000000000000..7036b2236881f --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/create_tsvb_model.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TSVBMetricModelCreator, TSVBMetricModel, TSVBSeries, InventoryMetric } from './types'; + +export const createTSVBModel = ( + id: InventoryMetric, + requires: string[], + series: TSVBSeries[], + interval = '>=300s', + dropLastBucket = true +): TSVBMetricModelCreator => (timeField, indexPattern): TSVBMetricModel => ({ + id, + requires, + drop_last_bucket: dropLastBucket, + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series, +}); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/host/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/host/index.ts index 08056e650a32e..54d3267eef57a 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/host/index.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/host/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { metrics } from './metrics'; import { InventoryModel } from '../types'; import { @@ -13,7 +14,21 @@ import { export const host: InventoryModel = { id: 'host', + displayName: i18n.translate('xpack.infra.inventoryModel.host.displayName', { + defaultMessage: 'Hosts', + }), requiredModules: ['system'], + crosslinkSupport: { + details: true, + logs: true, + apm: true, + uptime: true, + }, + fields: { + id: 'host.name', + name: 'host.name', + ip: 'host.ip', + }, metrics, requiredMetrics: [ 'hostSystemOverview', diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/host/metrics/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/host/metrics/index.ts index f4c0150309dd8..7f77f23e4fb95 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/host/metrics/index.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/host/metrics/index.ts @@ -52,4 +52,5 @@ export const metrics: InventoryMetrics = { }, snapshot: { count, cpu, load, logRate, memory, rx, tx }, defaultSnapshot: 'cpu', + defaultTimeRangeInSeconds: 3600, // 1 hour }; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/host/toolbar_items.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/host/toolbar_items.tsx index 8e1bb0dfb4816..f8df81a33a8ec 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/host/toolbar_items.tsx +++ b/x-pack/legacy/plugins/infra/common/inventory_models/host/toolbar_items.tsx @@ -4,63 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; -import { EuiFlexItem } from '@elastic/eui'; +import React from 'react'; import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; -import { WaffleMetricControls } from '../../../public/components/waffle/waffle_metric_controls'; -import { WaffleGroupByControls } from '../../../public/components/waffle/waffle_group_by_controls'; -import { InfraSnapshotMetricType } from '../../../public/graphql/types'; -import { - toGroupByOpt, - toMetricOpt, -} from '../../../public/components/inventory/toolbars/toolbar_wrapper'; +import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; +import { InfraSnapshotMetricType } from '../../graphql/types'; export const HostToolbarItems = (props: ToolbarProps) => { - const metricOptions = useMemo( - () => - [ - InfraSnapshotMetricType.cpu, - InfraSnapshotMetricType.memory, - InfraSnapshotMetricType.load, - InfraSnapshotMetricType.rx, - InfraSnapshotMetricType.tx, - InfraSnapshotMetricType.logRate, - ].map(toMetricOpt), - [] - ); - - const groupByOptions = useMemo( - () => - [ - 'cloud.availability_zone', - 'cloud.machine.type', - 'cloud.project.id', - 'cloud.provider', - 'service.type', - ].map(toGroupByOpt), - [] - ); - + const metricTypes = [ + InfraSnapshotMetricType.cpu, + InfraSnapshotMetricType.memory, + InfraSnapshotMetricType.load, + InfraSnapshotMetricType.rx, + InfraSnapshotMetricType.tx, + InfraSnapshotMetricType.logRate, + ]; + const groupByFields = [ + 'cloud.availability_zone', + 'cloud.machine.type', + 'cloud.project.id', + 'cloud.provider', + 'service.type', + ]; return ( - <> - - - - - - - + ); }; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/index.ts index 79aad7b2ccf6f..d9fd8fa465b7a 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/index.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/index.ts @@ -7,11 +7,15 @@ import { i18n } from '@kbn/i18n'; import { host } from './host'; import { pod } from './pod'; +import { awsEC2 } from './aws_ec2'; +import { awsS3 } from './aws_s3'; +import { awsRDS } from './aws_rds'; +import { awsSQS } from './aws_sqs'; import { container } from './container'; import { InventoryItemType } from './types'; export { metrics } from './metrics'; -const inventoryModels = [host, pod, container]; +export const inventoryModels = [host, pod, container, awsEC2, awsS3, awsRDS, awsSQS]; export const findInventoryModel = (type: InventoryItemType) => { const model = inventoryModels.find(m => m.id === type); @@ -24,3 +28,38 @@ export const findInventoryModel = (type: InventoryItemType) => { } return model; }; + +interface InventoryFields { + message: string[]; + host: string; + pod: string; + container: string; + timestamp: string; + tiebreaker: string; +} + +const LEGACY_TYPES = ['host', 'pod', 'container']; + +const getFieldByType = (type: InventoryItemType, fields: InventoryFields) => { + switch (type) { + case 'pod': + return fields.pod; + case 'host': + return fields.host; + case 'container': + return fields.container; + } +}; + +export const findInventoryFields = (type: InventoryItemType, fields: InventoryFields) => { + const inventoryModel = findInventoryModel(type); + if (LEGACY_TYPES.includes(type)) { + const id = getFieldByType(type, fields) || inventoryModel.fields.id; + return { + ...inventoryModel.fields, + id, + }; + } else { + return inventoryModel.fields; + } +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/layouts.ts b/x-pack/legacy/plugins/infra/common/inventory_models/layouts.ts index 0c593bec1af3a..d9008753adf7b 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/layouts.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/layouts.ts @@ -17,6 +17,10 @@ import { ReactNode, FunctionComponent } from 'react'; import { Layout as HostLayout } from './host/layout'; import { Layout as PodLayout } from './pod/layout'; import { Layout as ContainerLayout } from './container/layout'; +import { Layout as AwsEC2Layout } from './aws_ec2/layout'; +import { Layout as AwsS3Layout } from './aws_s3/layout'; +import { Layout as AwsRDSLayout } from './aws_rds/layout'; +import { Layout as AwsSQSLayout } from './aws_sqs/layout'; import { InventoryItemType } from './types'; import { LayoutProps } from '../../public/pages/metrics/types'; @@ -28,6 +32,10 @@ const layouts: Layouts = { host: HostLayout, pod: PodLayout, container: ContainerLayout, + awsEC2: AwsEC2Layout, + awsS3: AwsS3Layout, + awsRDS: AwsRDSLayout, + awsSQS: AwsSQSLayout, }; export const findLayout = (type: InventoryItemType) => { diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/metrics.ts b/x-pack/legacy/plugins/infra/common/inventory_models/metrics.ts index 78dc262b29bac..cadc059fc5aeb 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/metrics.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/metrics.ts @@ -8,6 +8,10 @@ import { metrics as hostMetrics } from './host/metrics'; import { metrics as sharedMetrics } from './shared/metrics'; import { metrics as podMetrics } from './pod/metrics'; import { metrics as containerMetrics } from './container/metrics'; +import { metrics as awsEC2Metrics } from './aws_ec2/metrics'; +import { metrics as awsS3Metrics } from './aws_s3/metrics'; +import { metrics as awsRDSMetrics } from './aws_rds/metrics'; +import { metrics as awsSQSMetrics } from './aws_sqs/metrics'; export const metrics = { tsvb: { @@ -15,5 +19,9 @@ export const metrics = { ...sharedMetrics.tsvb, ...podMetrics.tsvb, ...containerMetrics.tsvb, + ...awsEC2Metrics.tsvb, + ...awsS3Metrics.tsvb, + ...awsRDSMetrics.tsvb, + ...awsSQSMetrics.tsvb, }, }; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/pod/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/pod/index.ts index 66ace03abac00..3efc5827b4f23 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/pod/index.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/pod/index.ts @@ -4,13 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { metrics } from './metrics'; import { InventoryModel } from '../types'; import { nginx as nginxRequiredMetrics } from '../shared/metrics/required_metrics'; export const pod: InventoryModel = { id: 'pod', + displayName: i18n.translate('xpack.infra.inventoryModel.pod.displayName', { + defaultMessage: 'Kubernetes Pods', + }), requiredModules: ['kubernetes'], + crosslinkSupport: { + details: true, + logs: true, + apm: true, + uptime: true, + }, + fields: { + id: 'kubernetes.pod.uid', + name: 'kubernetes.pod.name', + ip: 'kubernetes.pod.ip', + }, metrics, requiredMetrics: [ 'podOverview', diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/pod/metrics/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/pod/metrics/index.ts index 2aa7ac6b496af..b4420b5532cc6 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/pod/metrics/index.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/pod/metrics/index.ts @@ -26,4 +26,5 @@ export const metrics: InventoryMetrics = { }, snapshot: { cpu, memory, rx, tx }, defaultSnapshot: 'cpu', + defaultTimeRangeInSeconds: 3600, // 1 hour }; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/pod/toolbar_items.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/pod/toolbar_items.tsx index cc0676fc60ae4..9ef4a889dc589 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/pod/toolbar_items.tsx +++ b/x-pack/legacy/plugins/infra/common/inventory_models/pod/toolbar_items.tsx @@ -4,54 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; -import { EuiFlexItem } from '@elastic/eui'; +import React from 'react'; import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; -import { WaffleMetricControls } from '../../../public/components/waffle/waffle_metric_controls'; -import { WaffleGroupByControls } from '../../../public/components/waffle/waffle_group_by_controls'; -import { InfraSnapshotMetricType } from '../../../public/graphql/types'; -import { - toGroupByOpt, - toMetricOpt, -} from '../../../public/components/inventory/toolbars/toolbar_wrapper'; +import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; +import { InfraSnapshotMetricType } from '../../graphql/types'; export const PodToolbarItems = (props: ToolbarProps) => { - const options = useMemo( - () => - [ - InfraSnapshotMetricType.cpu, - InfraSnapshotMetricType.memory, - InfraSnapshotMetricType.rx, - InfraSnapshotMetricType.tx, - ].map(toMetricOpt), - [] - ); - - const groupByOptions = useMemo( - () => ['kubernetes.namespace', 'kubernetes.node.name', 'service.type'].map(toGroupByOpt), - [] - ); - + const metricTypes = [ + InfraSnapshotMetricType.cpu, + InfraSnapshotMetricType.memory, + InfraSnapshotMetricType.rx, + InfraSnapshotMetricType.tx, + ]; + const groupByFields = ['kubernetes.namespace', 'kubernetes.node.name', 'service.type']; return ( - <> - - - - - - - + ); }; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/shared/compontents/metrics_and_groupby_toolbar_items.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/shared/compontents/metrics_and_groupby_toolbar_items.tsx new file mode 100644 index 0000000000000..c46ad5c6df952 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/shared/compontents/metrics_and_groupby_toolbar_items.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import { EuiFlexItem } from '@elastic/eui'; +import { ToolbarProps } from '../../../../public/components/inventory/toolbars/toolbar'; +import { WaffleMetricControls } from '../../../../public/components/waffle/waffle_metric_controls'; +import { WaffleGroupByControls } from '../../../../public/components/waffle/waffle_group_by_controls'; +import { InfraSnapshotMetricType } from '../../../../public/graphql/types'; +import { + toGroupByOpt, + toMetricOpt, +} from '../../../../public/components/inventory/toolbars/toolbar_wrapper'; + +interface Props extends ToolbarProps { + metricTypes: InfraSnapshotMetricType[]; + groupByFields: string[]; +} + +export const MetricsAndGroupByToolbarItems = (props: Props) => { + const metricOptions = useMemo(() => props.metricTypes.map(toMetricOpt), [props.metricTypes]); + + const groupByOptions = useMemo(() => props.groupByFields.map(toGroupByOpt), [ + props.groupByFields, + ]); + + return ( + <> + + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/shared/metrics/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/shared/metrics/index.ts index 6416aa08e8585..2bab5c5229c5b 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/shared/metrics/index.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/shared/metrics/index.ts @@ -35,4 +35,5 @@ export const metrics: InventoryMetrics = { count, }, defaultSnapshot: 'count', + defaultTimeRangeInSeconds: 3600, }; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/toolbars.ts b/x-pack/legacy/plugins/infra/common/inventory_models/toolbars.ts index dc3c409ac497e..05def078c7f2d 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/toolbars.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/toolbars.ts @@ -11,6 +11,10 @@ import { HostToolbarItems } from './host/toolbar_items'; import { ContainerToolbarItems } from './container/toolbar_items'; import { PodToolbarItems } from './pod/toolbar_items'; import { ToolbarProps } from '../../public/components/inventory/toolbars/toolbar'; +import { AwsEC2ToolbarItems } from './aws_ec2/toolbar_items'; +import { AwsS3ToolbarItems } from './aws_s3/toolbar_items'; +import { AwsRDSToolbarItems } from './aws_rds/toolbar_items'; +import { AwsSQSToolbarItems } from './aws_sqs/toolbar_items'; interface Toolbars { [type: string]: ReactNode; @@ -20,6 +24,10 @@ const toolbars: Toolbars = { host: HostToolbarItems, container: ContainerToolbarItems, pod: PodToolbarItems, + awsEC2: AwsEC2ToolbarItems, + awsS3: AwsS3ToolbarItems, + awsRDS: AwsRDSToolbarItems, + awsSQS: AwsSQSToolbarItems, }; export const findToolbar = (type: InventoryItemType) => { diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/types.ts b/x-pack/legacy/plugins/infra/common/inventory_models/types.ts index 93eaf214ad23e..e1cbdcb52ff27 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/types.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/types.ts @@ -30,6 +30,7 @@ export const InventoryFormatterTypeRT = rt.keyof({ bytes: null, number: null, percent: null, + highPercision: null, }); export type InventoryFormatterType = rt.TypeOf; export type InventoryItemType = rt.TypeOf; @@ -72,6 +73,24 @@ export const InventoryMetricRT = rt.keyof({ awsNetworkPackets: null, awsDiskioBytes: null, awsDiskioOps: null, + awsEC2CpuUtilization: null, + awsEC2NetworkTraffic: null, + awsEC2DiskIOBytes: null, + awsS3TotalRequests: null, + awsS3NumberOfObjects: null, + awsS3BucketSize: null, + awsS3DownloadBytes: null, + awsS3UploadBytes: null, + awsRDSCpuTotal: null, + awsRDSConnections: null, + awsRDSQueriesExecuted: null, + awsRDSActiveTransactions: null, + awsRDSLatency: null, + awsSQSMessagesVisible: null, + awsSQSMessagesDelayed: null, + awsSQSMessagesSent: null, + awsSQSMessagesEmpty: null, + awsSQSOldestMessage: null, custom: null, }); export type InventoryMetric = rt.TypeOf; @@ -162,6 +181,8 @@ export const TSVBSeriesRT = rt.intersection([ }), ]); +export type TSVBSeries = rt.TypeOf; + export const TSVBMetricModelRT = rt.intersection([ rt.type({ id: InventoryMetricRT, @@ -176,6 +197,7 @@ export const TSVBMetricModelRT = rt.intersection([ filter: rt.string, map_field_to: rt.string, id_type: rt.keyof({ cloud: null, node: null }), + drop_last_bucket: rt.boolean, }), ]); @@ -267,6 +289,22 @@ export const SnapshotMetricTypeRT = rt.keyof({ tx: null, rx: null, logRate: null, + diskIOReadBytes: null, + diskIOWriteBytes: null, + s3TotalRequests: null, + s3NumberOfObjects: null, + s3BucketSize: null, + s3DownloadBytes: null, + s3UploadBytes: null, + rdsConnections: null, + rdsQueriesExecuted: null, + rdsActiveTransactions: null, + rdsLatency: null, + sqsMessagesVisible: null, + sqsMessagesDelayed: null, + sqsMessagesSent: null, + sqsMessagesEmpty: null, + sqsOldestMessage: null, }); export type SnapshotMetricType = rt.TypeOf; @@ -275,11 +313,25 @@ export interface InventoryMetrics { tsvb: { [name: string]: TSVBMetricModelCreator }; snapshot: { [name: string]: SnapshotModel }; defaultSnapshot: SnapshotMetricType; + /** This is used by the inventory view to calculate the appropriate amount of time for the metrics detail page. Some metris like awsS3 require multiple days where others like host only need an hour.*/ + defaultTimeRangeInSeconds: number; } export interface InventoryModel { id: string; + displayName: string; requiredModules: string[]; + fields: { + id: string; + name: string; + ip?: string; + }; + crosslinkSupport: { + details: boolean; + logs: boolean; + apm: boolean; + uptime: boolean; + }; metrics: InventoryMetrics; requiredMetrics: InventoryMetric[]; } diff --git a/x-pack/legacy/plugins/infra/public/components/inventory/layout.tsx b/x-pack/legacy/plugins/infra/public/components/inventory/layout.tsx index 47858624fde0f..cb48c99963d17 100644 --- a/x-pack/legacy/plugins/infra/public/components/inventory/layout.tsx +++ b/x-pack/legacy/plugins/infra/public/components/inventory/layout.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { InfraWaffleMapOptions, InfraWaffleMapBounds } from '../../lib/lib'; import { InfraNodeType, - InfraTimerangeInput, InfraSnapshotMetricInput, InfraSnapshotGroupbyInput, } from '../../graphql/types'; @@ -23,7 +22,7 @@ export interface LayoutProps { options: InfraWaffleMapOptions; nodeType: InfraNodeType; onDrilldown: (filter: KueryFilterQuery) => void; - timeRange: InfraTimerangeInput; + currentTime: number; onViewChange: (view: string) => void; view: string; boundsOverride: InfraWaffleMapBounds; @@ -42,7 +41,7 @@ export const Layout = (props: LayoutProps) => { props.groupBy, props.nodeType, props.sourceId, - props.timeRange + props.currentTime ); return ( <> @@ -55,7 +54,7 @@ export const Layout = (props: LayoutProps) => { loading={loading} reload={reload} onDrilldown={props.onDrilldown} - timeRange={props.timeRange} + currentTime={props.currentTime} onViewChange={props.onViewChange} view={props.view} autoBounds={props.autoBounds} diff --git a/x-pack/legacy/plugins/infra/public/components/inventory/toolbars/toolbar_wrapper.tsx b/x-pack/legacy/plugins/infra/public/components/inventory/toolbars/toolbar_wrapper.tsx index 7cb86f6e4d0ec..c4721fee4b746 100644 --- a/x-pack/legacy/plugins/infra/public/components/inventory/toolbars/toolbar_wrapper.tsx +++ b/x-pack/legacy/plugins/infra/public/components/inventory/toolbars/toolbar_wrapper.tsx @@ -81,6 +81,54 @@ const ToolbarTranslations = { Count: i18n.translate('xpack.infra.waffle.metricOptions.countText', { defaultMessage: 'Count', }), + DiskIOReadBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOReadBytes', { + defaultMessage: 'Disk Reads', + }), + DiskIOWriteBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOWriteBytes', { + defaultMessage: 'Disk Writes', + }), + s3BucketSize: i18n.translate('xpack.infra.waffle.metricOptions.s3BucketSize', { + defaultMessage: 'Bucket Size', + }), + s3TotalRequests: i18n.translate('xpack.infra.waffle.metricOptions.s3TotalRequests', { + defaultMessage: 'Total Requests', + }), + s3NumberOfObjects: i18n.translate('xpack.infra.waffle.metricOptions.s3NumberOfObjects', { + defaultMessage: 'Number of Objects', + }), + s3DownloadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3DownloadBytes', { + defaultMessage: 'Downloads (Bytes)', + }), + s3UploadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3UploadBytes', { + defaultMessage: 'Uploads (Bytes)', + }), + rdsConnections: i18n.translate('xpack.infra.waffle.metricOptions.rdsConnections', { + defaultMessage: 'Connections', + }), + rdsQueriesExecuted: i18n.translate('xpack.infra.waffle.metricOptions.rdsQueriesExecuted', { + defaultMessage: 'Queries Executed', + }), + rdsActiveTransactions: i18n.translate('xpack.infra.waffle.metricOptions.rdsActiveTransactions', { + defaultMessage: 'Active Transactions', + }), + rdsLatency: i18n.translate('xpack.infra.waffle.metricOptions.rdsLatency', { + defaultMessage: 'Latency', + }), + sqsMessagesVisible: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesVisible', { + defaultMessage: 'Messages Available', + }), + sqsMessagesDelayed: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesDelayed', { + defaultMessage: 'Messages Delayed', + }), + sqsMessagesSent: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesSent', { + defaultMessage: 'Messages Added', + }), + sqsMessagesEmpty: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesEmpty', { + defaultMessage: 'Messages Returned Empty', + }), + sqsOldestMessage: i18n.translate('xpack.infra.waffle.metricOptions.sqsOldestMessage', { + defaultMessage: 'Oldest Message', + }), }; export const toGroupByOpt = (field: string) => ({ @@ -126,5 +174,85 @@ export const toMetricOpt = (metric: InfraSnapshotMetricType) => { text: ToolbarTranslations.Count, value: InfraSnapshotMetricType.count, }; + case InfraSnapshotMetricType.diskIOReadBytes: + return { + text: ToolbarTranslations.DiskIOReadBytes, + value: InfraSnapshotMetricType.diskIOReadBytes, + }; + case InfraSnapshotMetricType.diskIOWriteBytes: + return { + text: ToolbarTranslations.DiskIOWriteBytes, + value: InfraSnapshotMetricType.diskIOWriteBytes, + }; + case InfraSnapshotMetricType.s3BucketSize: + return { + text: ToolbarTranslations.s3BucketSize, + value: InfraSnapshotMetricType.s3BucketSize, + }; + case InfraSnapshotMetricType.s3TotalRequests: + return { + text: ToolbarTranslations.s3TotalRequests, + value: InfraSnapshotMetricType.s3TotalRequests, + }; + case InfraSnapshotMetricType.s3NumberOfObjects: + return { + text: ToolbarTranslations.s3NumberOfObjects, + value: InfraSnapshotMetricType.s3NumberOfObjects, + }; + case InfraSnapshotMetricType.s3DownloadBytes: + return { + text: ToolbarTranslations.s3DownloadBytes, + value: InfraSnapshotMetricType.s3DownloadBytes, + }; + case InfraSnapshotMetricType.s3UploadBytes: + return { + text: ToolbarTranslations.s3UploadBytes, + value: InfraSnapshotMetricType.s3UploadBytes, + }; + case InfraSnapshotMetricType.rdsConnections: + return { + text: ToolbarTranslations.rdsConnections, + value: InfraSnapshotMetricType.rdsConnections, + }; + case InfraSnapshotMetricType.rdsQueriesExecuted: + return { + text: ToolbarTranslations.rdsQueriesExecuted, + value: InfraSnapshotMetricType.rdsQueriesExecuted, + }; + case InfraSnapshotMetricType.rdsActiveTransactions: + return { + text: ToolbarTranslations.rdsActiveTransactions, + value: InfraSnapshotMetricType.rdsActiveTransactions, + }; + case InfraSnapshotMetricType.rdsLatency: + return { + text: ToolbarTranslations.rdsLatency, + value: InfraSnapshotMetricType.rdsLatency, + }; + case InfraSnapshotMetricType.sqsMessagesVisible: + return { + text: ToolbarTranslations.sqsMessagesVisible, + value: InfraSnapshotMetricType.sqsMessagesVisible, + }; + case InfraSnapshotMetricType.sqsMessagesDelayed: + return { + text: ToolbarTranslations.sqsMessagesDelayed, + value: InfraSnapshotMetricType.sqsMessagesDelayed, + }; + case InfraSnapshotMetricType.sqsMessagesSent: + return { + text: ToolbarTranslations.sqsMessagesSent, + value: InfraSnapshotMetricType.sqsMessagesSent, + }; + case InfraSnapshotMetricType.sqsMessagesEmpty: + return { + text: ToolbarTranslations.sqsMessagesEmpty, + value: InfraSnapshotMetricType.sqsMessagesEmpty, + }; + case InfraSnapshotMetricType.sqsOldestMessage: + return { + text: ToolbarTranslations.sqsOldestMessage, + value: InfraSnapshotMetricType.sqsOldestMessage, + }; } }; diff --git a/x-pack/legacy/plugins/infra/public/components/nodes_overview/index.tsx b/x-pack/legacy/plugins/infra/public/components/nodes_overview/index.tsx index acddbee8db267..edf1b228b278a 100644 --- a/x-pack/legacy/plugins/infra/public/components/nodes_overview/index.tsx +++ b/x-pack/legacy/plugins/infra/public/components/nodes_overview/index.tsx @@ -11,12 +11,7 @@ import { get, max, min } from 'lodash'; import React from 'react'; import euiStyled from '../../../../../common/eui_styled_components'; -import { - InfraSnapshotMetricType, - InfraSnapshotNode, - InfraNodeType, - InfraTimerangeInput, -} from '../../graphql/types'; +import { InfraSnapshotMetricType, InfraSnapshotNode, InfraNodeType } from '../../graphql/types'; import { InfraFormatterType, InfraWaffleMapBounds, InfraWaffleMapOptions } from '../../lib/lib'; import { KueryFilterQuery } from '../../store/local/waffle_filter'; import { createFormatter } from '../../utils/formatters'; @@ -34,7 +29,7 @@ interface Props { loading: boolean; reload: () => void; onDrilldown: (filter: KueryFilterQuery) => void; - timeRange: InfraTimerangeInput; + currentTime: number; onViewChange: (view: string) => void; view: string; boundsOverride: InfraWaffleMapBounds; @@ -67,6 +62,38 @@ const METRIC_FORMATTERS: MetricFormatters = { formatter: InfraFormatterType.abbreviatedNumber, template: '{{value}}/s', }, + [InfraSnapshotMetricType.diskIOReadBytes]: { + formatter: InfraFormatterType.bytes, + template: '{{value}}/s', + }, + [InfraSnapshotMetricType.diskIOWriteBytes]: { + formatter: InfraFormatterType.bytes, + template: '{{value}}/s', + }, + [InfraSnapshotMetricType.s3BucketSize]: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + [InfraSnapshotMetricType.s3TotalRequests]: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}', + }, + [InfraSnapshotMetricType.s3NumberOfObjects]: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}', + }, + [InfraSnapshotMetricType.s3UploadBytes]: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + [InfraSnapshotMetricType.s3DownloadBytes]: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + [InfraSnapshotMetricType.sqsOldestMessage]: { + formatter: InfraFormatterType.number, + template: '{{value}} seconds', + }, }; const calculateBoundsFromNodes = (nodes: InfraSnapshotNode[]): InfraWaffleMapBounds => { @@ -92,8 +119,8 @@ export const NodesOverview = class extends React.Component { nodeType, reload, view, + currentTime, options, - timeRange, } = this.props; if (loading) { return ( @@ -152,7 +179,7 @@ export const NodesOverview = class extends React.Component { nodes={nodes} options={options} formatter={this.formatter} - timeRange={timeRange} + currentTime={currentTime} onFilter={this.handleDrilldown} /> @@ -163,7 +190,7 @@ export const NodesOverview = class extends React.Component { nodes={nodes} options={options} formatter={this.formatter} - timeRange={timeRange} + currentTime={currentTime} onFilter={this.handleDrilldown} bounds={bounds} dataBounds={dataBounds} diff --git a/x-pack/legacy/plugins/infra/public/components/nodes_overview/table.tsx b/x-pack/legacy/plugins/infra/public/components/nodes_overview/table.tsx index 5b201d2be4333..b4abf962bd892 100644 --- a/x-pack/legacy/plugins/infra/public/components/nodes_overview/table.tsx +++ b/x-pack/legacy/plugins/infra/public/components/nodes_overview/table.tsx @@ -10,12 +10,7 @@ import { i18n } from '@kbn/i18n'; import { last } from 'lodash'; import React from 'react'; import { createWaffleMapNode } from '../../containers/waffle/nodes_to_wafflemap'; -import { - InfraSnapshotNode, - InfraSnapshotNodePath, - InfraTimerangeInput, - InfraNodeType, -} from '../../graphql/types'; +import { InfraSnapshotNode, InfraSnapshotNodePath, InfraNodeType } from '../../graphql/types'; import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib'; import { fieldToName } from '../waffle/lib/field_to_display_name'; import { NodeContextMenu } from '../waffle/node_context_menu'; @@ -25,7 +20,7 @@ interface Props { nodeType: InfraNodeType; options: InfraWaffleMapOptions; formatter: (subject: string | number) => string; - timeRange: InfraTimerangeInput; + currentTime: number; onFilter: (filter: string) => void; } @@ -49,7 +44,7 @@ const getGroupPaths = (path: InfraSnapshotNodePath[]) => { export const TableView = class extends React.PureComponent { public readonly state: State = initialState; public render() { - const { nodes, options, formatter, timeRange, nodeType } = this.props; + const { nodes, options, formatter, currentTime, nodeType } = this.props; const columns = [ { field: 'name', @@ -68,7 +63,7 @@ export const TableView = class extends React.PureComponent { node={item.node} nodeType={nodeType} closePopover={this.closePopoverFor(uniqueID)} - timeRange={timeRange} + currentTime={currentTime} isPopoverOpen={this.state.isPopoverOpen.includes(uniqueID)} options={options} popoverPosition="rightCenter" diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/group_of_groups.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/group_of_groups.tsx index 3f456c3c8d406..7a229fbbe02ec 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/group_of_groups.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/group_of_groups.tsx @@ -7,7 +7,7 @@ import React from 'react'; import euiStyled from '../../../../../common/eui_styled_components'; -import { InfraNodeType, InfraTimerangeInput } from '../../graphql/types'; +import { InfraNodeType } from '../../graphql/types'; import { InfraWaffleMapBounds, InfraWaffleMapGroupOfGroups, @@ -23,7 +23,7 @@ interface Props { formatter: (val: number) => string; bounds: InfraWaffleMapBounds; nodeType: InfraNodeType; - timeRange: InfraTimerangeInput; + currentTime: number; } export const GroupOfGroups: React.FC = props => { @@ -41,7 +41,7 @@ export const GroupOfGroups: React.FC = props => { formatter={props.formatter} bounds={props.bounds} nodeType={props.nodeType} - timeRange={props.timeRange} + currentTime={props.currentTime} /> ))} diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/group_of_nodes.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/group_of_nodes.tsx index bc7d31a301496..c40c68cbdbf28 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/group_of_nodes.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/group_of_nodes.tsx @@ -7,7 +7,7 @@ import React from 'react'; import euiStyled from '../../../../../common/eui_styled_components'; -import { InfraNodeType, InfraTimerangeInput } from '../../graphql/types'; +import { InfraNodeType } from '../../graphql/types'; import { InfraWaffleMapBounds, InfraWaffleMapGroupOfNodes, @@ -24,7 +24,7 @@ interface Props { isChild: boolean; bounds: InfraWaffleMapBounds; nodeType: InfraNodeType; - timeRange: InfraTimerangeInput; + currentTime: number; } export const GroupOfNodes: React.FC = ({ @@ -35,7 +35,7 @@ export const GroupOfNodes: React.FC = ({ isChild = false, bounds, nodeType, - timeRange, + currentTime, }) => { const width = group.width > 200 ? group.width : 200; return ( @@ -51,7 +51,7 @@ export const GroupOfNodes: React.FC = ({ formatter={formatter} bounds={bounds} nodeType={nodeType} - timeRange={timeRange} + currentTime={currentTime} /> ))} diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/lib/field_to_display_name.ts b/x-pack/legacy/plugins/infra/public/components/waffle/lib/field_to_display_name.ts index b34b2801a50f1..7160c8eaa8dde 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/lib/field_to_display_name.ts +++ b/x-pack/legacy/plugins/infra/public/components/waffle/lib/field_to_display_name.ts @@ -10,6 +10,14 @@ interface Lookup { [id: string]: string; } +const availabilityZoneName = i18n.translate('xpack.infra.groupByDisplayNames.availabilityZone', { + defaultMessage: 'Availability zone', +}); + +const machineTypeName = i18n.translate('xpack.infra.groupByDisplayNames.machineType', { + defaultMessage: 'Machine type', +}); + export const fieldToName = (field: string) => { const LOOKUP: Lookup = { 'kubernetes.namespace': i18n.translate('xpack.infra.groupByDisplayNames.kubernetesNamespace', { @@ -21,12 +29,8 @@ export const fieldToName = (field: string) => { 'host.name': i18n.translate('xpack.infra.groupByDisplayNames.hostName', { defaultMessage: 'Host', }), - 'cloud.availability_zone': i18n.translate('xpack.infra.groupByDisplayNames.availabilityZone', { - defaultMessage: 'Availability zone', - }), - 'cloud.machine.type': i18n.translate('xpack.infra.groupByDisplayNames.machineType', { - defaultMessage: 'Machine type', - }), + 'cloud.availability_zone': availabilityZoneName, + 'cloud.machine.type': machineTypeName, 'cloud.project.id': i18n.translate('xpack.infra.groupByDisplayNames.projectID', { defaultMessage: 'Project ID', }), @@ -36,6 +40,32 @@ export const fieldToName = (field: string) => { 'service.type': i18n.translate('xpack.infra.groupByDisplayNames.serviceType', { defaultMessage: 'Service type', }), + 'aws.cloud.availability_zone': availabilityZoneName, + 'aws.cloud.machine.type': machineTypeName, + 'aws.tags': i18n.translate('xpack.infra.groupByDisplayNames.tags', { + defaultMessage: 'Tags', + }), + 'aws.ec2.instance.image.id': i18n.translate('xpack.infra.groupByDisplayNames.image', { + defaultMessage: 'Image', + }), + 'aws.ec2.instance.state.name': i18n.translate('xpack.infra.groupByDisplayNames.state.name', { + defaultMessage: 'State', + }), + 'cloud.region': i18n.translate('xpack.infra.groupByDisplayNames.cloud.region', { + defaultMessage: 'Region', + }), + 'aws.rds.db_instance.class': i18n.translate( + 'xpack.infra.groupByDisplayNames.rds.db_instance.class', + { + defaultMessage: 'Instance Class', + } + ), + 'aws.rds.db_instance.status': i18n.translate( + 'xpack.infra.groupByDisplayNames.rds.db_instance.status', + { + defaultMessage: 'Status', + } + ), }; return LOOKUP[field] || field; }; diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/map.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/map.tsx index ed7db4fe3dfe1..6c0209a60f1cd 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/map.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/map.tsx @@ -11,7 +11,7 @@ import { isWaffleMapGroupWithGroups, isWaffleMapGroupWithNodes, } from '../../containers/waffle/type_guards'; -import { InfraSnapshotNode, InfraNodeType, InfraTimerangeInput } from '../../graphql/types'; +import { InfraSnapshotNode, InfraNodeType } from '../../graphql/types'; import { InfraWaffleMapBounds, InfraWaffleMapOptions } from '../../lib/lib'; import { AutoSizer } from '../auto_sizer'; import { GroupOfGroups } from './group_of_groups'; @@ -24,7 +24,7 @@ interface Props { nodeType: InfraNodeType; options: InfraWaffleMapOptions; formatter: (subject: string | number) => string; - timeRange: InfraTimerangeInput; + currentTime: number; onFilter: (filter: string) => void; bounds: InfraWaffleMapBounds; dataBounds: InfraWaffleMapBounds; @@ -33,7 +33,7 @@ interface Props { export const Map: React.FC = ({ nodes, options, - timeRange, + currentTime, onFilter, formatter, bounds, @@ -59,7 +59,7 @@ export const Map: React.FC = ({ formatter={formatter} bounds={bounds} nodeType={nodeType} - timeRange={timeRange} + currentTime={currentTime} /> ); } @@ -74,7 +74,7 @@ export const Map: React.FC = ({ isChild={false} bounds={bounds} nodeType={nodeType} - timeRange={timeRange} + currentTime={currentTime} /> ); } diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/node.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/node.tsx index 8f09a3fdca9cf..f0770064c3cf9 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/node.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/node.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import moment from 'moment'; import { darken, readableColor } from 'polished'; import React from 'react'; @@ -12,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { ConditionalToolTip } from './conditional_tooltip'; import euiStyled from '../../../../../common/eui_styled_components'; -import { InfraTimerangeInput, InfraNodeType } from '../../graphql/types'; +import { InfraNodeType } from '../../graphql/types'; import { InfraWaffleMapBounds, InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib'; import { colorFromValue } from './lib/color_from_value'; import { NodeContextMenu } from './node_context_menu'; @@ -30,13 +29,13 @@ interface Props { formatter: (val: number) => string; bounds: InfraWaffleMapBounds; nodeType: InfraNodeType; - timeRange: InfraTimerangeInput; + currentTime: number; } export const Node = class extends React.PureComponent { public readonly state: State = initialState; public render() { - const { nodeType, node, options, squareSize, bounds, formatter, timeRange } = this.props; + const { nodeType, node, options, squareSize, bounds, formatter, currentTime } = this.props; const { isPopoverOpen } = this.state; const { metric } = node; const valueMode = squareSize > 70; @@ -44,12 +43,6 @@ export const Node = class extends React.PureComponent { const rawValue = (metric && metric.value) || 0; const color = colorFromValue(options.legend, rawValue, bounds); const value = formatter(rawValue); - const newTimerange = { - ...timeRange, - from: moment(timeRange.to) - .subtract(1, 'hour') - .valueOf(), - }; const nodeAriaLabel = i18n.translate('xpack.infra.node.ariaLabel', { defaultMessage: '{nodeName}, click to open menu', values: { nodeName: node.name }, @@ -61,7 +54,7 @@ export const Node = class extends React.PureComponent { isPopoverOpen={isPopoverOpen} closePopover={this.closePopover} options={options} - timeRange={newTimerange} + currentTime={currentTime} popoverPosition="downCenter" > { + const inventoryModel = findInventoryModel(nodeType); // Due to the changing nature of the fields between APM and this UI, // We need to have some exceptions until 7.0 & ECS is finalized. Reference // #26620 for the details for these fields. // TODO: This is tech debt, remove it after 7.0 & ECS migration. - const APM_FIELDS = { - [InfraNodeType.host]: 'host.hostname', - [InfraNodeType.container]: 'container.id', - [InfraNodeType.pod]: 'kubernetes.pod.uid', - }; + const apmField = nodeType === InfraNodeType.host ? 'host.hostname' : inventoryModel.fields.id; const nodeLogsMenuItem = { name: i18n.translate('xpack.infra.nodeContextMenu.viewLogsName', { @@ -62,11 +59,12 @@ export const NodeContextMenu = injectUICapabilities( href: getNodeLogsUrl({ nodeType, nodeId: node.id, - time: timeRange.to, + time: currentTime, }), 'data-test-subj': 'viewLogsContextMenuItem', }; + const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000; const nodeDetailMenuItem = { name: i18n.translate('xpack.infra.nodeContextMenu.viewMetricsName', { defaultMessage: 'View metrics', @@ -74,45 +72,47 @@ export const NodeContextMenu = injectUICapabilities( href: getNodeDetailUrl({ nodeType, nodeId: node.id, - from: timeRange.from, - to: timeRange.to, + from: nodeDetailFrom, + to: currentTime, }), }; const apmTracesMenuItem = { name: i18n.translate('xpack.infra.nodeContextMenu.viewAPMTraces', { - defaultMessage: 'View {nodeType} APM traces', - values: { nodeType }, + defaultMessage: 'View APM traces', }), - href: `../app/apm#/traces?_g=()&kuery=${APM_FIELDS[nodeType]}:"${node.id}"`, + href: `../app/apm#/traces?_g=()&kuery=${apmField}:"${node.id}"`, 'data-test-subj': 'viewApmTracesContextMenuItem', }; const uptimeMenuItem = { name: i18n.translate('xpack.infra.nodeContextMenu.viewUptimeLink', { - defaultMessage: 'View {nodeType} in Uptime', - values: { nodeType }, + defaultMessage: 'View in Uptime', }), href: createUptimeLink(options, nodeType, node), }; - const showLogsLink = node.id && uiCapabilities.logs.show; - const showAPMTraceLink = uiCapabilities.apm && uiCapabilities.apm.show; + const showDetail = inventoryModel.crosslinkSupport.details; + const showLogsLink = + inventoryModel.crosslinkSupport.logs && node.id && uiCapabilities.logs.show; + const showAPMTraceLink = + inventoryModel.crosslinkSupport.apm && uiCapabilities.apm && uiCapabilities.apm.show; const showUptimeLink = - [InfraNodeType.pod, InfraNodeType.container].includes(nodeType) || node.ip; + inventoryModel.crosslinkSupport.uptime && + ([InfraNodeType.pod, InfraNodeType.container].includes(nodeType) || node.ip); - const panels: EuiContextMenuPanelDescriptor[] = [ - { - id: 0, - title: '', - items: [ - ...(showLogsLink ? [nodeLogsMenuItem] : []), - nodeDetailMenuItem, - ...(showAPMTraceLink ? [apmTracesMenuItem] : []), - ...(showUptimeLink ? [uptimeMenuItem] : []), - ], - }, + const items = [ + ...(showLogsLink ? [nodeLogsMenuItem] : []), + ...(showDetail ? [nodeDetailMenuItem] : []), + ...(showAPMTraceLink ? [apmTracesMenuItem] : []), + ...(showUptimeLink ? [uptimeMenuItem] : []), ]; + const panels: EuiContextMenuPanelDescriptor[] = [{ id: 0, title: '', items }]; + + // If there is nothing to show then we need to return the child as is + if (items.length === 0) { + return <>{children}; + } return ( void; } +const getDisplayNameForType = (type: InventoryItemType) => { + const inventoryModel = findInventoryModel(type); + return inventoryModel.displayName; +}; + export const WaffleInventorySwitcher: React.FC = ({ changeNodeType, changeGroupBy, @@ -48,44 +59,62 @@ export const WaffleInventorySwitcher: React.FC = ( const goToHost = useCallback(() => goToNodeType('host' as InfraNodeType), [goToNodeType]); const goToK8 = useCallback(() => goToNodeType('pod' as InfraNodeType), [goToNodeType]); const goToDocker = useCallback(() => goToNodeType('container' as InfraNodeType), [goToNodeType]); + const goToAwsEC2 = useCallback(() => goToNodeType('awsEC2' as InfraNodeType), [goToNodeType]); + const goToAwsS3 = useCallback(() => goToNodeType('awsS3' as InfraNodeType), [goToNodeType]); + const goToAwsRDS = useCallback(() => goToNodeType('awsRDS' as InfraNodeType), [goToNodeType]); + const goToAwsSQS = useCallback(() => goToNodeType('awsSQS' as InfraNodeType), [goToNodeType]); const panels = useMemo( - () => [ - { - id: 0, - items: [ - { - name: i18n.translate('xpack.infra.waffle.nodeTypeSwitcher.hostsLabel', { - defaultMessage: 'Hosts', - }), - icon: 'host', - onClick: goToHost, - }, - { - name: 'Kubernetes', - icon: 'kubernetes', - onClick: goToK8, - }, - { - name: 'Docker', - icon: 'docker', - onClick: goToDocker, - }, - ], - }, - ], - [goToDocker, goToHost, goToK8] + () => + [ + { + id: 'firstPanel', + items: [ + { + name: getDisplayNameForType('host'), + onClick: goToHost, + }, + { + name: getDisplayNameForType('pod'), + onClick: goToK8, + }, + { + name: getDisplayNameForType('container'), + onClick: goToDocker, + }, + { + name: 'AWS', + panel: 'awsPanel', + }, + ], + }, + { + id: 'awsPanel', + title: 'AWS', + items: [ + { + name: getDisplayNameForType('awsEC2'), + onClick: goToAwsEC2, + }, + { + name: getDisplayNameForType('awsS3'), + onClick: goToAwsS3, + }, + { + name: getDisplayNameForType('awsRDS'), + onClick: goToAwsRDS, + }, + { + name: getDisplayNameForType('awsSQS'), + onClick: goToAwsSQS, + }, + ], + }, + ] as EuiContextMenuPanelDescriptor[], + [goToAwsEC2, goToAwsRDS, goToAwsS3, goToAwsSQS, goToDocker, goToHost, goToK8] ); + const selectedText = useMemo(() => { - switch (nodeType) { - case InfraNodeType.host: - return i18n.translate('xpack.infra.waffle.nodeTypeSwitcher.hostsLabel', { - defaultMessage: 'Hosts', - }); - case InfraNodeType.pod: - return 'Kubernetes'; - case InfraNodeType.container: - return 'Docker'; - } + return getDisplayNameForType(nodeType); }, [nodeType]); return ( @@ -107,7 +136,7 @@ export const WaffleInventorySwitcher: React.FC = ( withTitle anchorPosition="downLeft" > - + ); diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/waffle_metric_controls.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/waffle_metric_controls.tsx index b0ea6f13f2bb6..d5ae6fcf7f7a2 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/waffle_metric_controls.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/waffle_metric_controls.tsx @@ -44,7 +44,7 @@ export const WaffleMetricControls = class extends React.PureComponent o.value === metric.type); if (!currentLabel) { - return 'null'; + return null; } const panels: EuiContextMenuPanelDescriptor[] = [ { diff --git a/x-pack/legacy/plugins/infra/public/containers/waffle/use_snaphot.ts b/x-pack/legacy/plugins/infra/public/containers/waffle/use_snaphot.ts index 4c8d41afd36b5..63b91bd97776a 100644 --- a/x-pack/legacy/plugins/infra/public/containers/waffle/use_snaphot.ts +++ b/x-pack/legacy/plugins/infra/public/containers/waffle/use_snaphot.ts @@ -12,7 +12,6 @@ import { InfraNodeType, InfraSnapshotMetricInput, InfraSnapshotGroupbyInput, - InfraTimerangeInput, } from '../../graphql/types'; import { throwErrors, createPlainError } from '../../../common/runtime_types'; import { useHTTPRequest } from '../../hooks/use_http_request'; @@ -27,7 +26,7 @@ export function useSnapshot( groupBy: InfraSnapshotGroupbyInput[], nodeType: InfraNodeType, sourceId: string, - timerange: InfraTimerangeInput + currentTime: number ) { const decodeResponse = (response: any) => { return pipe( @@ -36,6 +35,12 @@ export function useSnapshot( ); }; + const timerange = { + interval: '1m', + to: currentTime, + from: currentTime - 360 * 1000, + }; + const { error, loading, response, makeRequest } = useHTTPRequest( '/api/metrics/snapshot', 'POST', diff --git a/x-pack/legacy/plugins/infra/public/graphql/introspection.json b/x-pack/legacy/plugins/infra/public/graphql/introspection.json index 64792d606f446..ae8d3dcdd5bec 100644 --- a/x-pack/legacy/plugins/infra/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/infra/public/graphql/introspection.json @@ -2129,7 +2129,8 @@ "isDeprecated": false, "deprecationReason": null }, - { "name": "host", "description": "", "isDeprecated": false, "deprecationReason": null } + { "name": "host", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "awsEC2", "description": "", "isDeprecated": false, "deprecationReason": null } ], "possibleTypes": null }, @@ -2191,7 +2192,9 @@ { "name": "memory", "description": "", "isDeprecated": false, "deprecationReason": null }, { "name": "tx", "description": "", "isDeprecated": false, "deprecationReason": null }, { "name": "rx", "description": "", "isDeprecated": false, "deprecationReason": null }, - { "name": "logRate", "description": "", "isDeprecated": false, "deprecationReason": null } + { "name": "logRate", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "diskIOReadBytes", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "diskIOWriteBytes", "description": "", "isDeprecated": false, "deprecationReason": null } ], "possibleTypes": null }, @@ -2585,6 +2588,24 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "awsEC2CpuUtilization", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "awsEC2NetworkTraffic", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "awsEC2DiskIOBytes", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, { "name": "custom", "description": "", "isDeprecated": false, "deprecationReason": null } ], "possibleTypes": null diff --git a/x-pack/legacy/plugins/infra/public/graphql/types.ts b/x-pack/legacy/plugins/infra/public/graphql/types.ts index 8d0e75523a8ed..3715f02bb252e 100644 --- a/x-pack/legacy/plugins/infra/public/graphql/types.ts +++ b/x-pack/legacy/plugins/infra/public/graphql/types.ts @@ -554,6 +554,10 @@ export enum InfraNodeType { pod = 'pod', container = 'container', host = 'host', + awsEC2 = 'awsEC2', + awsS3 = 'awsS3', + awsRDS = 'awsRDS', + awsSQS = 'awsSQS', } export enum InfraSnapshotMetricType { @@ -564,6 +568,22 @@ export enum InfraSnapshotMetricType { tx = 'tx', rx = 'rx', logRate = 'logRate', + diskIOReadBytes = 'diskIOReadBytes', + diskIOWriteBytes = 'diskIOWriteBytes', + s3TotalRequests = 's3TotalRequests', + s3NumberOfObjects = 's3NumberOfObjects', + s3BucketSize = 's3BucketSize', + s3DownloadBytes = 's3DownloadBytes', + s3UploadBytes = 's3UploadBytes', + rdsConnections = 'rdsConnections', + rdsQueriesExecuted = 'rdsQueriesExecuted', + rdsActiveTransactions = 'rdsActiveTransactions', + rdsLatency = 'rdsLatency', + sqsMessagesVisible = 'sqsMessagesVisible', + sqsMessagesDelayed = 'sqsMessagesDelayed', + sqsMessagesSent = 'sqsMessagesSent', + sqsMessagesEmpty = 'sqsMessagesEmpty', + sqsOldestMessage = 'sqsOldestMessage', } export enum InfraMetric { @@ -604,6 +624,24 @@ export enum InfraMetric { awsNetworkPackets = 'awsNetworkPackets', awsDiskioBytes = 'awsDiskioBytes', awsDiskioOps = 'awsDiskioOps', + awsEC2CpuUtilization = 'awsEC2CpuUtilization', + awsEC2DiskIOBytes = 'awsEC2DiskIOBytes', + awsEC2NetworkTraffic = 'awsEC2NetworkTraffic', + awsS3TotalRequests = 'awsS3TotalRequests', + awsS3NumberOfObjects = 'awsS3NumberOfObjects', + awsS3BucketSize = 'awsS3BucketSize', + awsS3DownloadBytes = 'awsS3DownloadBytes', + awsS3UploadBytes = 'awsS3UploadBytes', + awsRDSCpuTotal = 'awsRDSCpuTotal', + awsRDSConnections = 'awsRDSConnections', + awsRDSQueriesExecuted = 'awsRDSQueriesExecuted', + awsRDSActiveTransactions = 'awsRDSActiveTransactions', + awsRDSLatency = 'awsRDSLatency', + awsSQSMessagesVisible = 'awsSQSMessagesVisible', + awsSQSMessagesDelayed = 'awsSQSMessagesDelayed', + awsSQSMessagesSent = 'awsSQSMessagesSent', + awsSQSMessagesEmpty = 'awsSQSMessagesEmpty', + awsSQSOldestMessage = 'awsSQSOldestMessage', custom = 'custom', } diff --git a/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/page_content.tsx b/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/page_content.tsx index 04aa0a9188a57..85b551a448d0e 100644 --- a/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/page_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/page_content.tsx @@ -21,7 +21,7 @@ export const SnapshotPageContent: React.FC = () => ( {({ filterQueryAsJson, applyFilterQuery }) => ( - {({ currentTimeRange, isAutoReloading }) => ( + {({ currentTime }) => ( {({ metric, @@ -33,12 +33,12 @@ export const SnapshotPageContent: React.FC = () => ( boundsOverride, }) => ( ; } +const ITEM_TYPES = inventoryModels.map(m => m.id).join('|'); + export const LinkToPage: React.FC = props => ( ; +const getFieldByNodeType = (nodeType: InfraNodeType, fields: SourceConfigurationFields.Fields) => { + const inventoryFields = findInventoryFields(nodeType, fields); + return inventoryFields.id; +}; + export const RedirectToNodeLogs = ({ match: { params: { nodeId, nodeType, sourceId = 'default' }, @@ -50,7 +56,7 @@ export const RedirectToNodeLogs = ({ return null; } - const nodeFilter = `${configuration.fields[nodeType]}: ${nodeId}`; + const nodeFilter = `${getFieldByNodeType(nodeType, configuration.fields)}: ${nodeId}`; const userFilter = getFilterFromLocation(location); const filter = userFilter ? `(${nodeFilter}) and (${userFilter})` : nodeFilter; diff --git a/x-pack/legacy/plugins/infra/public/utils/formatters/high_precision.ts b/x-pack/legacy/plugins/infra/public/utils/formatters/high_precision.ts new file mode 100644 index 0000000000000..391b19d2af91b --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/utils/formatters/high_precision.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const formatHighPercision = (val: number) => { + return Number(val).toLocaleString('en', { + maximumFractionDigits: 5, + }); +}; diff --git a/x-pack/legacy/plugins/infra/public/utils/formatters/index.ts b/x-pack/legacy/plugins/infra/public/utils/formatters/index.ts index efb20e71a9ce4..3c60dba747825 100644 --- a/x-pack/legacy/plugins/infra/public/utils/formatters/index.ts +++ b/x-pack/legacy/plugins/infra/public/utils/formatters/index.ts @@ -10,6 +10,7 @@ import { createBytesFormatter } from './bytes'; import { formatNumber } from './number'; import { formatPercent } from './percent'; import { InventoryFormatterType } from '../../../common/inventory_models/types'; +import { formatHighPercision } from './high_precision'; export const FORMATTERS = { number: formatNumber, @@ -21,6 +22,7 @@ export const FORMATTERS = { // bytes in bits formatted string out bits: createBytesFormatter(InfraWaffleMapDataFormat.bitsDecimal), percent: formatPercent, + highPercision: formatHighPercision, }; export const createFormatter = (format: InventoryFormatterType, template: string = '{{value}}') => ( diff --git a/x-pack/legacy/plugins/infra/server/graphql/types.ts b/x-pack/legacy/plugins/infra/server/graphql/types.ts index 8f87979dbde2e..88ad5b2f58f23 100644 --- a/x-pack/legacy/plugins/infra/server/graphql/types.ts +++ b/x-pack/legacy/plugins/infra/server/graphql/types.ts @@ -580,6 +580,10 @@ export enum InfraNodeType { pod = 'pod', container = 'container', host = 'host', + awsEC2 = 'awsEC2', + awsS3 = 'awsS3', + awsRDS = 'awsRDS', + awsSQS = 'awsSQS' } export enum InfraSnapshotMetricType { @@ -590,6 +594,22 @@ export enum InfraSnapshotMetricType { tx = 'tx', rx = 'rx', logRate = 'logRate', + diskIOReadBytes = 'diskIOReadBytes', + diskIOWriteBytes = 'diskIOWriteBytes', + s3TotalRequests = 's3TotalRequests', + s3NumberOfObjects = 's3NumberOfObjects', + s3BucketSize = 's3BucketSize', + s3DownloadBytes = 's3DownloadBytes', + s3UploadBytes = 's3UploadBytes', + rdsConnections = 'rdsConnections', + rdsQueriesExecuted = 'rdsQueriesExecuted', + rdsActiveTransactions = 'rdsActiveTransactions', + rdsLatency = 'rdsLatency', + sqsMessagesVisible = 'sqsOldestMessage', + sqsMessagesDelayed = 'sqsMessagesDelayed', + sqsMessagesSent = 'sqsMessagesSent', + sqsMessagesEmpty = 'sqsMessagesEmpty', + sqsOldestMessage = 'sqsOldestMessage', } export enum InfraMetric { @@ -630,6 +650,24 @@ export enum InfraMetric { awsNetworkPackets = 'awsNetworkPackets', awsDiskioBytes = 'awsDiskioBytes', awsDiskioOps = 'awsDiskioOps', + awsEC2CpuUtilization = 'awsEC2CpuUtilization', + awsEC2DiskIOBytes = 'awsEC2DiskIOBytes', + awsEC2NetworkTraffic = 'awsEC2NetworkTraffic', + awsS3TotalRequests = 'awsS3TotalRequests', + awsS3NumberOfObjects = 'awsS3NumberOfObjects', + awsS3BucketSize = 'awsS3BucketSize', + awsS3DownloadBytes = 'awsS3DownloadBytes', + awsS3UploadBytes = 'awsS3UploadBytes', + awsRDSCpuTotal = 'awsRDSCpuTotal', + awsRDSConnections = 'awsRDSConnections', + awsRDSQueriesExecuted = 'awsRDSQueriesExecuted', + awsRDSActiveTransactions = 'awsRDSActiveTransactions', + awsRDSLatency = 'awsRDSLatency', + awsSQSMessagesVisible = 'awsSQSMessagesVisible', + awsSQSMessagesDelayed = 'awsSQSMessagesDelayed', + awsSQSMessagesSent = 'awsSQSMessagesSent', + awsSQSMessagesEmpty = 'awsSQSMessagesEmpty', + awsSQSOldestMessage = 'awsSQSOldestMessage', custom = 'custom', } diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts index db3c516841cd4..c4146f5758d80 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts @@ -7,11 +7,11 @@ import { i18n } from '@kbn/i18n'; import { flatten, get } from 'lodash'; import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; -import { InfraMetric, InfraMetricData, InfraNodeType } from '../../../graphql/types'; +import { InfraMetric, InfraMetricData } from '../../../graphql/types'; import { KibanaFramework } from '../framework/kibana_framework_adapter'; import { InfraMetricsAdapter, InfraMetricsRequestOptions } from './adapter_types'; import { checkValidNode } from './lib/check_valid_node'; -import { metrics } from '../../../../common/inventory_models'; +import { metrics, findInventoryFields } from '../../../../common/inventory_models'; import { TSVBMetricModelCreator } from '../../../../common/inventory_models/types'; import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; @@ -27,13 +27,10 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { options: InfraMetricsRequestOptions, rawRequest: KibanaRequest // NP_TODO: Temporarily needed until metrics getVisData no longer needs full request ): Promise { - const fields = { - [InfraNodeType.host]: options.sourceConfiguration.fields.host, - [InfraNodeType.container]: options.sourceConfiguration.fields.container, - [InfraNodeType.pod]: options.sourceConfiguration.fields.pod, - }; const indexPattern = `${options.sourceConfiguration.metricAlias},${options.sourceConfiguration.logAlias}`; - const nodeField = fields[options.nodeType]; + const fields = findInventoryFields(options.nodeType, options.sourceConfiguration.fields); + const nodeField = fields.id; + const search = (searchOptions: object) => this.framework.callWithRequest<{}, Aggregation>(requestContext, 'search', searchOptions); diff --git a/x-pack/legacy/plugins/infra/server/lib/constants.ts b/x-pack/legacy/plugins/infra/server/lib/constants.ts index 4f2fa561da0c5..0765256c4160c 100644 --- a/x-pack/legacy/plugins/infra/server/lib/constants.ts +++ b/x-pack/legacy/plugins/infra/server/lib/constants.ts @@ -4,21 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { InfraNodeType } from '../graphql/types'; - -// Used for metadata and snapshots resolvers to find the field that contains -// a displayable name of a node. -// Intentionally not the same as xpack.infra.sources.default.fields.{host,container,pod}. -// TODO: consider moving this to source configuration too. -export const NAME_FIELDS = { - [InfraNodeType.host]: 'host.name', - [InfraNodeType.pod]: 'kubernetes.pod.name', - [InfraNodeType.container]: 'container.name', -}; -export const IP_FIELDS = { - [InfraNodeType.host]: 'host.ip', - [InfraNodeType.pod]: 'kubernetes.pod.ip', - [InfraNodeType.container]: 'container.ip_address', -}; - export const CLOUD_METRICS_MODULES = ['aws']; diff --git a/x-pack/legacy/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts b/x-pack/legacy/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts new file mode 100644 index 0000000000000..6c27e54a78bee --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { uniq } from 'lodash'; +import { RequestHandlerContext } from 'kibana/server'; +import { InfraSnapshotRequestOptions } from './types'; +import { InfraTimerangeInput } from '../../../public/graphql/types'; +import { getMetricsAggregations } from './query_helpers'; +import { calculateMetricInterval } from '../../utils/calculate_metric_interval'; +import { SnapshotModel, SnapshotModelMetricAggRT } from '../../../common/inventory_models/types'; +import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; + +export const createTimeRangeWithInterval = async ( + framework: KibanaFramework, + requestContext: RequestHandlerContext, + options: InfraSnapshotRequestOptions +): Promise => { + const aggregations = getMetricsAggregations(options); + const modules = aggregationsToModules(aggregations); + const interval = + (await calculateMetricInterval( + framework, + requestContext, + { + indexPattern: options.sourceConfiguration.metricAlias, + timestampField: options.sourceConfiguration.fields.timestamp, + timerange: { from: options.timerange.from, to: options.timerange.to }, + }, + modules, + options.nodeType + )) || 60000; + return { + interval: `${interval}s`, + from: options.timerange.to - interval * 5000, // We need at least 5 buckets worth of data + to: options.timerange.to, + }; +}; + +const aggregationsToModules = (aggregations: SnapshotModel): string[] => { + return uniq( + Object.values(aggregations) + .reduce((modules, agg) => { + if (SnapshotModelMetricAggRT.is(agg)) { + return modules.concat(Object.values(agg).map(a => a?.field)); + } + return modules; + }, [] as Array) + .filter(v => v) + .map(field => + field! + .split(/\./) + .slice(0, 2) + .join('.') + ) + ) as string[]; +}; diff --git a/x-pack/legacy/plugins/infra/server/lib/snapshot/query_helpers.ts b/x-pack/legacy/plugins/infra/server/lib/snapshot/query_helpers.ts index 6ebbe8775562c..44d32c7b915a8 100644 --- a/x-pack/legacy/plugins/infra/server/lib/snapshot/query_helpers.ts +++ b/x-pack/legacy/plugins/infra/server/lib/snapshot/query_helpers.ts @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { findInventoryModel } from '../../../common/inventory_models/index'; -import { InfraSnapshotRequestOptions } from './snapshot'; -import { NAME_FIELDS } from '../constants'; +import { findInventoryModel, findInventoryFields } from '../../../common/inventory_models/index'; +import { InfraSnapshotRequestOptions } from './types'; import { getIntervalInSeconds } from '../../utils/get_interval_in_seconds'; import { SnapshotModelRT, SnapshotModel } from '../../../common/inventory_models/types'; @@ -21,28 +19,35 @@ interface GroupBySource { }; } +export const getFieldByNodeType = (options: InfraSnapshotRequestOptions) => { + const inventoryFields = findInventoryFields(options.nodeType, options.sourceConfiguration.fields); + return inventoryFields.id; +}; + export const getGroupedNodesSources = (options: InfraSnapshotRequestOptions) => { + const fields = findInventoryFields(options.nodeType, options.sourceConfiguration.fields); const sources: GroupBySource[] = options.groupBy.map(gb => { return { [`${gb.field}`]: { terms: { field: gb.field } } }; }); sources.push({ id: { - terms: { field: options.sourceConfiguration.fields[options.nodeType] }, + terms: { field: fields.id }, }, }); sources.push({ - name: { terms: { field: NAME_FIELDS[options.nodeType], missing_bucket: true } }, + name: { terms: { field: fields.name, missing_bucket: true } }, }); return sources; }; export const getMetricsSources = (options: InfraSnapshotRequestOptions) => { - return [{ id: { terms: { field: options.sourceConfiguration.fields[options.nodeType] } } }]; + const fields = findInventoryFields(options.nodeType, options.sourceConfiguration.fields); + return [{ id: { terms: { field: fields.id } } }]; }; export const getMetricsAggregations = (options: InfraSnapshotRequestOptions): SnapshotModel => { - const model = findInventoryModel(options.nodeType); - const aggregation = get(model, ['metrics', 'snapshot', options.metric.type]); + const inventoryModel = findInventoryModel(options.nodeType); + const aggregation = inventoryModel.metrics.snapshot?.[options.metric.type]; if (!SnapshotModelRT.is(aggregation)) { throw new Error( i18n.translate('xpack.infra.snapshot.missingSnapshotMetricError', { diff --git a/x-pack/legacy/plugins/infra/server/lib/snapshot/response_helpers.ts b/x-pack/legacy/plugins/infra/server/lib/snapshot/response_helpers.ts index 6b18d9489c100..d22f41ff152f7 100644 --- a/x-pack/legacy/plugins/infra/server/lib/snapshot/response_helpers.ts +++ b/x-pack/legacy/plugins/infra/server/lib/snapshot/response_helpers.ts @@ -14,8 +14,8 @@ import { InfraNodeType, } from '../../graphql/types'; import { getIntervalInSeconds } from '../../utils/get_interval_in_seconds'; -import { InfraSnapshotRequestOptions } from './snapshot'; -import { IP_FIELDS } from '../constants'; +import { InfraSnapshotRequestOptions } from './types'; +import { findInventoryModel } from '../../../common/inventory_models'; export interface InfraSnapshotNodeMetricsBucket { key: { id: string }; @@ -73,12 +73,13 @@ export const getIPFromBucket = ( nodeType: InfraNodeType, bucket: InfraSnapshotNodeGroupByBucket ): string | null => { - const ip = get( - bucket, - `ip.hits.hits[0]._source.${IP_FIELDS[nodeType]}`, - null - ); - + const inventoryModel = findInventoryModel(nodeType); + if (!inventoryModel.fields.ip) { + return null; + } + const ip = get(bucket, `ip.hits.hits[0]._source.${inventoryModel.fields.ip}`, null) as + | string[] + | null; if (Array.isArray(ip)) { return ip.find(isIPv4) || null; } else if (typeof ip === 'string') { diff --git a/x-pack/legacy/plugins/infra/server/lib/snapshot/snapshot.ts b/x-pack/legacy/plugins/infra/server/lib/snapshot/snapshot.ts index 95769414832cc..d1db0ef07b338 100644 --- a/x-pack/legacy/plugins/infra/server/lib/snapshot/snapshot.ts +++ b/x-pack/legacy/plugins/infra/server/lib/snapshot/snapshot.ts @@ -5,14 +5,7 @@ */ import { RequestHandlerContext } from 'src/core/server'; -import { - InfraSnapshotGroupbyInput, - InfraSnapshotMetricInput, - InfraSnapshotNode, - InfraTimerangeInput, - InfraNodeType, - InfraSourceConfiguration, -} from '../../graphql/types'; +import { InfraSnapshotNode } from '../../graphql/types'; import { InfraDatabaseSearchResponse } from '../adapters/framework'; import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; import { InfraSources } from '../sources'; @@ -32,18 +25,11 @@ import { InfraSnapshotNodeGroupByBucket, InfraSnapshotNodeMetricsBucket, } from './response_helpers'; -import { IP_FIELDS } from '../constants'; import { getAllCompositeData } from '../../utils/get_all_composite_data'; import { createAfterKeyHandler } from '../../utils/create_afterkey_handler'; - -export interface InfraSnapshotRequestOptions { - nodeType: InfraNodeType; - sourceConfiguration: InfraSourceConfiguration; - timerange: InfraTimerangeInput; - groupBy: InfraSnapshotGroupbyInput[]; - metric: InfraSnapshotMetricInput; - filterQuery: JsonObject | undefined; -} +import { findInventoryModel } from '../../../common/inventory_models'; +import { InfraSnapshotRequestOptions } from './types'; +import { createTimeRangeWithInterval } from './create_timerange_with_interval'; export class InfraSnapshot { constructor(private readonly libs: { sources: InfraSources; framework: KibanaFramework }) {} @@ -56,8 +42,22 @@ export class InfraSnapshot { // in order to page through the results of their respective composite aggregations. // Both chains of requests are supposed to run in parallel, and their results be merged // when they have both been completed. - const groupedNodesPromise = requestGroupedNodes(requestContext, options, this.libs.framework); - const nodeMetricsPromise = requestNodeMetrics(requestContext, options, this.libs.framework); + const timeRangeWithIntervalApplied = await createTimeRangeWithInterval( + this.libs.framework, + requestContext, + options + ); + const optionsWithTimerange = { ...options, timerange: timeRangeWithIntervalApplied }; + const groupedNodesPromise = requestGroupedNodes( + requestContext, + optionsWithTimerange, + this.libs.framework + ); + const nodeMetricsPromise = requestNodeMetrics( + requestContext, + optionsWithTimerange, + this.libs.framework + ); const groupedNodeBuckets = await groupedNodesPromise; const nodeMetricBuckets = await nodeMetricsPromise; @@ -79,6 +79,7 @@ const requestGroupedNodes = async ( options: InfraSnapshotRequestOptions, framework: KibanaFramework ): Promise => { + const inventoryModel = findInventoryModel(options.nodeType); const query = { allowNoIndices: true, index: `${options.sourceConfiguration.logAlias},${options.sourceConfiguration.metricAlias}`, @@ -112,7 +113,7 @@ const requestGroupedNodes = async ( top_hits: { sort: [{ [options.sourceConfiguration.fields.timestamp]: { order: 'desc' } }], _source: { - includes: [IP_FIELDS[options.nodeType]], + includes: inventoryModel.fields.ip ? [inventoryModel.fields.ip] : [], }, size: 1, }, diff --git a/x-pack/legacy/plugins/infra/server/lib/snapshot/types.ts b/x-pack/legacy/plugins/infra/server/lib/snapshot/types.ts new file mode 100644 index 0000000000000..778f5045894a9 --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/lib/snapshot/types.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { JsonObject } from '../../../common/typed_json'; +import { + InfraNodeType, + InfraSourceConfiguration, + InfraTimerangeInput, + InfraSnapshotGroupbyInput, + InfraSnapshotMetricInput, +} from '../../../public/graphql/types'; + +export interface InfraSnapshotRequestOptions { + nodeType: InfraNodeType; + sourceConfiguration: InfraSourceConfiguration; + timerange: InfraTimerangeInput; + groupBy: InfraSnapshotGroupbyInput[]; + metric: InfraSnapshotMetricInput; + filterQuery: JsonObject | undefined; +} diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_id_field_name.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_id_field_name.ts deleted file mode 100644 index 5f6bdd30fa2b8..0000000000000 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_id_field_name.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { InfraSourceConfiguration } from '../../../lib/sources'; - -export const getIdFieldName = (sourceConfiguration: InfraSourceConfiguration, nodeType: string) => { - switch (nodeType) { - case 'host': - return sourceConfiguration.fields.host; - case 'container': - return sourceConfiguration.fields.container; - default: - return sourceConfiguration.fields.pod; - } -}; diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts index 3bd22062c26a0..191339565b813 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts @@ -12,8 +12,8 @@ import { } from '../../../lib/adapters/framework'; import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; -import { getIdFieldName } from './get_id_field_name'; -import { NAME_FIELDS } from '../../../lib/constants'; +import { findInventoryFields } from '../../../../common/inventory_models'; +import { InventoryItemType } from '../../../../common/inventory_models/types'; export interface InfraMetricsAdapterResponse { id: string; @@ -26,10 +26,9 @@ export const getMetricMetadata = async ( requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, nodeId: string, - nodeType: 'host' | 'pod' | 'container' + nodeType: InventoryItemType ): Promise => { - const idFieldName = getIdFieldName(sourceConfiguration, nodeType); - + const fields = findInventoryFields(nodeType, sourceConfiguration.fields); const metricQuery = { allowNoIndices: true, ignoreUnavailable: true, @@ -40,7 +39,7 @@ export const getMetricMetadata = async ( must_not: [{ match: { 'event.dataset': 'aws.ec2' } }], filter: [ { - match: { [idFieldName]: nodeId }, + match: { [fields.id]: nodeId }, }, ], }, @@ -49,7 +48,7 @@ export const getMetricMetadata = async ( aggs: { nodeName: { terms: { - field: NAME_FIELDS[nodeType], + field: fields.name, size: 1, }, }, diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts index 1567b6d1bd1ec..4ff0df30abedd 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { first } from 'lodash'; +import { first, set, startsWith } from 'lodash'; import { RequestHandlerContext } from 'src/core/server'; import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; @@ -12,14 +12,15 @@ import { InfraNodeType } from '../../../graphql/types'; import { InfraMetadataInfo } from '../../../../common/http_api/metadata_api'; import { getPodNodeName } from './get_pod_node_name'; import { CLOUD_METRICS_MODULES } from '../../../lib/constants'; -import { getIdFieldName } from './get_id_field_name'; +import { findInventoryFields } from '../../../../common/inventory_models'; +import { InventoryItemType } from '../../../../common/inventory_models/types'; export const getNodeInfo = async ( framework: KibanaFramework, requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, nodeId: string, - nodeType: 'host' | 'pod' | 'container' + nodeType: InventoryItemType ): Promise => { // If the nodeType is a Kubernetes pod then we need to get the node info // from a host record instead of a pod. This is due to the fact that any host @@ -45,6 +46,7 @@ export const getNodeInfo = async ( } return {}; } + const fields = findInventoryFields(nodeType, sourceConfiguration.fields); const params = { allowNoIndices: true, ignoreUnavailable: true, @@ -55,12 +57,18 @@ export const getNodeInfo = async ( _source: ['host.*', 'cloud.*'], query: { bool: { - must_not: CLOUD_METRICS_MODULES.map(module => ({ match: { 'event.module': module } })), - filter: [{ match: { [getIdFieldName(sourceConfiguration, nodeType)]: nodeId } }], + filter: [{ match: { [fields.id]: nodeId } }], }, }, }, }; + if (!CLOUD_METRICS_MODULES.some(m => startsWith(nodeType, m))) { + set( + params, + 'body.query.bool.must_not', + CLOUD_METRICS_MODULES.map(module => ({ match: { 'event.module': module } })) + ); + } const response = await framework.callWithRequest<{ _source: InfraMetadataInfo }, {}>( requestContext, 'search', diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts index 47ffc7f83b6bc..be6e29a794d09 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts @@ -8,7 +8,7 @@ import { first, get } from 'lodash'; import { RequestHandlerContext } from 'src/core/server'; import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; -import { getIdFieldName } from './get_id_field_name'; +import { findInventoryFields } from '../../../../common/inventory_models'; export const getPodNodeName = async ( framework: KibanaFramework, @@ -17,6 +17,7 @@ export const getPodNodeName = async ( nodeId: string, nodeType: 'host' | 'pod' | 'container' ): Promise => { + const fields = findInventoryFields(nodeType, sourceConfiguration.fields); const params = { allowNoIndices: true, ignoreUnavailable: true, @@ -28,7 +29,7 @@ export const getPodNodeName = async ( query: { bool: { filter: [ - { match: { [getIdFieldName(sourceConfiguration, nodeType)]: nodeId } }, + { match: { [fields.id]: nodeId } }, { exists: { field: `kubernetes.node.name` } }, ], }, diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/has_apm_data.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/has_apm_data.ts index ab242804173c0..9ca0819d74d46 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/has_apm_data.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/has_apm_data.ts @@ -8,24 +8,25 @@ import { RequestHandlerContext } from 'src/core/server'; import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; -import { getIdFieldName } from './get_id_field_name'; +import { findInventoryFields } from '../../../../common/inventory_models'; +import { InventoryItemType } from '../../../../common/inventory_models/types'; export const hasAPMData = async ( framework: KibanaFramework, requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, nodeId: string, - nodeType: 'host' | 'pod' | 'container' + nodeType: InventoryItemType ) => { const apmIndices = await framework.plugins.apm.getApmIndices( requestContext.core.savedObjects.client ); const apmIndex = apmIndices['apm_oss.transactionIndices'] || 'apm-*'; + const fields = findInventoryFields(nodeType, sourceConfiguration.fields); // There is a bug in APM ECS data where host.name is not set. // This will fixed with: https://github.com/elastic/apm-server/issues/2502 - const nodeFieldName = - nodeType === 'host' ? 'host.hostname' : getIdFieldName(sourceConfiguration, nodeType); + const nodeFieldName = nodeType === 'host' ? 'host.hostname' : fields.id; const params = { allowNoIndices: true, ignoreUnavailable: true, diff --git a/x-pack/legacy/plugins/infra/server/routes/snapshot/index.ts b/x-pack/legacy/plugins/infra/server/routes/snapshot/index.ts index 013a261d24831..ae707bae79b9e 100644 --- a/x-pack/legacy/plugins/infra/server/routes/snapshot/index.ts +++ b/x-pack/legacy/plugins/infra/server/routes/snapshot/index.ts @@ -9,12 +9,12 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { InfraBackendLibs } from '../../lib/infra_types'; -import { InfraSnapshotRequestOptions } from '../../lib/snapshot'; import { UsageCollector } from '../../usage/usage_collector'; import { parseFilterQuery } from '../../utils/serialized_query'; import { InfraNodeType, InfraSnapshotMetricInput } from '../../../public/graphql/types'; import { SnapshotRequestRT, SnapshotNodeResponseRT } from '../../../common/http_api/snapshot_api'; import { throwErrors } from '../../../common/runtime_types'; +import { InfraSnapshotRequestOptions } from '../../lib/snapshot/types'; const escapeHatch = schema.object({}, { allowUnknowns: true }); diff --git a/x-pack/legacy/plugins/infra/server/utils/calculate_metric_interval.ts b/x-pack/legacy/plugins/infra/server/utils/calculate_metric_interval.ts index 5eb5d424cdd73..6247c0f0298a0 100644 --- a/x-pack/legacy/plugins/infra/server/utils/calculate_metric_interval.ts +++ b/x-pack/legacy/plugins/infra/server/utils/calculate_metric_interval.ts @@ -5,6 +5,8 @@ */ import { RequestHandlerContext } from 'src/core/server'; +import { InfraNodeType } from '../graphql/types'; +import { findInventoryModel } from '../../common/inventory_models'; import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter'; interface Options { @@ -24,8 +26,14 @@ export const calculateMetricInterval = async ( framework: KibanaFramework, requestContext: RequestHandlerContext, options: Options, - modules: string[] + modules: string[], + nodeType?: InfraNodeType // TODO: check that this type still makes sense ) => { + let from = options.timerange.from; + if (nodeType) { + const inventoryModel = findInventoryModel(nodeType); + from = options.timerange.to - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000; + } const query = { allowNoIndices: true, index: options.indexPattern, @@ -37,7 +45,7 @@ export const calculateMetricInterval = async ( { range: { [options.timestampField]: { - gte: options.timerange.from, + gte: from, lte: options.timerange.to, format: 'epoch_millis', }, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5c5947de48a06..003c5c9ebc48b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6002,10 +6002,8 @@ "xpack.infra.metricsExplorer.openInTSVB": "ビジュアライザーで開く", "xpack.infra.metricsExplorer.viewNodeDetail": "{name} のメトリックを表示", "xpack.infra.node.ariaLabel": "{nodeName}、クリックしてメニューを開きます", - "xpack.infra.nodeContextMenu.viewAPMTraces": "{nodeType} APM トレースを表示", "xpack.infra.nodeContextMenu.viewLogsName": "ログを表示", "xpack.infra.nodeContextMenu.viewMetricsName": "メトリックを表示", - "xpack.infra.nodeContextMenu.viewUptimeLink": "アップタイムで {nodeType} を表示", "xpack.infra.nodesToWaffleMap.groupsWithGroups.allName": "すべて", "xpack.infra.nodesToWaffleMap.groupsWithNodes.allName": "すべて", "xpack.infra.notFoundPage.noContentFoundErrorTitle": "コンテンツがありません", @@ -6063,7 +6061,6 @@ "xpack.infra.waffle.metricOptions.outboundTrafficText": "送信トラフィック", "xpack.infra.waffle.noDataDescription": "期間またはフィルターを調整してみてください。", "xpack.infra.waffle.noDataTitle": "表示するデータがありません。", - "xpack.infra.waffle.nodeTypeSwitcher.hostsLabel": "すべてのホスト", "xpack.infra.waffle.selectTwoGroupingsTitle": "最大 2 つのグループ分けを選択してください", "xpack.infra.waffle.unableToSelectGroupErrorMessage": "{nodeType} のオプションでグループを選択できません", "xpack.infra.waffle.unableToSelectMetricErrorTitle": "メトリックのオプションまたは値を選択できません", @@ -12732,4 +12729,4 @@ "xpack.licensing.welcomeBanner.licenseIsExpiredDescription.updateYourLicenseLinkText": "ライセンスを更新", "xpack.licensing.welcomeBanner.licenseIsExpiredTitle": "ご使用の {licenseType} ライセンスは期限切れです" } -} \ No newline at end of file +} diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 59102702fc9b3..746af86eadb1b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5943,10 +5943,8 @@ "xpack.infra.metricsExplorer.openInTSVB": "在 Visualize 中打开", "xpack.infra.metricsExplorer.viewNodeDetail": "查看 {name} 的指标", "xpack.infra.node.ariaLabel": "{nodeName},单击打开菜单", - "xpack.infra.nodeContextMenu.viewAPMTraces": "查看 {nodeType} APM 跟踪", "xpack.infra.nodeContextMenu.viewLogsName": "查看日志", "xpack.infra.nodeContextMenu.viewMetricsName": "查看指标", - "xpack.infra.nodeContextMenu.viewUptimeLink": "在 Uptime 中查看 {nodeType}", "xpack.infra.nodesToWaffleMap.groupsWithGroups.allName": "全部", "xpack.infra.nodesToWaffleMap.groupsWithNodes.allName": "全部", "xpack.infra.notFoundPage.noContentFoundErrorTitle": "未找到任何内容", @@ -6004,7 +6002,6 @@ "xpack.infra.waffle.metricOptions.outboundTrafficText": "出站流量", "xpack.infra.waffle.noDataDescription": "尝试调整您的时间或筛选。", "xpack.infra.waffle.noDataTitle": "没有可显示的数据。", - "xpack.infra.waffle.nodeTypeSwitcher.hostsLabel": "主机", "xpack.infra.waffle.selectTwoGroupingsTitle": "选择最多两个分组", "xpack.infra.waffle.unableToSelectGroupErrorMessage": "无法选择 {nodeType} 的分组依据选项", "xpack.infra.waffle.unableToSelectMetricErrorTitle": "无法选择指标选项或指标值。", diff --git a/x-pack/test/api_integration/apis/infra/waffle.ts b/x-pack/test/api_integration/apis/infra/waffle.ts index 41bdb08932999..1f79ad4eee4e5 100644 --- a/x-pack/test/api_integration/apis/infra/waffle.ts +++ b/x-pack/test/api_integration/apis/infra/waffle.ts @@ -189,9 +189,9 @@ export default function({ getService }: FtrProviderContext) { expect(firstNode).to.have.property('metric'); expect(firstNode.metric).to.eql({ name: 'cpu', - value: 0.003666666666666667, - avg: 0.00809090909090909, - max: 0.057833333333333334, + value: 0.009285714285714286, + max: 0.009285714285714286, + avg: 0.0015476190476190477, }); } }); @@ -279,9 +279,9 @@ export default function({ getService }: FtrProviderContext) { expect(firstNode).to.have.property('metric'); expect(firstNode.metric).to.eql({ name: 'cpu', - value: 0.003666666666666667, - avg: 0.00809090909090909, - max: 0.057833333333333334, + value: 0.009285714285714286, + max: 0.009285714285714286, + avg: 0.0015476190476190477, }); const secondNode = nodes[1]; expect(secondNode).to.have.property('path'); @@ -291,9 +291,9 @@ export default function({ getService }: FtrProviderContext) { expect(secondNode).to.have.property('metric'); expect(secondNode.metric).to.eql({ name: 'cpu', - value: 0.003666666666666667, - avg: 0.00809090909090909, - max: 0.057833333333333334, + value: 0.009285714285714286, + max: 0.009285714285714286, + avg: 0.0015476190476190477, }); } }); From bf2db8d5a6636f7b2d141cf015752b6d696f3a28 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 12 Dec 2019 18:12:44 -0700 Subject: [PATCH 25/36] skip failing suite (#52969) (cherry picked from commit f634877b62e4b8ca69a764c25d4f19abb599c88d) --- .../kerberos_api_integration/apis/security/kerberos_login.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts index 0346da334d2f2..bd35f21d8f428 100644 --- a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts +++ b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts @@ -344,7 +344,8 @@ export default function({ getService }: FtrProviderContext) { }); }); - describe('API access with missing access token document or expired refresh token.', () => { + // FAILING: https://github.com/elastic/kibana/issues/52969 + describe.skip('API access with missing access token document or expired refresh token.', () => { let sessionCookie: Cookie; beforeEach(async () => { From 6ee092e8ac8775ad0656109910ff31db3d21c2a7 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 13 Dec 2019 04:25:21 +0000 Subject: [PATCH 26/36] chore(NA): merge and solve conflicts with upstream (#52972) --- package.json | 2 +- src/cli_keystore/cli_keystore.js | 25 ++++++++++++++++++++++--- x-pack/package.json | 2 +- yarn.lock | 7 ++++++- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index d03f4b7059833..8700cdb79cef5 100644 --- a/package.json +++ b/package.json @@ -152,7 +152,7 @@ "check-disk-space": "^2.1.0", "chokidar": "3.2.1", "color": "1.0.3", - "commander": "2.20.0", + "commander": "3.0.2", "compare-versions": "3.5.1", "core-js": "^3.2.1", "css-loader": "2.1.1", diff --git a/src/cli_keystore/cli_keystore.js b/src/cli_keystore/cli_keystore.js index 47b49d936028e..d20d55f23d83c 100644 --- a/src/cli_keystore/cli_keystore.js +++ b/src/cli_keystore/cli_keystore.js @@ -17,6 +17,7 @@ * under the License. */ +import _ from 'lodash'; import { join } from 'path'; import { pkg } from '../core/server/utils'; @@ -32,6 +33,7 @@ import { listCli } from './list'; import { addCli } from './add'; import { removeCli } from './remove'; +const argv = process.env.kbnWorkerArgv ? JSON.parse(process.env.kbnWorkerArgv) : process.argv.slice(); const program = new Command('bin/kibana-keystore'); program @@ -43,8 +45,25 @@ listCli(program, keystore); addCli(program, keystore); removeCli(program, keystore); -program.parse(process.argv); +program + .command('help ') + .description('get the help for a specific command') + .action(function (cmdName) { + const cmd = _.find(program.commands, { _name: cmdName }); + if (!cmd) return program.error(`unknown command ${cmdName}`); + cmd.help(); + }); + +program + .command('*', null, { noHelp: true }) + .action(function (cmd) { + program.error(`unknown command ${cmd}`); + }); -if (!program.args.length) { - program.help(); +// check for no command name +const subCommand = argv[2] && !String(argv[2][0]).match(/^-|^\.|\//); +if (!subCommand) { + program.defaultHelp(); } + +program.parse(process.argv); diff --git a/x-pack/package.json b/x-pack/package.json index 1356bf4b7edd6..96688f8f3800b 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -114,7 +114,7 @@ "chalk": "^2.4.2", "chance": "1.0.18", "cheerio": "0.22.0", - "commander": "2.20.0", + "commander": "3.0.2", "copy-webpack-plugin": "^5.0.4", "cypress": "^3.6.1", "cypress-multi-reporters": "^1.2.3", diff --git a/yarn.lock b/yarn.lock index 9d097368aeab7..420b22a664505 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8306,7 +8306,12 @@ commander@2.17.x, commander@~2.17.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== -commander@2.20.0, commander@^2.13.0, commander@^2.15.1, commander@^2.16.0, commander@^2.19.0, commander@^2.20.0, commander@^2.7.1: +commander@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" + integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== + +commander@^2.13.0, commander@^2.15.1, commander@^2.16.0, commander@^2.19.0, commander@^2.20.0, commander@^2.7.1: version "2.20.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== From 4823430a7a87e5dec2537c5f129ccd5cf2065d51 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 12 Dec 2019 21:36:45 -0700 Subject: [PATCH 27/36] [Maps] Move field-meta implementation to style property (#52828) (#52966) * improve clarity * rename for clarity * feedback * add comment * remove debug statements --- .../legend/style_property_legend_row.js | 5 +- .../components/legend/vector_style_legend.js | 2 +- .../vector/components/style_option_shapes.js | 5 - .../properties/dynamic_style_property.js | 47 ++++++++++ .../layers/styles/vector/vector_style.js | 94 +++++++------------ .../maps/public/layers/vector_layer.js | 2 +- 6 files changed, 83 insertions(+), 72 deletions(-) diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js index 6bd8b64ddf832..b8e4ea0cca4df 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js @@ -8,7 +8,6 @@ import _ from 'lodash'; import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { rangeShape } from '../style_option_shapes'; import { getVectorStyleLabel } from '../get_vector_style_label'; import { StyleLegendRow } from '../../../components/style_legend_row'; @@ -25,7 +24,7 @@ export class StylePropertyLegendRow extends Component { } render() { - const { range, style } = this.props; + const { meta: range, style } = this.props; const min = this._formatValue(_.get(range, 'min', EMPTY_VALUE)); const minLabel = this.props.style.isFieldMetaEnabled() && range && range.isMinOutsideStdRange ? `< ${min}` : min; @@ -48,6 +47,6 @@ export class StylePropertyLegendRow extends Component { StylePropertyLegendRow.propTypes = { label: PropTypes.string, fieldFormatter: PropTypes.func, - range: rangeShape, + meta: PropTypes.object, style: PropTypes.object }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_style_legend.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_style_legend.js index 5f4139432a912..a1c82b29a1590 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_style_legend.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_style_legend.js @@ -35,7 +35,7 @@ export class VectorStyleLegend extends Component { const rowDescriptors = rows.map(row => { return { label: row.label, - range: row.range, + range: row.meta, styleOptions: row.style.getOptions(), }; }); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_option_shapes.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_option_shapes.js index a2edc8cb4f686..d2b5178174e12 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_option_shapes.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_option_shapes.js @@ -35,8 +35,3 @@ export const dynamicSizeShape = PropTypes.shape({ maxSize: PropTypes.number.isRequired, field: fieldShape, }); - -export const rangeShape = PropTypes.shape({ - min: PropTypes.number.isRequired, - max: PropTypes.number.isRequired, -}); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js index a72502f9f17fb..1ce94f796573b 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js @@ -60,4 +60,51 @@ export class DynamicStyleProperty extends AbstractStyleProperty { getFieldMetaOptions() { return _.get(this.getOptions(), 'fieldMetaOptions', {}); } + + + pluckStyleMetaFromFeatures(features) { + + const name = this.getField().getName(); + let min = Infinity; + let max = -Infinity; + for (let i = 0; i < features.length; i++) { + const feature = features[i]; + const newValue = parseFloat(feature.properties[name]); + if (!isNaN(newValue)) { + min = Math.min(min, newValue); + max = Math.max(max, newValue); + } + } + + return (min === Infinity || max === -Infinity) ? null : ({ + min: min, + max: max, + delta: max - min + }); + + } + + pluckStyleMetaFromFieldMetaData(fieldMetaData) { + + const realFieldName = this._field.getESDocFieldName ? this._field.getESDocFieldName() : this._field.getName(); + const stats = fieldMetaData[realFieldName]; + if (!stats) { + return null; + } + + const sigma = _.get(this.getFieldMetaOptions(), 'sigma', DEFAULT_SIGMA); + const stdLowerBounds = stats.avg - (stats.std_deviation * sigma); + const stdUpperBounds = stats.avg + (stats.std_deviation * sigma); + const min = Math.max(stats.min, stdLowerBounds); + const max = Math.min(stats.max, stdUpperBounds); + return { + min, + max, + delta: max - min, + isMinOutsideStdRange: stats.min < stdLowerBounds, + isMaxOutsideStdRange: stats.max > stdUpperBounds, + }; + + } + } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js index 426af96b63ba2..3b7857180ccae 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js @@ -180,24 +180,17 @@ export class VectorStyle extends AbstractStyle { } async pluckStyleMetaFromSourceDataRequest(sourceDataRequest) { + const features = _.get(sourceDataRequest.getData(), 'features', []); if (features.length === 0) { return {}; } - const scaledFields = this.getDynamicPropertiesArray() - .map(styleProperty => { - return { - name: styleProperty.getField().getName(), - min: Infinity, - max: -Infinity - }; - }); + const dynamicProperties = this.getDynamicPropertiesArray(); const supportedFeatures = await this._source.getSupportedShapeTypes(); const isSingleFeatureType = supportedFeatures.length === 1; - - if (scaledFields.length === 0 && isSingleFeatureType) { + if (dynamicProperties.length === 0 && isSingleFeatureType) { // no meta data to pull from source data request. return {}; } @@ -216,15 +209,6 @@ export class VectorStyle extends AbstractStyle { if (!hasPolygons && POLYGONS.includes(feature.geometry.type)) { hasPolygons = true; } - - for (let j = 0; j < scaledFields.length; j++) { - const scaledField = scaledFields[j]; - const newValue = parseFloat(feature.properties[scaledField.name]); - if (!isNaN(newValue)) { - scaledField.min = Math.min(scaledField.min, newValue); - scaledField.max = Math.max(scaledField.max, newValue); - } - } } const featuresMeta = { @@ -235,13 +219,11 @@ export class VectorStyle extends AbstractStyle { } }; - scaledFields.forEach(({ min, max, name }) => { - if (min !== Infinity && max !== -Infinity) { - featuresMeta[name] = { - min, - max, - delta: max - min, - }; + dynamicProperties.forEach(dynamicProperty => { + const styleMeta = dynamicProperty.pluckStyleMetaFromFeatures(features); + if (styleMeta) { + const name = dynamicProperty.getField().getName(); + featuresMeta[name] = styleMeta; } }); @@ -290,13 +272,15 @@ export class VectorStyle extends AbstractStyle { return this._isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.POLYGON); } - _getFieldRange = (fieldName) => { - const fieldRangeFromLocalFeatures = _.get(this._descriptor, ['__styleMeta', fieldName]); + _getFieldMeta = (fieldName) => { + + const fieldMetaFromLocalFeatures = _.get(this._descriptor, ['__styleMeta', fieldName]); + const dynamicProps = this.getDynamicPropertiesArray(); const dynamicProp = dynamicProps.find(dynamicProp => { return fieldName === dynamicProp.getField().getName(); }); if (!dynamicProp || !dynamicProp.isFieldMetaEnabled()) { - return fieldRangeFromLocalFeatures; + return fieldMetaFromLocalFeatures; } let dataRequestId; @@ -313,34 +297,19 @@ export class VectorStyle extends AbstractStyle { } if (!dataRequestId) { - return fieldRangeFromLocalFeatures; + return fieldMetaFromLocalFeatures; } const styleMetaDataRequest = this._layer._findDataRequestForSource(dataRequestId); if (!styleMetaDataRequest || !styleMetaDataRequest.hasData()) { - return fieldRangeFromLocalFeatures; + return fieldMetaFromLocalFeatures; } const data = styleMetaDataRequest.getData(); - const field = dynamicProp.getField(); - const realFieldName = field.getESDocFieldName ? field.getESDocFieldName() : field.getName(); - const stats = data[realFieldName]; - if (!stats) { - return fieldRangeFromLocalFeatures; - } + const fieldMeta = dynamicProp.pluckStyleMetaFromFieldMetaData(data); + + return fieldMeta ? fieldMeta : fieldMetaFromLocalFeatures; - const sigma = _.get(dynamicProp.getFieldMetaOptions(), 'sigma', 3); - const stdLowerBounds = stats.avg - (stats.std_deviation * sigma); - const stdUpperBounds = stats.avg + (stats.std_deviation * sigma); - const min = Math.max(stats.min, stdLowerBounds); - const max = Math.min(stats.max, stdUpperBounds); - return { - min, - max, - delta: max - min, - isMinOutsideStdRange: stats.min < stdLowerBounds, - isMaxOutsideStdRange: stats.max > stdUpperBounds, - }; } _getStyleMeta = () => { @@ -392,7 +361,7 @@ export class VectorStyle extends AbstractStyle { return { label: await style.getField().getLabel(), fieldFormatter: await this._source.getFieldFormatter(style.getField().getName()), - range: this._getFieldRange(style.getField().getName()), + meta: this._getFieldMeta(style.getField().getName()), style, }; }); @@ -402,7 +371,7 @@ export class VectorStyle extends AbstractStyle { return ; } - _getStyleFields() { + _getFeatureStyleParams() { return this.getDynamicPropertiesArray() .map(styleProperty => { @@ -424,7 +393,7 @@ export class VectorStyle extends AbstractStyle { supportsFeatureState, isScaled, name: field.getName(), - range: this._getFieldRange(field.getName()), + meta: this._getFieldMeta(field.getName()), computedName: getComputedFieldName(styleProperty.getStyleName(), field.getName()), }; }); @@ -443,14 +412,14 @@ export class VectorStyle extends AbstractStyle { } } - setFeatureState(featureCollection, mbMap, sourceId) { + setFeatureStateAndStyleProps(featureCollection, mbMap, mbSourceId) { if (!featureCollection) { return; } - const styleFields = this._getStyleFields(); - if (styleFields.length === 0) { + const featureStateParams = this._getFeatureStyleParams(); + if (featureStateParams.length === 0) { return; } @@ -464,8 +433,8 @@ export class VectorStyle extends AbstractStyle { for (let i = 0; i < featureCollection.features.length; i++) { const feature = featureCollection.features[i]; - for (let j = 0; j < styleFields.length; j++) { - const { supportsFeatureState, isScaled, name, range, computedName } = styleFields[j]; + for (let j = 0; j < featureStateParams.length; j++) { + const { supportsFeatureState, isScaled, name, meta: range, computedName } = featureStateParams[j]; const value = parseFloat(feature.properties[name]); let styleValue; if (isScaled) { @@ -484,15 +453,16 @@ export class VectorStyle extends AbstractStyle { feature.properties[computedName] = styleValue; } } - tmpFeatureIdentifier.source = sourceId; + tmpFeatureIdentifier.source = mbSourceId; tmpFeatureIdentifier.id = feature.id; mbMap.setFeatureState(tmpFeatureIdentifier, tmpFeatureState); } - const hasGeoJsonProperties = styleFields.some(({ supportsFeatureState }) => { - return !supportsFeatureState; - }); - return hasGeoJsonProperties; + //returns boolean indicating if styles do not support feature-state and some values are stored in geojson properties + //this return-value is used in an optimization for style-updates with mapbox-gl. + //`true` indicates the entire data needs to reset on the source (otherwise the style-rules will not be reapplied) + //`false` indicates the data does not need to be reset on the store, because styles are re-evaluated if they use featureState + return featureStateParams.some(({ supportsFeatureState }) => !supportsFeatureState); } arePointsSymbolizedAsCircles() { diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index 60a6201ac872c..f7db17d06ee2a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -509,7 +509,7 @@ export class VectorLayer extends AbstractLayer { // "feature-state" data expressions are not supported with layout properties. // To work around this limitation, // scaled layout properties (like icon-size) must fall back to geojson property values :( - const hasGeoJsonProperties = this._style.setFeatureState(featureCollection, mbMap, this.getId()); + const hasGeoJsonProperties = this._style.setFeatureStateAndStyleProps(featureCollection, mbMap, this.getId()); if (featureCollection !== featureCollectionOnMap || hasGeoJsonProperties) { mbGeoJSONSource.setData(featureCollection); } From 087db5b65cf4f0c515b69eaf1cbe188445dfa319 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Fri, 13 Dec 2019 12:07:55 +0300 Subject: [PATCH 28/36] Kibana 7.0.0 URL field formatter doesn't render relative hyperlinks properly (#52874) (#52975) Closes #35235 --- src/legacy/ui/public/field_editor/field_editor.js | 3 +-- src/plugins/data/common/field_formats/field_format.ts | 4 ++++ src/plugins/data/public/index_patterns/fields/field.ts | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/legacy/ui/public/field_editor/field_editor.js b/src/legacy/ui/public/field_editor/field_editor.js index f3c5990caae64..130d969e5cd02 100644 --- a/src/legacy/ui/public/field_editor/field_editor.js +++ b/src/legacy/ui/public/field_editor/field_editor.js @@ -689,7 +689,6 @@ export class FieldEditor extends PureComponent { } saveField = async () => { - const fieldFormat = this.state.field.format; const field = this.state.field.toActualField(); const { indexPattern } = this.props; const { fieldFormatId } = this.state; @@ -727,7 +726,7 @@ export class FieldEditor extends PureComponent { if (!fieldFormatId) { indexPattern.fieldFormatMap[field.name] = undefined; } else { - indexPattern.fieldFormatMap[field.name] = fieldFormat; + indexPattern.fieldFormatMap[field.name] = field.format; } return indexPattern.save() diff --git a/src/plugins/data/common/field_formats/field_format.ts b/src/plugins/data/common/field_formats/field_format.ts index dd445a33f21c5..85d276767b5a7 100644 --- a/src/plugins/data/common/field_formats/field_format.ts +++ b/src/plugins/data/common/field_formats/field_format.ts @@ -194,6 +194,10 @@ export abstract class FieldFormat { [HTML_CONTEXT_TYPE]: htmlContentTypeSetup(this, this.htmlConvert), }; } + + static isInstanceOfFieldFormat(fieldFormat: any): fieldFormat is FieldFormat { + return Boolean(fieldFormat && fieldFormat.convert); + } } export type IFieldFormat = PublicMethodsOf; diff --git a/src/plugins/data/public/index_patterns/fields/field.ts b/src/plugins/data/public/index_patterns/fields/field.ts index c8c8ac1ffd321..6ed3c2be8f96e 100644 --- a/src/plugins/data/public/index_patterns/fields/field.ts +++ b/src/plugins/data/public/index_patterns/fields/field.ts @@ -94,7 +94,8 @@ export class Field implements IFieldType { if (!type) type = getKbnFieldType('unknown'); let format = spec.format; - if (!format || !(format instanceof FieldFormat)) { + + if (!FieldFormat.isInstanceOfFieldFormat(format)) { const fieldFormats = getFieldFormats(); format = From ad5b013d737502bb3b07d13e0bf290e8a84bc5d9 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Fri, 13 Dec 2019 10:58:16 +0100 Subject: [PATCH 29/36] [Console] Fix load from remote (#52814) (#52895) * Fix load remote state * Clean up variable usage, add comment, move forceRetokenize to private method * Optimize sequence of checking hash on initial load --- .../editor/legacy/console_editor/editor.tsx | 77 +++++++++++++++---- .../application/hooks/use_set_input_editor.ts | 13 ++-- .../legacy_core_editor/legacy_core_editor.ts | 35 +++++---- 3 files changed, 90 insertions(+), 35 deletions(-) diff --git a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.tsx b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.tsx index 442ed330e9b7a..a52c15c20c902 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.tsx @@ -20,6 +20,11 @@ import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react'; import { EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { debounce } from 'lodash'; + +// Node v5 querystring for browser. +// @ts-ignore +import * as qs from 'querystring-browser'; import { EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useServicesContext, useEditorReadContext } from '../../../../contexts'; @@ -80,17 +85,64 @@ function EditorUI() { useEffect(() => { editorInstanceRef.current = senseEditor.create(editorRef.current!); + const editor = editorInstanceRef.current; + + const readQueryParams = () => { + const [, queryString] = (window.location.hash || '').split('?'); + return qs.parse(queryString || ''); + }; + + const loadBufferFromRemote = (url: string) => { + if (/^https?:\/\//.test(url)) { + const loadFrom: Record = { + url, + // Having dataType here is required as it doesn't allow jQuery to `eval` content + // coming from the external source thereby preventing XSS attack. + dataType: 'text', + kbnXsrfToken: false, + }; + + if (/https?:\/\/api\.github\.com/.test(url)) { + loadFrom.headers = { Accept: 'application/vnd.github.v3.raw' }; + } - const { content: text } = history.getSavedEditorState() || { - content: DEFAULT_INPUT_VALUE, + // Fire and forget. + $.ajax(loadFrom).done(async data => { + const coreEditor = editor.getCoreEditor(); + await editor.update(data, true); + editor.moveToNextRequestEdge(false); + coreEditor.clearSelection(); + editor.highlightCurrentRequestsAndUpdateActionBar(); + coreEditor.getContainer().focus(); + }); + } }; - editorInstanceRef.current.update(text); + + // Support for loading a console snippet from a remote source, like support docs. + const onHashChange = debounce(() => { + const { load_from: url } = readQueryParams(); + if (!url) { + return; + } + loadBufferFromRemote(url); + }, 200); + window.addEventListener('hashchange', onHashChange); + + const initialQueryParams = readQueryParams(); + if (initialQueryParams.load_from) { + loadBufferFromRemote(initialQueryParams.load_from); + } else { + const { content: text } = history.getSavedEditorState() || { + content: DEFAULT_INPUT_VALUE, + }; + editor.update(text); + } function setupAutosave() { let timer: number; const saveDelay = 500; - editorInstanceRef.current!.getCoreEditor().on('change', () => { + editor.getCoreEditor().on('change', () => { if (timer) { clearTimeout(timer); } @@ -100,35 +152,34 @@ function EditorUI() { function saveCurrentState() { try { - const content = editorInstanceRef.current!.getCoreEditor().getValue(); + const content = editor.getCoreEditor().getValue(); history.updateCurrentState(content); } catch (e) { // Ignoring saving error } } - setInputEditor(editorInstanceRef.current); + setInputEditor(editor); setTextArea(editorRef.current!.querySelector('textarea')); mappings.retrieveAutoCompleteInfo(); - const unsubscribeResizer = subscribeResizeChecker( - editorRef.current!, - editorInstanceRef.current.getCoreEditor() - ); + const unsubscribeResizer = subscribeResizeChecker(editorRef.current!, editor.getCoreEditor()); setupAutosave(); return () => { unsubscribeResizer(); mappings.clearSubscriptions(); + window.removeEventListener('hashchange', onHashChange); }; }, [history, setInputEditor]); useEffect(() => { - applyCurrentSettings(editorInstanceRef.current!.getCoreEditor(), settings); + const { current: editor } = editorInstanceRef; + applyCurrentSettings(editor!.getCoreEditor(), settings); // Preserve legacy focus behavior after settings have updated. - editorInstanceRef - .current!.getCoreEditor() + editor! + .getCoreEditor() .getContainer() .focus(); }, [settings]); diff --git a/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_set_input_editor.ts b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_set_input_editor.ts index 672f3e269ead9..fbd53762c27e6 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_set_input_editor.ts +++ b/src/legacy/core_plugins/console/public/np_ready/application/hooks/use_set_input_editor.ts @@ -16,15 +16,18 @@ * specific language governing permissions and limitations * under the License. */ - +import { useCallback } from 'react'; import { useEditorActionContext } from '../contexts/editor_context'; import { instance as registry } from '../contexts/editor_context/editor_registry'; export const useSetInputEditor = () => { const dispatch = useEditorActionContext(); - return (editor: any) => { - dispatch({ type: 'setInputEditor', payload: editor }); - registry.setInputEditor(editor); - }; + return useCallback( + (editor: any) => { + dispatch({ type: 'setInputEditor', payload: editor }); + registry.setInputEditor(editor); + }, + [dispatch] + ); }; diff --git a/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/legacy_core_editor.ts b/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/legacy_core_editor.ts index 621f4eeb0163e..608c73335b3e5 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/legacy_core_editor.ts +++ b/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/legacy_core_editor.ts @@ -104,25 +104,12 @@ export class LegacyCoreEditor implements CoreEditor { return this.editor.getValue(); } - setValue(text: string, forceRetokenize: boolean): Promise { + async setValue(text: string, forceRetokenize: boolean): Promise { const session = this.editor.getSession(); session.setValue(text); - return new Promise(resolve => { - if (!forceRetokenize) { - // resolve immediately - resolve(); - return; - } - - // force update of tokens, but not on this thread to allow for ace rendering. - setTimeout(function() { - let i; - for (i = 0; i < session.getLength(); i++) { - session.getTokens(i); - } - resolve(); - }); - }); + if (forceRetokenize) { + await this.forceRetokenize(); + } } getLineValue(lineNumber: number): string { @@ -241,6 +228,20 @@ export class LegacyCoreEditor implements CoreEditor { return Boolean((this.editor as any).completer && (this.editor as any).completer.activated); } + private forceRetokenize() { + const session = this.editor.getSession(); + return new Promise(resolve => { + // force update of tokens, but not on this thread to allow for ace rendering. + setTimeout(function() { + let i; + for (i = 0; i < session.getLength(); i++) { + session.getTokens(i); + } + resolve(); + }); + }); + } + // eslint-disable-next-line @typescript-eslint/camelcase private DO_NOT_USE_onPaste(text: string) { if (text && curl.detectCURL(text)) { From 09e269508a94bab58bc3484698e933cd1742c467 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Fri, 13 Dec 2019 11:44:39 +0100 Subject: [PATCH 30/36] Implements config deprecation in New Platform (#52251) (#52976) * implements 'rename' and 'unset' deprecations * introduce usage of ConfigDeprecationProvider * adapt RawConfigService to only returns unmodified raw config * apply deprecations when accessing config * register legacy plugin deprecation in new platform * implements ConfigService#validate * add exemple config deprecation usage in testbed * documentation * export public config deprecation types * fix new test due to rebase * name ConfigDeprecationFactory * update generated doc * add tests for unset and move it to src/core/utils * add tests for renameFromRoot and unusedFromRoot * cast paths as any as get expects a fixed-length string array * use specific logger for deprecations * add additional test on renameFromRoot * update migration guide * migrate core deprecations to NP * add integration test * use same log context as legacy * remove old deprecation warnings integration tests, now covered in NP * migrates csp deprecation to NP * removes deprecationWarningMixin from legacy * remove legacy core deprecations * remove unused import * rename setupConfigSchemas to setupCoreConfig * update generated doc --- ...a-plugin-public.savedobjectsclient.find.md | 2 +- ...kibana-plugin-public.savedobjectsclient.md | 2 +- .../kibana-plugin-server.configdeprecation.md | 18 + ...-plugin-server.configdeprecationfactory.md | 36 ++ ...-server.configdeprecationfactory.rename.md | 36 ++ ...configdeprecationfactory.renamefromroot.md | 38 ++ ...-server.configdeprecationfactory.unused.md | 35 ++ ...configdeprecationfactory.unusedfromroot.md | 37 ++ ...a-plugin-server.configdeprecationlogger.md | 13 + ...plugin-server.configdeprecationprovider.md | 28 ++ .../core/server/kibana-plugin-server.md | 6 +- ...ver.pluginconfigdescriptor.deprecations.md | 13 + ...na-plugin-server.pluginconfigdescriptor.md | 7 +- src/cli/cluster/cluster_manager.js | 3 +- src/core/MIGRATION.md | 68 +++- src/core/public/public.api.md | 2 +- src/core/server/bootstrap.ts | 9 +- src/core/server/config/config_service.mock.ts | 2 + .../config/config_service.test.mocks.ts | 5 + src/core/server/config/config_service.test.ts | 199 ++++++--- src/core/server/config/config_service.ts | 84 +++- .../deprecation/apply_deprecations.test.ts | 85 ++++ .../config/deprecation/apply_deprecations.ts | 40 ++ .../deprecation/core_deprecations.test.ts | 211 ++++++++++ .../config/deprecation/core_deprecations.ts | 114 ++++++ .../deprecation/deprecation_factory.test.ts | 379 ++++++++++++++++++ .../config/deprecation/deprecation_factory.ts | 95 +++++ .../server/config/deprecation/index.ts} | 26 +- src/core/server/config/deprecation/types.ts | 141 +++++++ src/core/server/config/index.ts | 9 +- .../config_deprecation.test.mocks.ts} | 13 +- .../config_deprecation.test.ts | 64 +++ .../server/config/raw_config_service.mock.ts | 39 ++ .../server/config/raw_config_service.test.ts | 34 +- src/core/server/config/raw_config_service.ts | 11 +- src/core/server/http/http_service.test.ts | 13 +- src/core/server/index.ts | 11 +- .../config/ensure_valid_configuration.test.ts | 2 +- .../config/ensure_valid_configuration.ts | 2 +- .../config/get_unused_config_keys.test.ts | 67 +--- .../legacy/config/get_unused_config_keys.ts | 9 +- src/core/server/legacy/config/index.ts | 8 +- .../legacy_deprecation_adapters.test.ts | 106 +++++ .../config/legacy_deprecation_adapters.ts | 57 +++ .../config/legacy_object_to_config_adapter.ts | 3 +- src/core/server/legacy/config/types.ts | 30 ++ src/core/server/legacy/legacy_service.test.ts | 72 +++- src/core/server/legacy/legacy_service.ts | 16 +- .../legacy/logging/legacy_logging_server.ts | 4 +- .../plugins/find_legacy_plugin_specs.ts | 3 +- .../server/logging/logging_service.mock.ts | 2 +- .../discovery/plugins_discovery.test.ts | 8 +- .../server/plugins/plugin_context.test.ts | 8 +- .../server/plugins/plugins_service.test.ts | 50 ++- src/core/server/plugins/plugins_service.ts | 6 + src/core/server/plugins/types.ts | 12 +- src/core/server/root/index.test.mocks.ts | 8 +- src/core/server/root/index.test.ts | 20 +- src/core/server/root/index.ts | 10 +- src/core/server/server.api.md | 33 +- src/core/server/server.test.ts | 32 +- src/core/server/server.ts | 20 +- src/core/utils/index.ts | 1 + src/core/utils/unset.test.ts | 104 +++++ src/core/utils/unset.ts | 49 +++ .../plugin_discovery/find_plugin_specs.js | 6 +- .../plugin_config/settings.js | 3 +- .../config/__tests__/deprecation_warnings.js | 116 ------ src/legacy/server/config/index.js | 1 - .../server/config/transform_deprecations.js | 127 ------ .../config/transform_deprecations.test.js | 182 --------- src/legacy/server/kbn_server.js | 5 +- src/plugins/testbed/server/index.ts | 5 + src/test_utils/kbn_server.ts | 7 +- 74 files changed, 2399 insertions(+), 723 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.configdeprecation.md create mode 100644 docs/development/core/server/kibana-plugin-server.configdeprecationfactory.md create mode 100644 docs/development/core/server/kibana-plugin-server.configdeprecationfactory.rename.md create mode 100644 docs/development/core/server/kibana-plugin-server.configdeprecationfactory.renamefromroot.md create mode 100644 docs/development/core/server/kibana-plugin-server.configdeprecationfactory.unused.md create mode 100644 docs/development/core/server/kibana-plugin-server.configdeprecationfactory.unusedfromroot.md create mode 100644 docs/development/core/server/kibana-plugin-server.configdeprecationlogger.md create mode 100644 docs/development/core/server/kibana-plugin-server.configdeprecationprovider.md create mode 100644 docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.deprecations.md create mode 100644 src/core/server/config/deprecation/apply_deprecations.test.ts create mode 100644 src/core/server/config/deprecation/apply_deprecations.ts create mode 100644 src/core/server/config/deprecation/core_deprecations.test.ts create mode 100644 src/core/server/config/deprecation/core_deprecations.ts create mode 100644 src/core/server/config/deprecation/deprecation_factory.test.ts create mode 100644 src/core/server/config/deprecation/deprecation_factory.ts rename src/{legacy/server/config/__tests__/fixtures/run_kbn_server_startup.js => core/server/config/deprecation/index.ts} (63%) create mode 100644 src/core/server/config/deprecation/types.ts rename src/{legacy/server/config/deprecation_warnings.js => core/server/config/integration_tests/config_deprecation.test.mocks.ts} (70%) create mode 100644 src/core/server/config/integration_tests/config_deprecation.test.ts create mode 100644 src/core/server/config/raw_config_service.mock.ts create mode 100644 src/core/server/legacy/config/legacy_deprecation_adapters.test.ts create mode 100644 src/core/server/legacy/config/legacy_deprecation_adapters.ts create mode 100644 src/core/utils/unset.test.ts create mode 100644 src/core/utils/unset.ts delete mode 100644 src/legacy/server/config/__tests__/deprecation_warnings.js delete mode 100644 src/legacy/server/config/transform_deprecations.js delete mode 100644 src/legacy/server/config/transform_deprecations.test.js diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md index 1ce18834f5319..a4fa3f17d0d94 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md @@ -9,5 +9,5 @@ Search for objects Signature: ```typescript -find: (options: Pick) => Promise>; +find: (options: Pick) => Promise>; ``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md index 6033c667c1866..3c4e33db4af91 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md @@ -20,7 +20,7 @@ export declare class SavedObjectsClient | [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | (objects?: {
id: string;
type: string;
}[]) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>> | Returns an array of objects by id | | [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>> | Persists an object | | [delete](./kibana-plugin-public.savedobjectsclient.delete.md) | | (type: string, id: string) => Promise<{}> | Deletes an object | -| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "fields" | "searchFields" | "defaultSearchOperator" | "hasReference" | "sortField" | "perPage">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | +| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "perPage" | "sortField" | "fields" | "searchFields" | "hasReference" | "defaultSearchOperator">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | | [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <T extends SavedObjectAttributes>(type: string, id: string) => Promise<SimpleSavedObject<T>> | Fetches a single object | ## Methods diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecation.md b/docs/development/core/server/kibana-plugin-server.configdeprecation.md new file mode 100644 index 0000000000000..ba7e40b8dc624 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecation.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) + +## ConfigDeprecation type + +Configuration deprecation returned from [ConfigDeprecationProvider](./kibana-plugin-server.configdeprecationprovider.md) that handles a single deprecation from the configuration. + +Signature: + +```typescript +export declare type ConfigDeprecation = (config: Record, fromPath: string, logger: ConfigDeprecationLogger) => Record; +``` + +## Remarks + +This should only be manually implemented if [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) does not provide the proper helpers for a specific deprecation need. + diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.md b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.md new file mode 100644 index 0000000000000..f022d6c1d064a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.md @@ -0,0 +1,36 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) + +## ConfigDeprecationFactory interface + +Provides helpers to generates the most commonly used [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) when invoking a [ConfigDeprecationProvider](./kibana-plugin-server.configdeprecationprovider.md). + +See methods documentation for more detailed examples. + +Signature: + +```typescript +export interface ConfigDeprecationFactory +``` + +## Methods + +| Method | Description | +| --- | --- | +| [rename(oldKey, newKey)](./kibana-plugin-server.configdeprecationfactory.rename.md) | Rename a configuration property from inside a plugin's configuration path. Will log a deprecation warning if the oldKey was found and deprecation applied. | +| [renameFromRoot(oldKey, newKey)](./kibana-plugin-server.configdeprecationfactory.renamefromroot.md) | Rename a configuration property from the root configuration. Will log a deprecation warning if the oldKey was found and deprecation applied.This should be only used when renaming properties from different configuration's path. To rename properties from inside a plugin's configuration, use 'rename' instead. | +| [unused(unusedKey)](./kibana-plugin-server.configdeprecationfactory.unused.md) | Remove a configuration property from inside a plugin's configuration path. Will log a deprecation warning if the unused key was found and deprecation applied. | +| [unusedFromRoot(unusedKey)](./kibana-plugin-server.configdeprecationfactory.unusedfromroot.md) | Remove a configuration property from the root configuration. Will log a deprecation warning if the unused key was found and deprecation applied.This should be only used when removing properties from outside of a plugin's configuration. To remove properties from inside a plugin's configuration, use 'unused' instead. | + +## Example + + +```typescript +const provider: ConfigDeprecationProvider = ({ rename, unused }) => [ + rename('oldKey', 'newKey'), + unused('deprecatedKey'), +] + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.rename.md b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.rename.md new file mode 100644 index 0000000000000..5bbbad763c545 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.rename.md @@ -0,0 +1,36 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) > [rename](./kibana-plugin-server.configdeprecationfactory.rename.md) + +## ConfigDeprecationFactory.rename() method + +Rename a configuration property from inside a plugin's configuration path. Will log a deprecation warning if the oldKey was found and deprecation applied. + +Signature: + +```typescript +rename(oldKey: string, newKey: string): ConfigDeprecation; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| oldKey | string | | +| newKey | string | | + +Returns: + +`ConfigDeprecation` + +## Example + +Rename 'myplugin.oldKey' to 'myplugin.newKey' + +```typescript +const provider: ConfigDeprecationProvider = ({ rename }) => [ + rename('oldKey', 'newKey'), +] + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.renamefromroot.md b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.renamefromroot.md new file mode 100644 index 0000000000000..d35ba25256fa1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.renamefromroot.md @@ -0,0 +1,38 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) > [renameFromRoot](./kibana-plugin-server.configdeprecationfactory.renamefromroot.md) + +## ConfigDeprecationFactory.renameFromRoot() method + +Rename a configuration property from the root configuration. Will log a deprecation warning if the oldKey was found and deprecation applied. + +This should be only used when renaming properties from different configuration's path. To rename properties from inside a plugin's configuration, use 'rename' instead. + +Signature: + +```typescript +renameFromRoot(oldKey: string, newKey: string): ConfigDeprecation; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| oldKey | string | | +| newKey | string | | + +Returns: + +`ConfigDeprecation` + +## Example + +Rename 'oldplugin.key' to 'newplugin.key' + +```typescript +const provider: ConfigDeprecationProvider = ({ renameFromRoot }) => [ + renameFromRoot('oldplugin.key', 'newplugin.key'), +] + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.unused.md b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.unused.md new file mode 100644 index 0000000000000..0381480e84c4d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.unused.md @@ -0,0 +1,35 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) > [unused](./kibana-plugin-server.configdeprecationfactory.unused.md) + +## ConfigDeprecationFactory.unused() method + +Remove a configuration property from inside a plugin's configuration path. Will log a deprecation warning if the unused key was found and deprecation applied. + +Signature: + +```typescript +unused(unusedKey: string): ConfigDeprecation; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| unusedKey | string | | + +Returns: + +`ConfigDeprecation` + +## Example + +Flags 'myplugin.deprecatedKey' as unused + +```typescript +const provider: ConfigDeprecationProvider = ({ unused }) => [ + unused('deprecatedKey'), +] + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.unusedfromroot.md b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.unusedfromroot.md new file mode 100644 index 0000000000000..ed37638b07375 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.unusedfromroot.md @@ -0,0 +1,37 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) > [unusedFromRoot](./kibana-plugin-server.configdeprecationfactory.unusedfromroot.md) + +## ConfigDeprecationFactory.unusedFromRoot() method + +Remove a configuration property from the root configuration. Will log a deprecation warning if the unused key was found and deprecation applied. + +This should be only used when removing properties from outside of a plugin's configuration. To remove properties from inside a plugin's configuration, use 'unused' instead. + +Signature: + +```typescript +unusedFromRoot(unusedKey: string): ConfigDeprecation; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| unusedKey | string | | + +Returns: + +`ConfigDeprecation` + +## Example + +Flags 'somepath.deprecatedProperty' as unused + +```typescript +const provider: ConfigDeprecationProvider = ({ unusedFromRoot }) => [ + unusedFromRoot('somepath.deprecatedProperty'), +] + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationlogger.md b/docs/development/core/server/kibana-plugin-server.configdeprecationlogger.md new file mode 100644 index 0000000000000..d2bb2a4e441b3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationlogger.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigDeprecationLogger](./kibana-plugin-server.configdeprecationlogger.md) + +## ConfigDeprecationLogger type + +Logger interface used when invoking a [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) + +Signature: + +```typescript +export declare type ConfigDeprecationLogger = (message: string) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationprovider.md b/docs/development/core/server/kibana-plugin-server.configdeprecationprovider.md new file mode 100644 index 0000000000000..f5da9e9452bb5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationprovider.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigDeprecationProvider](./kibana-plugin-server.configdeprecationprovider.md) + +## ConfigDeprecationProvider type + +A provider that should returns a list of [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md). + +See [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) for more usage examples. + +Signature: + +```typescript +export declare type ConfigDeprecationProvider = (factory: ConfigDeprecationFactory) => ConfigDeprecation[]; +``` + +## Example + + +```typescript +const provider: ConfigDeprecationProvider = ({ rename, unused }) => [ + rename('oldKey', 'newKey'), + unused('deprecatedKey'), + myCustomDeprecation, +] + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 1506fdbb2b37f..06dcede0f2dfe 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -46,6 +46,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [Capabilities](./kibana-plugin-server.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. | | [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) | APIs to manage the [Capabilities](./kibana-plugin-server.capabilities.md) that will be used by the application.Plugins relying on capabilities to toggle some of their features should register them during the setup phase using the registerProvider method.Plugins having the responsibility to restrict capabilities depending on a given context should register their capabilities switcher using the registerSwitcher method.Refers to the methods documentation for complete description and examples. | | [CapabilitiesStart](./kibana-plugin-server.capabilitiesstart.md) | APIs to access the application [Capabilities](./kibana-plugin-server.capabilities.md). | +| [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) | Provides helpers to generates the most commonly used [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) when invoking a [ConfigDeprecationProvider](./kibana-plugin-server.configdeprecationprovider.md).See methods documentation for more detailed examples. | | [ContextSetup](./kibana-plugin-server.contextsetup.md) | An object that handles registration of context providers and configuring handlers with context. | | [CoreSetup](./kibana-plugin-server.coresetup.md) | Context passed to the plugins setup method. | | [CoreStart](./kibana-plugin-server.corestart.md) | Context passed to the plugins start method. | @@ -82,7 +83,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [OnPreResponseToolkit](./kibana-plugin-server.onpreresponsetoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | | [PackageInfo](./kibana-plugin-server.packageinfo.md) | | | [Plugin](./kibana-plugin-server.plugin.md) | The interface that should be returned by a PluginInitializer. | -| [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) | Describes a plugin configuration schema and capabilities. | +| [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) | Describes a plugin configuration properties. | | [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | | [PluginManifest](./kibana-plugin-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. | | [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) | | @@ -152,6 +153,9 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AuthResult](./kibana-plugin-server.authresult.md) | | | [CapabilitiesProvider](./kibana-plugin-server.capabilitiesprovider.md) | See [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) | | [CapabilitiesSwitcher](./kibana-plugin-server.capabilitiesswitcher.md) | See [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) | +| [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) | Configuration deprecation returned from [ConfigDeprecationProvider](./kibana-plugin-server.configdeprecationprovider.md) that handles a single deprecation from the configuration. | +| [ConfigDeprecationLogger](./kibana-plugin-server.configdeprecationlogger.md) | Logger interface used when invoking a [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) | +| [ConfigDeprecationProvider](./kibana-plugin-server.configdeprecationprovider.md) | A provider that should returns a list of [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md).See [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) for more usage examples. | | [ConfigPath](./kibana-plugin-server.configpath.md) | | | [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) | | | [GetAuthHeaders](./kibana-plugin-server.getauthheaders.md) | Get headers to authenticate a user against Elasticsearch. | diff --git a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.deprecations.md b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.deprecations.md new file mode 100644 index 0000000000000..00574101838f2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.deprecations.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) > [deprecations](./kibana-plugin-server.pluginconfigdescriptor.deprecations.md) + +## PluginConfigDescriptor.deprecations property + +Provider for the [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) to apply to the plugin configuration. + +Signature: + +```typescript +deprecations?: ConfigDeprecationProvider; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md index 41fdcfe5df45d..671298a67381a 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md +++ b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md @@ -4,7 +4,7 @@ ## PluginConfigDescriptor interface -Describes a plugin configuration schema and capabilities. +Describes a plugin configuration properties. Signature: @@ -16,6 +16,7 @@ export interface PluginConfigDescriptor | Property | Type | Description | | --- | --- | --- | +| [deprecations](./kibana-plugin-server.pluginconfigdescriptor.deprecations.md) | ConfigDeprecationProvider | Provider for the [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) to apply to the plugin configuration. | | [exposeToBrowser](./kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md) | {
[P in keyof T]?: boolean;
} | List of configuration properties that will be available on the client-side plugin. | | [schema](./kibana-plugin-server.pluginconfigdescriptor.schema.md) | PluginConfigSchema<T> | Schema to use to validate the plugin configuration.[PluginConfigSchema](./kibana-plugin-server.pluginconfigschema.md) | @@ -39,6 +40,10 @@ export const config: PluginConfigDescriptor = { uiProp: true, }, schema: configSchema, + deprecations: ({ rename, unused }) => [ + rename('securityKey', 'secret'), + unused('deprecatedProperty'), + ], }; ``` diff --git a/src/cli/cluster/cluster_manager.js b/src/cli/cluster/cluster_manager.js index d322c878e105b..a07ab709cdea7 100644 --- a/src/cli/cluster/cluster_manager.js +++ b/src/cli/cluster/cluster_manager.js @@ -29,7 +29,6 @@ import { REPO_ROOT } from '@kbn/dev-utils'; import Log from '../log'; import Worker from './worker'; import { Config } from '../../legacy/server/config/config'; -import { transformDeprecations } from '../../legacy/server/config/transform_deprecations'; process.env.kbnWorkerType = 'managr'; @@ -37,7 +36,7 @@ export default class ClusterManager { static create(opts, settings = {}, basePathProxy) { return new ClusterManager( opts, - Config.withDefaultSchema(transformDeprecations(settings)), + Config.withDefaultSchema(settings), basePathProxy ); } diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 6825b8e33b36a..10be6c0ac2ea4 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -47,6 +47,7 @@ - [UI Exports](#ui-exports) - [How to](#how-to) - [Configure plugin](#configure-plugin) + - [Handle plugin configuration deprecations](#handle-plugin-config-deprecations) - [Mock new platform services in tests](#mock-new-platform-services-in-tests) - [Writing mocks for your plugin](#writing-mocks-for-your-plugin) - [Using mocks in your tests](#using-mocks-in-your-tests) @@ -1220,13 +1221,20 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy'; In server code, `core` can be accessed from either `server.newPlatform` or `kbnServer.newPlatform`. There are not currently very many services available on the server-side: -| Legacy Platform | New Platform | Notes | -| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -| `server.config()` | [`initializerContext.config.create()`](/docs/development/core/server/kibana-plugin-server.plugininitializercontext.config.md) | Must also define schema. See _[how to configure plugin](#configure-plugin)_ | -| `server.route` | [`core.http.createRouter`](/docs/development/core/server/kibana-plugin-server.httpservicesetup.createrouter.md) | [Examples](./MIGRATION_EXAMPLES.md#route-registration) | -| `request.getBasePath()` | [`core.http.basePath.get`](/docs/development/core/server/kibana-plugin-server.httpservicesetup.basepath.md) | | -| `server.plugins.elasticsearch.getCluster('data')` | [`core.elasticsearch.dataClient$`](/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient_.md) | Handlers will also include a pre-configured client | -| `server.plugins.elasticsearch.getCluster('admin')` | [`core.elasticsearch.adminClient$`](/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient_.md) | Handlers will also include a pre-configured client | +| Legacy Platform | New Platform | Notes | +| ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| `server.config()` | [`initializerContext.config.create()`](/docs/development/core/server/kibana-plugin-server.plugininitializercontext.config.md) | Must also define schema. See _[how to configure plugin](#configure-plugin)_ | +| `server.route` | [`core.http.createRouter`](/docs/development/core/server/kibana-plugin-server.httpservicesetup.createrouter.md) | [Examples](./MIGRATION_EXAMPLES.md#route-registration) | +| `request.getBasePath()` | [`core.http.basePath.get`](/docs/development/core/server/kibana-plugin-server.httpservicesetup.basepath.md) | | +| `server.plugins.elasticsearch.getCluster('data')` | [`core.elasticsearch.dataClient$`](/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient_.md) | Handlers will also include a pre-configured client | +| `server.plugins.elasticsearch.getCluster('admin')` | [`core.elasticsearch.adminClient$`](/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient_.md) | Handlers will also include a pre-configured client | +| `xpackMainPlugin.info.feature(pluginID).registerLicenseCheckResultsGenerator` | [`x-pack licensing plugin`](/x-pack/plugins/licensing/README.md) | | +| `server.savedObjects.setScopedSavedObjectsClientFactory` | [`core.savedObjects.setClientFactory`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.setclientfactory.md) | | +| `server.savedObjects.addScopedSavedObjectsClientWrapperFactory` | [`core.savedObjects.addClientWrapper`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.addclientwrapper.md) | | +| `server.savedObjects.getSavedObjectsRepository` | [`core.savedObjects.createInternalRepository`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createinternalrepository.md) [`core.savedObjects.createScopedRepository`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createscopedrepository.md) | | +| `server.savedObjects.getScopedSavedObjectsClient` | [`core.savedObjects.getScopedClient`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicestart.getscopedclient.md) | | +| `request.getSavedObjectsClient` | [`context.core.savedObjects.client`](/docs/development/core/server/kibana-plugin-server.requesthandlercontext.core.md) | | +| `kibana.Plugin.deprecations` | [Handle plugin configuration deprecations](#handle-plugin-config-deprecations) and [`PluginConfigDescriptor.deprecations`](docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md) | Deprecations from New Platform are not applied to legacy configuration | | `xpackMainPlugin.info.feature(pluginID).registerLicenseCheckResultsGenerator` | [`x-pack licensing plugin`](/x-pack/plugins/licensing/README.md) | | _See also: [Server's CoreSetup API Docs](/docs/development/core/server/kibana-plugin-server.coresetup.md)_ @@ -1399,6 +1407,52 @@ export const config = { }; ``` +#### Handle plugin configuration deprecations + +If your plugin have deprecated properties, you can describe them using the `deprecations` config descriptor field. + +The system is quite similar to the legacy plugin's deprecation management. The most important difference +is that deprecations are managed on a per-plugin basis, meaning that you don't need to specify the whole +property path, but use the relative path from your plugin's configuration root. + +```typescript +// my_plugin/server/index.ts +import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from 'kibana/server'; + +const configSchema = schema.object({ + newProperty: schema.string({ defaultValue: 'Some string' }), +}); + +type ConfigType = TypeOf; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + deprecations: ({ rename, unused }) => [ + rename('oldProperty', 'newProperty'), + unused('someUnusedProperty'), + ] +}; +``` + +In some cases, accessing the whole configuration for deprecations is necessary. For these edge cases, +`renameFromRoot` and `unusedFromRoot` are also accessible when declaring deprecations. + +```typescript +// my_plugin/server/index.ts +export const config: PluginConfigDescriptor = { + schema: configSchema, + deprecations: ({ renameFromRoot, unusedFromRoot }) => [ + renameFromRoot('oldplugin.property', 'myplugin.property'), + unusedFromRoot('oldplugin.deprecated'), + ] +}; +``` + +Note that deprecations registered in new platform's plugins are not applied to the legacy configuration. +During migration, if you still need the deprecations to be effective in the legacy plugin, you need to declare them in +both plugin definitions. + ### Mock new platform services in tests #### Writing mocks for your plugin diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index b745c23d52212..37c204a519801 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -886,7 +886,7 @@ export class SavedObjectsClient { bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>; create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; delete: (type: string, id: string) => Promise<{}>; - find: (options: Pick) => Promise>; + find: (options: Pick) => Promise>; get: (type: string, id: string) => Promise>; update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; } diff --git a/src/core/server/bootstrap.ts b/src/core/server/bootstrap.ts index a0cf3d1602879..dff0c00a4625e 100644 --- a/src/core/server/bootstrap.ts +++ b/src/core/server/bootstrap.ts @@ -20,7 +20,6 @@ import chalk from 'chalk'; import { isMaster } from 'cluster'; import { CliArgs, Env, RawConfigService } from './config'; -import { LegacyObjectToConfigAdapter } from './legacy'; import { Root } from './root'; import { CriticalError } from './errors'; @@ -62,14 +61,10 @@ export async function bootstrap({ isDevClusterMaster: isMaster && cliArgs.dev && features.isClusterModeSupported, }); - const rawConfigService = new RawConfigService( - env.configs, - rawConfig => new LegacyObjectToConfigAdapter(applyConfigOverrides(rawConfig)) - ); - + const rawConfigService = new RawConfigService(env.configs, applyConfigOverrides); rawConfigService.loadConfig(); - const root = new Root(rawConfigService.getConfig$(), env, onRootShutdown); + const root = new Root(rawConfigService, env, onRootShutdown); process.on('SIGHUP', () => reloadLoggingConfig()); diff --git a/src/core/server/config/config_service.mock.ts b/src/core/server/config/config_service.mock.ts index e87869e92deeb..b05b13d9e2d51 100644 --- a/src/core/server/config/config_service.mock.ts +++ b/src/core/server/config/config_service.mock.ts @@ -34,6 +34,8 @@ const createConfigServiceMock = ({ getUnusedPaths: jest.fn(), isEnabledAtPath: jest.fn(), setSchema: jest.fn(), + addDeprecationProvider: jest.fn(), + validate: jest.fn(), }; mocked.atPath.mockReturnValue(new BehaviorSubject(atPath)); mocked.getConfig$.mockReturnValue(new BehaviorSubject(new ObjectToConfigAdapter(getConfig$))); diff --git a/src/core/server/config/config_service.test.mocks.ts b/src/core/server/config/config_service.test.mocks.ts index 8fa1ec997d625..1299c4c0b4eb1 100644 --- a/src/core/server/config/config_service.test.mocks.ts +++ b/src/core/server/config/config_service.test.mocks.ts @@ -19,3 +19,8 @@ export const mockPackage = new Proxy({ raw: {} as any }, { get: (obj, prop) => obj.raw[prop] }); jest.mock('../../../../package.json', () => mockPackage); + +export const mockApplyDeprecations = jest.fn((config, deprecations, log) => config); +jest.mock('./deprecation/apply_deprecations', () => ({ + applyDeprecations: mockApplyDeprecations, +})); diff --git a/src/core/server/config/config_service.test.ts b/src/core/server/config/config_service.test.ts index 131e1dd501792..773a444dea948 100644 --- a/src/core/server/config/config_service.test.ts +++ b/src/core/server/config/config_service.test.ts @@ -20,13 +20,14 @@ /* eslint-disable max-classes-per-file */ import { BehaviorSubject, Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; +import { first, take } from 'rxjs/operators'; -import { mockPackage } from './config_service.test.mocks'; +import { mockPackage, mockApplyDeprecations } from './config_service.test.mocks'; +import { rawConfigServiceMock } from './raw_config_service.mock'; import { schema } from '@kbn/config-schema'; -import { ConfigService, Env, ObjectToConfigAdapter } from '.'; +import { ConfigService, Env } from '.'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { getEnvOptions } from './__mocks__/env'; @@ -34,9 +35,12 @@ const emptyArgv = getEnvOptions(); const defaultEnv = new Env('/kibana', emptyArgv); const logger = loggingServiceMock.create(); +const getRawConfigProvider = (rawConfig: Record) => + rawConfigServiceMock.create({ rawConfig }); + test('returns config at path as observable', async () => { - const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'foo' })); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfig = getRawConfigProvider({ key: 'foo' }); + const configService = new ConfigService(rawConfig, defaultEnv, logger); const stringSchema = schema.string(); await configService.setSchema('key', stringSchema); @@ -48,21 +52,36 @@ test('returns config at path as observable', async () => { }); test('throws if config at path does not match schema', async () => { - const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 123 })); + const rawConfig = getRawConfigProvider({ key: 123 }); - const configService = new ConfigService(config$, defaultEnv, logger); + const configService = new ConfigService(rawConfig, defaultEnv, logger); + await configService.setSchema('key', schema.string()); - await expect( - configService.setSchema('key', schema.string()) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"[config validation of [key]]: expected value of type [string] but got [number]"` - ); + const valuesReceived: any[] = []; + await configService + .atPath('key') + .pipe(take(1)) + .subscribe( + value => { + valuesReceived.push(value); + }, + error => { + valuesReceived.push(error); + } + ); + + await expect(valuesReceived).toMatchInlineSnapshot(` + Array [ + [Error: [config validation of [key]]: expected value of type [string] but got [number]], + ] + `); }); test('re-validate config when updated', async () => { - const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' })); + const rawConfig$ = new BehaviorSubject>({ key: 'value' }); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig$ }); - const configService = new ConfigService(config$, defaultEnv, logger); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); configService.setSchema('key', schema.string()); const valuesReceived: any[] = []; @@ -75,19 +94,19 @@ test('re-validate config when updated', async () => { } ); - config$.next(new ObjectToConfigAdapter({ key: 123 })); + rawConfig$.next({ key: 123 }); await expect(valuesReceived).toMatchInlineSnapshot(` - Array [ - "value", - [Error: [config validation of [key]]: expected value of type [string] but got [number]], - ] + Array [ + "value", + [Error: [config validation of [key]]: expected value of type [string] but got [number]], + ] `); }); test("returns undefined if fetching optional config at a path that doesn't exist", async () => { - const config$ = new BehaviorSubject(new ObjectToConfigAdapter({})); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfig = getRawConfigProvider({}); + const configService = new ConfigService(rawConfig, defaultEnv, logger); const value$ = configService.optionalAtPath('unique-name'); const value = await value$.pipe(first()).toPromise(); @@ -96,8 +115,8 @@ test("returns undefined if fetching optional config at a path that doesn't exist }); test('returns observable config at optional path if it exists', async () => { - const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ value: 'bar' })); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfig = getRawConfigProvider({ value: 'bar' }); + const configService = new ConfigService(rawConfig, defaultEnv, logger); await configService.setSchema('value', schema.string()); const value$ = configService.optionalAtPath('value'); @@ -107,8 +126,10 @@ test('returns observable config at optional path if it exists', async () => { }); test("does not push new configs when reloading if config at path hasn't changed", async () => { - const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' })); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfig$ = new BehaviorSubject>({ key: 'value' }); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig$ }); + + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); await configService.setSchema('key', schema.string()); const valuesReceived: any[] = []; @@ -116,14 +137,16 @@ test("does not push new configs when reloading if config at path hasn't changed" valuesReceived.push(value); }); - config$.next(new ObjectToConfigAdapter({ key: 'value' })); + rawConfig$.next({ key: 'value' }); expect(valuesReceived).toEqual(['value']); }); test('pushes new config when reloading and config at path has changed', async () => { - const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' })); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfig$ = new BehaviorSubject>({ key: 'value' }); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig$ }); + + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); await configService.setSchema('key', schema.string()); const valuesReceived: any[] = []; @@ -131,14 +154,14 @@ test('pushes new config when reloading and config at path has changed', async () valuesReceived.push(value); }); - config$.next(new ObjectToConfigAdapter({ key: 'new value' })); + rawConfig$.next({ key: 'new value' }); expect(valuesReceived).toEqual(['value', 'new value']); }); test("throws error if 'schema' is not defined for a key", async () => { - const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' })); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: { key: 'value' } }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); const configs = configService.atPath('key'); @@ -148,8 +171,8 @@ test("throws error if 'schema' is not defined for a key", async () => { }); test("throws error if 'setSchema' called several times for the same key", async () => { - const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' })); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: { key: 'value' } }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); const addSchema = async () => await configService.setSchema('key', schema.string()); await addSchema(); await expect(addSchema()).rejects.toMatchInlineSnapshot( @@ -157,6 +180,32 @@ test("throws error if 'setSchema' called several times for the same key", async ); }); +test('flags schema paths as handled when registering a schema', async () => { + const rawConfigProvider = rawConfigServiceMock.create({ + rawConfig: { + service: { + string: 'str', + number: 42, + }, + }, + }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); + await configService.setSchema( + 'service', + schema.object({ + string: schema.string(), + number: schema.number(), + }) + ); + + expect(await configService.getUsedPaths()).toMatchInlineSnapshot(` + Array [ + "service.string", + "service.number", + ] + `); +}); + test('tracks unhandled paths', async () => { const initialConfig = { bar: { @@ -178,8 +227,8 @@ test('tracks unhandled paths', async () => { }, }; - const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig)); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); configService.atPath('foo'); configService.atPath(['bar', 'deep2']); @@ -201,8 +250,8 @@ test('correctly passes context', async () => { }; const env = new Env('/kibana', getEnvOptions()); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: { foo: {} } }); - const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ foo: {} })); const schemaDefinition = schema.object({ branchRef: schema.string({ defaultValue: schema.contextRef('branch'), @@ -219,7 +268,7 @@ test('correctly passes context', async () => { defaultValue: schema.contextRef('version'), }), }); - const configService = new ConfigService(config$, env, logger); + const configService = new ConfigService(rawConfigProvider, env, logger); await configService.setSchema('foo', schemaDefinition); const value$ = configService.atPath('foo'); @@ -234,8 +283,8 @@ test('handles enabled path, but only marks the enabled path as used', async () = }, }; - const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig)); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); const isEnabled = await configService.isEnabledAtPath('pid'); expect(isEnabled).toBe(true); @@ -252,8 +301,8 @@ test('handles enabled path when path is array', async () => { }, }; - const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig)); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); const isEnabled = await configService.isEnabledAtPath(['pid']); expect(isEnabled).toBe(true); @@ -270,8 +319,8 @@ test('handles disabled path and marks config as used', async () => { }, }; - const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig)); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); const isEnabled = await configService.isEnabledAtPath('pid'); expect(isEnabled).toBe(false); @@ -287,9 +336,9 @@ test('does not throw if schema does not define "enabled" schema', async () => { }, }; - const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig)); - const configService = new ConfigService(config$, defaultEnv, logger); - expect( + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); + await expect( configService.setSchema( 'pid', schema.object({ @@ -310,8 +359,8 @@ test('does not throw if schema does not define "enabled" schema', async () => { test('treats config as enabled if config path is not present in config', async () => { const initialConfig = {}; - const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig)); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); const isEnabled = await configService.isEnabledAtPath('pid'); expect(isEnabled).toBe(true); @@ -327,8 +376,8 @@ test('read "enabled" even if its schema is not present', async () => { }, }; - const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig)); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); const isEnabled = await configService.isEnabledAtPath('foo'); expect(isEnabled).toBe(true); @@ -337,8 +386,8 @@ test('read "enabled" even if its schema is not present', async () => { test('allows plugins to specify "enabled" flag via validation schema', async () => { const initialConfig = {}; - const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig)); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); await configService.setSchema( 'foo', @@ -361,3 +410,49 @@ test('allows plugins to specify "enabled" flag via validation schema', async () expect(await configService.isEnabledAtPath('baz')).toBe(true); }); + +test('does not throw during validation is every schema is valid', async () => { + const rawConfig = getRawConfigProvider({ stringKey: 'foo', numberKey: 42 }); + + const configService = new ConfigService(rawConfig, defaultEnv, logger); + await configService.setSchema('stringKey', schema.string()); + await configService.setSchema('numberKey', schema.number()); + + await expect(configService.validate()).resolves.toBeUndefined(); +}); + +test('throws during validation is any schema is invalid', async () => { + const rawConfig = getRawConfigProvider({ stringKey: 123, numberKey: 42 }); + + const configService = new ConfigService(rawConfig, defaultEnv, logger); + await configService.setSchema('stringKey', schema.string()); + await configService.setSchema('numberKey', schema.number()); + + await expect(configService.validate()).rejects.toThrowErrorMatchingInlineSnapshot( + `"[config validation of [stringKey]]: expected value of type [string] but got [number]"` + ); +}); + +test('logs deprecation warning during validation', async () => { + const rawConfig = getRawConfigProvider({}); + const configService = new ConfigService(rawConfig, defaultEnv, logger); + + mockApplyDeprecations.mockImplementationOnce((config, deprecations, log) => { + log('some deprecation message'); + log('another deprecation message'); + return config; + }); + + loggingServiceMock.clear(logger); + await configService.validate(); + expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "some deprecation message", + ], + Array [ + "another deprecation message", + ], + ] + `); +}); diff --git a/src/core/server/config/config_service.ts b/src/core/server/config/config_service.ts index c18a5b2000e01..61630f43bffb5 100644 --- a/src/core/server/config/config_service.ts +++ b/src/core/server/config/config_service.ts @@ -19,12 +19,20 @@ import { Type } from '@kbn/config-schema'; import { isEqual } from 'lodash'; -import { Observable } from 'rxjs'; -import { distinctUntilChanged, first, map } from 'rxjs/operators'; +import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; +import { distinctUntilChanged, first, map, shareReplay, take } from 'rxjs/operators'; import { Config, ConfigPath, Env } from '.'; import { Logger, LoggerFactory } from '../logging'; import { hasConfigPathIntersection } from './config'; +import { RawConfigurationProvider } from './raw_config_service'; +import { + applyDeprecations, + ConfigDeprecationWithContext, + ConfigDeprecationProvider, + configDeprecationFactory, +} from './deprecation'; +import { LegacyObjectToConfigAdapter } from '../legacy/config'; /** @internal */ export type IConfigService = PublicMethodsOf; @@ -32,6 +40,9 @@ export type IConfigService = PublicMethodsOf; /** @internal */ export class ConfigService { private readonly log: Logger; + private readonly deprecationLog: Logger; + + private readonly config$: Observable; /** * Whenever a config if read at a path, we mark that path as 'handled'. We can @@ -39,13 +50,23 @@ export class ConfigService { */ private readonly handledPaths: ConfigPath[] = []; private readonly schemas = new Map>(); + private readonly deprecations = new BehaviorSubject([]); constructor( - private readonly config$: Observable, + private readonly rawConfigProvider: RawConfigurationProvider, private readonly env: Env, logger: LoggerFactory ) { this.log = logger.get('config'); + this.deprecationLog = logger.get('config', 'deprecation'); + + this.config$ = combineLatest([this.rawConfigProvider.getConfig$(), this.deprecations]).pipe( + map(([rawConfig, deprecations]) => { + const migrated = applyDeprecations(rawConfig, deprecations); + return new LegacyObjectToConfigAdapter(migrated); + }), + shareReplay(1) + ); } /** @@ -58,10 +79,37 @@ export class ConfigService { } this.schemas.set(namespace, schema); + this.markAsHandled(path); + } - await this.validateConfig(path) - .pipe(first()) - .toPromise(); + /** + * Register a {@link ConfigDeprecationProvider} to be used when validating and migrating the configuration + */ + public addDeprecationProvider(path: ConfigPath, provider: ConfigDeprecationProvider) { + const flatPath = pathToString(path); + this.deprecations.next([ + ...this.deprecations.value, + ...provider(configDeprecationFactory).map(deprecation => ({ + deprecation, + path: flatPath, + })), + ]); + } + + /** + * Validate the whole configuration and log the deprecation warnings. + * + * This must be done after every schemas and deprecation providers have been registered. + */ + public async validate() { + const namespaces = [...this.schemas.keys()]; + for (let i = 0; i < namespaces.length; i++) { + await this.validateConfigAtPath(namespaces[i]) + .pipe(first()) + .toPromise(); + } + + await this.logDeprecation(); } /** @@ -79,7 +127,7 @@ export class ConfigService { * @param path - The path to the desired subset of the config. */ public atPath(path: ConfigPath) { - return this.validateConfig(path) as Observable; + return this.validateConfigAtPath(path) as Observable; } /** @@ -92,7 +140,7 @@ export class ConfigService { return this.getDistinctConfig(path).pipe( map(config => { if (config === undefined) return undefined; - return this.validate(path, config) as TSchema; + return this.validateAtPath(path, config) as TSchema; }) ); } @@ -148,7 +196,21 @@ export class ConfigService { return config.getFlattenedPaths().filter(path => isPathHandled(path, handledPaths)); } - private validate(path: ConfigPath, config: Record) { + private async logDeprecation() { + const rawConfig = await this.rawConfigProvider + .getConfig$() + .pipe(take(1)) + .toPromise(); + const deprecations = await this.deprecations.pipe(take(1)).toPromise(); + const deprecationMessages: string[] = []; + const logger = (msg: string) => deprecationMessages.push(msg); + applyDeprecations(rawConfig, deprecations, logger); + deprecationMessages.forEach(msg => { + this.deprecationLog.warn(msg); + }); + } + + private validateAtPath(path: ConfigPath, config: Record) { const namespace = pathToString(path); const schema = this.schemas.get(namespace); if (!schema) { @@ -165,8 +227,8 @@ export class ConfigService { ); } - private validateConfig(path: ConfigPath) { - return this.getDistinctConfig(path).pipe(map(config => this.validate(path, config))); + private validateConfigAtPath(path: ConfigPath) { + return this.getDistinctConfig(path).pipe(map(config => this.validateAtPath(path, config))); } private getDistinctConfig(path: ConfigPath) { diff --git a/src/core/server/config/deprecation/apply_deprecations.test.ts b/src/core/server/config/deprecation/apply_deprecations.test.ts new file mode 100644 index 0000000000000..25cae80d8b5cb --- /dev/null +++ b/src/core/server/config/deprecation/apply_deprecations.test.ts @@ -0,0 +1,85 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { applyDeprecations } from './apply_deprecations'; +import { ConfigDeprecation, ConfigDeprecationWithContext } from './types'; +import { configDeprecationFactory as deprecations } from './deprecation_factory'; + +const wrapHandler = ( + handler: ConfigDeprecation, + path: string = '' +): ConfigDeprecationWithContext => ({ + deprecation: handler, + path, +}); + +describe('applyDeprecations', () => { + it('calls all deprecations handlers once', () => { + const handlerA = jest.fn(); + const handlerB = jest.fn(); + const handlerC = jest.fn(); + applyDeprecations( + {}, + [handlerA, handlerB, handlerC].map(h => wrapHandler(h)) + ); + expect(handlerA).toHaveBeenCalledTimes(1); + expect(handlerB).toHaveBeenCalledTimes(1); + expect(handlerC).toHaveBeenCalledTimes(1); + }); + + it('calls handlers with correct arguments', () => { + const logger = () => undefined; + const initialConfig = { foo: 'bar', deprecated: 'deprecated' }; + const alteredConfig = { foo: 'bar' }; + + const handlerA = jest.fn().mockReturnValue(alteredConfig); + const handlerB = jest.fn().mockImplementation(conf => conf); + + applyDeprecations( + initialConfig, + [wrapHandler(handlerA, 'pathA'), wrapHandler(handlerB, 'pathB')], + logger + ); + + expect(handlerA).toHaveBeenCalledWith(initialConfig, 'pathA', logger); + expect(handlerB).toHaveBeenCalledWith(alteredConfig, 'pathB', logger); + }); + + it('returns the migrated config', () => { + const initialConfig = { foo: 'bar', deprecated: 'deprecated', renamed: 'renamed' }; + + const migrated = applyDeprecations(initialConfig, [ + wrapHandler(deprecations.unused('deprecated')), + wrapHandler(deprecations.rename('renamed', 'newname')), + ]); + + expect(migrated).toEqual({ foo: 'bar', newname: 'renamed' }); + }); + + it('does not alter the initial config', () => { + const initialConfig = { foo: 'bar', deprecated: 'deprecated' }; + + const migrated = applyDeprecations(initialConfig, [ + wrapHandler(deprecations.unused('deprecated')), + ]); + + expect(initialConfig).toEqual({ foo: 'bar', deprecated: 'deprecated' }); + expect(migrated).toEqual({ foo: 'bar' }); + }); +}); diff --git a/src/core/server/config/deprecation/apply_deprecations.ts b/src/core/server/config/deprecation/apply_deprecations.ts new file mode 100644 index 0000000000000..f7f95709ed846 --- /dev/null +++ b/src/core/server/config/deprecation/apply_deprecations.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { cloneDeep } from 'lodash'; +import { ConfigDeprecationWithContext, ConfigDeprecationLogger } from './types'; + +const noopLogger = (msg: string) => undefined; + +/** + * Applies deprecations on given configuration and logs any deprecation warning using provided logger. + * + * @internal + */ +export const applyDeprecations = ( + config: Record, + deprecations: ConfigDeprecationWithContext[], + logger: ConfigDeprecationLogger = noopLogger +) => { + let processed = cloneDeep(config); + deprecations.forEach(({ deprecation, path }) => { + processed = deprecation(processed, path, logger); + }); + return processed; +}; diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts new file mode 100644 index 0000000000000..b40dbdc1b6651 --- /dev/null +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -0,0 +1,211 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { coreDeprecationProvider } from './core_deprecations'; +import { configDeprecationFactory } from './deprecation_factory'; +import { applyDeprecations } from './apply_deprecations'; + +const initialEnv = { ...process.env }; + +const applyCoreDeprecations = (settings: Record = {}) => { + const deprecations = coreDeprecationProvider(configDeprecationFactory); + const deprecationMessages: string[] = []; + const migrated = applyDeprecations( + settings, + deprecations.map(deprecation => ({ + deprecation, + path: '', + })), + msg => deprecationMessages.push(msg) + ); + return { + messages: deprecationMessages, + migrated, + }; +}; + +describe('core deprecations', () => { + beforeEach(() => { + process.env = { ...initialEnv }; + }); + + describe('configPath', () => { + it('logs a warning if CONFIG_PATH environ variable is set', () => { + process.env.CONFIG_PATH = 'somepath'; + const { messages } = applyCoreDeprecations(); + expect(messages).toMatchInlineSnapshot(` + Array [ + "Environment variable CONFIG_PATH is deprecated. It has been replaced with KIBANA_PATH_CONF pointing to a config folder", + ] + `); + }); + + it('does not log a warning if CONFIG_PATH environ variable is unset', () => { + delete process.env.CONFIG_PATH; + const { messages } = applyCoreDeprecations(); + expect(messages).toHaveLength(0); + }); + }); + + describe('dataPath', () => { + it('logs a warning if DATA_PATH environ variable is set', () => { + process.env.DATA_PATH = 'somepath'; + const { messages } = applyCoreDeprecations(); + expect(messages).toMatchInlineSnapshot(` + Array [ + "Environment variable \\"DATA_PATH\\" will be removed. It has been replaced with kibana.yml setting \\"path.data\\"", + ] + `); + }); + + it('does not log a warning if DATA_PATH environ variable is unset', () => { + delete process.env.DATA_PATH; + const { messages } = applyCoreDeprecations(); + expect(messages).toHaveLength(0); + }); + }); + + describe('rewriteBasePath', () => { + it('logs a warning is server.basePath is set and server.rewriteBasePath is not', () => { + const { messages } = applyCoreDeprecations({ + server: { + basePath: 'foo', + }, + }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "You should set server.basePath along with server.rewriteBasePath. Starting in 7.0, Kibana will expect that all requests start with server.basePath rather than expecting you to rewrite the requests in your reverse proxy. Set server.rewriteBasePath to false to preserve the current behavior and silence this warning.", + ] + `); + }); + + it('does not log a warning if both server.basePath and server.rewriteBasePath are unset', () => { + const { messages } = applyCoreDeprecations({ + server: {}, + }); + expect(messages).toHaveLength(0); + }); + + it('does not log a warning if both server.basePath and server.rewriteBasePath are set', () => { + const { messages } = applyCoreDeprecations({ + server: { + basePath: 'foo', + rewriteBasePath: true, + }, + }); + expect(messages).toHaveLength(0); + }); + }); + + describe('cspRulesDeprecation', () => { + describe('with nonce source', () => { + it('logs a warning', () => { + const settings = { + csp: { + rules: [`script-src 'self' 'nonce-{nonce}'`], + }, + }; + const { messages } = applyCoreDeprecations(settings); + expect(messages).toMatchInlineSnapshot(` + Array [ + "csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in script-src", + ] + `); + }); + + it('replaces a nonce', () => { + expect( + applyCoreDeprecations({ csp: { rules: [`script-src 'nonce-{nonce}'`] } }).migrated.csp + .rules + ).toEqual([`script-src 'self'`]); + expect( + applyCoreDeprecations({ csp: { rules: [`script-src 'unsafe-eval' 'nonce-{nonce}'`] } }) + .migrated.csp.rules + ).toEqual([`script-src 'unsafe-eval' 'self'`]); + }); + + it('removes a quoted nonce', () => { + expect( + applyCoreDeprecations({ csp: { rules: [`script-src 'self' 'nonce-{nonce}'`] } }).migrated + .csp.rules + ).toEqual([`script-src 'self'`]); + expect( + applyCoreDeprecations({ csp: { rules: [`script-src 'nonce-{nonce}' 'self'`] } }).migrated + .csp.rules + ).toEqual([`script-src 'self'`]); + }); + + it('removes a non-quoted nonce', () => { + expect( + applyCoreDeprecations({ csp: { rules: [`script-src 'self' nonce-{nonce}`] } }).migrated + .csp.rules + ).toEqual([`script-src 'self'`]); + expect( + applyCoreDeprecations({ csp: { rules: [`script-src nonce-{nonce} 'self'`] } }).migrated + .csp.rules + ).toEqual([`script-src 'self'`]); + }); + + it('removes a strange nonce', () => { + expect( + applyCoreDeprecations({ csp: { rules: [`script-src 'self' blah-{nonce}-wow`] } }).migrated + .csp.rules + ).toEqual([`script-src 'self'`]); + }); + + it('removes multiple nonces', () => { + expect( + applyCoreDeprecations({ + csp: { + rules: [ + `script-src 'nonce-{nonce}' 'self' blah-{nonce}-wow`, + `style-src 'nonce-{nonce}' 'self'`, + ], + }, + }).migrated.csp.rules + ).toEqual([`script-src 'self'`, `style-src 'self'`]); + }); + }); + + describe('without self source', () => { + it('logs a warning', () => { + const { messages } = applyCoreDeprecations({ + csp: { rules: [`script-src 'unsafe-eval'`] }, + }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "csp.rules must contain the 'self' source. Automatically adding to script-src.", + ] + `); + }); + + it('adds self', () => { + expect( + applyCoreDeprecations({ csp: { rules: [`script-src 'unsafe-eval'`] } }).migrated.csp.rules + ).toEqual([`script-src 'unsafe-eval' 'self'`]); + }); + }); + + it('does not add self to other policies', () => { + expect( + applyCoreDeprecations({ csp: { rules: [`worker-src blob:`] } }).migrated.csp.rules + ).toEqual([`worker-src blob:`]); + }); + }); +}); diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts new file mode 100644 index 0000000000000..6a401ec6625a2 --- /dev/null +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -0,0 +1,114 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { has, get } from 'lodash'; +import { ConfigDeprecationProvider, ConfigDeprecation } from './types'; + +const configPathDeprecation: ConfigDeprecation = (settings, fromPath, log) => { + if (has(process.env, 'CONFIG_PATH')) { + log( + `Environment variable CONFIG_PATH is deprecated. It has been replaced with KIBANA_PATH_CONF pointing to a config folder` + ); + } + return settings; +}; + +const dataPathDeprecation: ConfigDeprecation = (settings, fromPath, log) => { + if (has(process.env, 'DATA_PATH')) { + log( + `Environment variable "DATA_PATH" will be removed. It has been replaced with kibana.yml setting "path.data"` + ); + } + return settings; +}; + +const rewriteBasePathDeprecation: ConfigDeprecation = (settings, fromPath, log) => { + if (has(settings, 'server.basePath') && !has(settings, 'server.rewriteBasePath')) { + log( + 'You should set server.basePath along with server.rewriteBasePath. Starting in 7.0, Kibana ' + + 'will expect that all requests start with server.basePath rather than expecting you to rewrite ' + + 'the requests in your reverse proxy. Set server.rewriteBasePath to false to preserve the ' + + 'current behavior and silence this warning.' + ); + } + return settings; +}; + +const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, log) => { + const NONCE_STRING = `{nonce}`; + // Policies that should include the 'self' source + const SELF_POLICIES = Object.freeze(['script-src', 'style-src']); + const SELF_STRING = `'self'`; + + const rules: string[] = get(settings, 'csp.rules'); + if (rules) { + const parsed = new Map( + rules.map(ruleStr => { + const parts = ruleStr.split(/\s+/); + return [parts[0], parts.slice(1)]; + }) + ); + + settings.csp.rules = [...parsed].map(([policy, sourceList]) => { + if (sourceList.find(source => source.includes(NONCE_STRING))) { + log(`csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in ${policy}`); + sourceList = sourceList.filter(source => !source.includes(NONCE_STRING)); + + // Add 'self' if not present + if (!sourceList.find(source => source.includes(SELF_STRING))) { + sourceList.push(SELF_STRING); + } + } + + if ( + SELF_POLICIES.includes(policy) && + !sourceList.find(source => source.includes(SELF_STRING)) + ) { + log(`csp.rules must contain the 'self' source. Automatically adding to ${policy}.`); + sourceList.push(SELF_STRING); + } + + return `${policy} ${sourceList.join(' ')}`.trim(); + }); + } + + return settings; +}; + +export const coreDeprecationProvider: ConfigDeprecationProvider = ({ + unusedFromRoot, + renameFromRoot, +}) => [ + unusedFromRoot('savedObjects.indexCheckTimeout'), + unusedFromRoot('server.xsrf.token'), + unusedFromRoot('uiSettings.enabled'), + renameFromRoot('optimize.lazy', 'optimize.watch'), + renameFromRoot('optimize.lazyPort', 'optimize.watchPort'), + renameFromRoot('optimize.lazyHost', 'optimize.watchHost'), + renameFromRoot('optimize.lazyPrebuild', 'optimize.watchPrebuild'), + renameFromRoot('optimize.lazyProxyTimeout', 'optimize.watchProxyTimeout'), + renameFromRoot('xpack.telemetry.enabled', 'telemetry.enabled'), + renameFromRoot('xpack.telemetry.config', 'telemetry.config'), + renameFromRoot('xpack.telemetry.banner', 'telemetry.banner'), + renameFromRoot('xpack.telemetry.url', 'telemetry.url'), + configPathDeprecation, + dataPathDeprecation, + rewriteBasePathDeprecation, + cspRulesDeprecation, +]; diff --git a/src/core/server/config/deprecation/deprecation_factory.test.ts b/src/core/server/config/deprecation/deprecation_factory.test.ts new file mode 100644 index 0000000000000..2595fdd923dd5 --- /dev/null +++ b/src/core/server/config/deprecation/deprecation_factory.test.ts @@ -0,0 +1,379 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ConfigDeprecationLogger } from './types'; +import { configDeprecationFactory } from './deprecation_factory'; + +describe('DeprecationFactory', () => { + const { rename, unused, renameFromRoot, unusedFromRoot } = configDeprecationFactory; + + let deprecationMessages: string[]; + const logger: ConfigDeprecationLogger = msg => deprecationMessages.push(msg); + + beforeEach(() => { + deprecationMessages = []; + }); + + describe('rename', () => { + it('moves the property to rename and logs a warning if old property exist and new one does not', () => { + const rawConfig = { + myplugin: { + deprecated: 'toberenamed', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const processed = rename('deprecated', 'renamed')(rawConfig, 'myplugin', logger); + expect(processed).toEqual({ + myplugin: { + renamed: 'toberenamed', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }); + expect(deprecationMessages).toMatchInlineSnapshot(` + Array [ + "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\"", + ] + `); + }); + it('does not alter config and does not log if old property is not present', () => { + const rawConfig = { + myplugin: { + new: 'new', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const processed = rename('deprecated', 'new')(rawConfig, 'myplugin', logger); + expect(processed).toEqual({ + myplugin: { + new: 'new', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }); + expect(deprecationMessages.length).toEqual(0); + }); + it('handles nested keys', () => { + const rawConfig = { + myplugin: { + oldsection: { + deprecated: 'toberenamed', + }, + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const processed = rename('oldsection.deprecated', 'newsection.renamed')( + rawConfig, + 'myplugin', + logger + ); + expect(processed).toEqual({ + myplugin: { + oldsection: {}, + newsection: { + renamed: 'toberenamed', + }, + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }); + expect(deprecationMessages).toMatchInlineSnapshot(` + Array [ + "\\"myplugin.oldsection.deprecated\\" is deprecated and has been replaced by \\"myplugin.newsection.renamed\\"", + ] + `); + }); + it('remove the old property but does not overrides the new one if they both exist, and logs a specific message', () => { + const rawConfig = { + myplugin: { + deprecated: 'deprecated', + renamed: 'renamed', + }, + }; + const processed = rename('deprecated', 'renamed')(rawConfig, 'myplugin', logger); + expect(processed).toEqual({ + myplugin: { + renamed: 'renamed', + }, + }); + expect(deprecationMessages).toMatchInlineSnapshot(` + Array [ + "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\". However both key are present, ignoring \\"myplugin.deprecated\\"", + ] + `); + }); + }); + + describe('renameFromRoot', () => { + it('moves the property from root and logs a warning if old property exist and new one does not', () => { + const rawConfig = { + myplugin: { + deprecated: 'toberenamed', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const processed = renameFromRoot('myplugin.deprecated', 'myplugin.renamed')( + rawConfig, + 'does-not-matter', + logger + ); + expect(processed).toEqual({ + myplugin: { + renamed: 'toberenamed', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }); + expect(deprecationMessages).toMatchInlineSnapshot(` + Array [ + "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\"", + ] + `); + }); + + it('can move a property to a different namespace', () => { + const rawConfig = { + oldplugin: { + deprecated: 'toberenamed', + valid: 'valid', + }, + newplugin: { + property: 'value', + }, + }; + const processed = renameFromRoot('oldplugin.deprecated', 'newplugin.renamed')( + rawConfig, + 'does-not-matter', + logger + ); + expect(processed).toEqual({ + oldplugin: { + valid: 'valid', + }, + newplugin: { + renamed: 'toberenamed', + property: 'value', + }, + }); + expect(deprecationMessages).toMatchInlineSnapshot(` + Array [ + "\\"oldplugin.deprecated\\" is deprecated and has been replaced by \\"newplugin.renamed\\"", + ] + `); + }); + + it('does not alter config and does not log if old property is not present', () => { + const rawConfig = { + myplugin: { + new: 'new', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const processed = renameFromRoot('myplugin.deprecated', 'myplugin.new')( + rawConfig, + 'does-not-matter', + logger + ); + expect(processed).toEqual({ + myplugin: { + new: 'new', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }); + expect(deprecationMessages.length).toEqual(0); + }); + + it('remove the old property but does not overrides the new one if they both exist, and logs a specific message', () => { + const rawConfig = { + myplugin: { + deprecated: 'deprecated', + renamed: 'renamed', + }, + }; + const processed = renameFromRoot('myplugin.deprecated', 'myplugin.renamed')( + rawConfig, + 'does-not-matter', + logger + ); + expect(processed).toEqual({ + myplugin: { + renamed: 'renamed', + }, + }); + expect(deprecationMessages).toMatchInlineSnapshot(` + Array [ + "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\". However both key are present, ignoring \\"myplugin.deprecated\\"", + ] + `); + }); + }); + + describe('unused', () => { + it('removes the unused property from the config and logs a warning is present', () => { + const rawConfig = { + myplugin: { + deprecated: 'deprecated', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const processed = unused('deprecated')(rawConfig, 'myplugin', logger); + expect(processed).toEqual({ + myplugin: { + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }); + expect(deprecationMessages).toMatchInlineSnapshot(` + Array [ + "myplugin.deprecated is deprecated and is no longer used", + ] + `); + }); + + it('handles deeply nested keys', () => { + const rawConfig = { + myplugin: { + section: { + deprecated: 'deprecated', + }, + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const processed = unused('section.deprecated')(rawConfig, 'myplugin', logger); + expect(processed).toEqual({ + myplugin: { + valid: 'valid', + section: {}, + }, + someOtherPlugin: { + property: 'value', + }, + }); + expect(deprecationMessages).toMatchInlineSnapshot(` + Array [ + "myplugin.section.deprecated is deprecated and is no longer used", + ] + `); + }); + + it('does not alter config and does not log if unused property is not present', () => { + const rawConfig = { + myplugin: { + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const processed = unused('deprecated')(rawConfig, 'myplugin', logger); + expect(processed).toEqual({ + myplugin: { + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }); + expect(deprecationMessages.length).toEqual(0); + }); + }); + + describe('unusedFromRoot', () => { + it('removes the unused property from the root config and logs a warning is present', () => { + const rawConfig = { + myplugin: { + deprecated: 'deprecated', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const processed = unusedFromRoot('myplugin.deprecated')(rawConfig, 'does-not-matter', logger); + expect(processed).toEqual({ + myplugin: { + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }); + expect(deprecationMessages).toMatchInlineSnapshot(` + Array [ + "myplugin.deprecated is deprecated and is no longer used", + ] + `); + }); + + it('does not alter config and does not log if unused property is not present', () => { + const rawConfig = { + myplugin: { + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const processed = unusedFromRoot('myplugin.deprecated')(rawConfig, 'does-not-matter', logger); + expect(processed).toEqual({ + myplugin: { + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }); + expect(deprecationMessages.length).toEqual(0); + }); + }); +}); diff --git a/src/core/server/config/deprecation/deprecation_factory.ts b/src/core/server/config/deprecation/deprecation_factory.ts new file mode 100644 index 0000000000000..6f7ed4c4e84cc --- /dev/null +++ b/src/core/server/config/deprecation/deprecation_factory.ts @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { get, set } from 'lodash'; +import { ConfigDeprecation, ConfigDeprecationLogger, ConfigDeprecationFactory } from './types'; +import { unset } from '../../../utils'; + +const _rename = ( + config: Record, + rootPath: string, + log: ConfigDeprecationLogger, + oldKey: string, + newKey: string +) => { + const fullOldPath = getPath(rootPath, oldKey); + const oldValue = get(config, fullOldPath); + if (oldValue === undefined) { + return config; + } + + unset(config, fullOldPath); + + const fullNewPath = getPath(rootPath, newKey); + const newValue = get(config, fullNewPath); + if (newValue === undefined) { + set(config, fullNewPath, oldValue); + log(`"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}"`); + } else { + log( + `"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}". However both key are present, ignoring "${fullOldPath}"` + ); + } + return config; +}; + +const _unused = ( + config: Record, + rootPath: string, + log: ConfigDeprecationLogger, + unusedKey: string +) => { + const fullPath = getPath(rootPath, unusedKey); + if (get(config, fullPath) === undefined) { + return config; + } + unset(config, fullPath); + log(`${fullPath} is deprecated and is no longer used`); + return config; +}; + +const rename = (oldKey: string, newKey: string): ConfigDeprecation => (config, rootPath, log) => + _rename(config, rootPath, log, oldKey, newKey); + +const renameFromRoot = (oldKey: string, newKey: string): ConfigDeprecation => ( + config, + rootPath, + log +) => _rename(config, '', log, oldKey, newKey); + +const unused = (unusedKey: string): ConfigDeprecation => (config, rootPath, log) => + _unused(config, rootPath, log, unusedKey); + +const unusedFromRoot = (unusedKey: string): ConfigDeprecation => (config, rootPath, log) => + _unused(config, '', log, unusedKey); + +const getPath = (rootPath: string, subPath: string) => + rootPath !== '' ? `${rootPath}.${subPath}` : subPath; + +/** + * The actual platform implementation of {@link ConfigDeprecationFactory} + * + * @internal + */ +export const configDeprecationFactory: ConfigDeprecationFactory = { + rename, + renameFromRoot, + unused, + unusedFromRoot, +}; diff --git a/src/legacy/server/config/__tests__/fixtures/run_kbn_server_startup.js b/src/core/server/config/deprecation/index.ts similarity index 63% rename from src/legacy/server/config/__tests__/fixtures/run_kbn_server_startup.js rename to src/core/server/config/deprecation/index.ts index 3eaf18be609d4..f79338665166b 100644 --- a/src/legacy/server/config/__tests__/fixtures/run_kbn_server_startup.js +++ b/src/core/server/config/deprecation/index.ts @@ -17,19 +17,13 @@ * under the License. */ -import { createRoot } from '../../../../../test_utils/kbn_server'; - -(async function run() { - const root = createRoot(JSON.parse(process.env.CREATE_SERVER_OPTS)); - - // We just need the server to run through startup so that it will - // log the deprecation messages. Once it has started up we close it - // to allow the process to exit naturally - try { - await root.setup(); - await root.start(); - } finally { - await root.shutdown(); - } - -}()); +export { + ConfigDeprecation, + ConfigDeprecationWithContext, + ConfigDeprecationLogger, + ConfigDeprecationFactory, + ConfigDeprecationProvider, +} from './types'; +export { configDeprecationFactory } from './deprecation_factory'; +export { coreDeprecationProvider } from './core_deprecations'; +export { applyDeprecations } from './apply_deprecations'; diff --git a/src/core/server/config/deprecation/types.ts b/src/core/server/config/deprecation/types.ts new file mode 100644 index 0000000000000..19fba7800c919 --- /dev/null +++ b/src/core/server/config/deprecation/types.ts @@ -0,0 +1,141 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Logger interface used when invoking a {@link ConfigDeprecation} + * + * @public + */ +export type ConfigDeprecationLogger = (message: string) => void; + +/** + * Configuration deprecation returned from {@link ConfigDeprecationProvider} that handles a single deprecation from the configuration. + * + * @remarks + * This should only be manually implemented if {@link ConfigDeprecationFactory} does not provide the proper helpers for a specific + * deprecation need. + * + * @public + */ +export type ConfigDeprecation = ( + config: Record, + fromPath: string, + logger: ConfigDeprecationLogger +) => Record; + +/** + * A provider that should returns a list of {@link ConfigDeprecation}. + * + * See {@link ConfigDeprecationFactory} for more usage examples. + * + * @example + * ```typescript + * const provider: ConfigDeprecationProvider = ({ rename, unused }) => [ + * rename('oldKey', 'newKey'), + * unused('deprecatedKey'), + * myCustomDeprecation, + * ] + * ``` + * + * @public + */ +export type ConfigDeprecationProvider = (factory: ConfigDeprecationFactory) => ConfigDeprecation[]; + +/** + * Provides helpers to generates the most commonly used {@link ConfigDeprecation} + * when invoking a {@link ConfigDeprecationProvider}. + * + * See methods documentation for more detailed examples. + * + * @example + * ```typescript + * const provider: ConfigDeprecationProvider = ({ rename, unused }) => [ + * rename('oldKey', 'newKey'), + * unused('deprecatedKey'), + * ] + * ``` + * + * @public + */ +export interface ConfigDeprecationFactory { + /** + * Rename a configuration property from inside a plugin's configuration path. + * Will log a deprecation warning if the oldKey was found and deprecation applied. + * + * @example + * Rename 'myplugin.oldKey' to 'myplugin.newKey' + * ```typescript + * const provider: ConfigDeprecationProvider = ({ rename }) => [ + * rename('oldKey', 'newKey'), + * ] + * ``` + */ + rename(oldKey: string, newKey: string): ConfigDeprecation; + /** + * Rename a configuration property from the root configuration. + * Will log a deprecation warning if the oldKey was found and deprecation applied. + * + * This should be only used when renaming properties from different configuration's path. + * To rename properties from inside a plugin's configuration, use 'rename' instead. + * + * @example + * Rename 'oldplugin.key' to 'newplugin.key' + * ```typescript + * const provider: ConfigDeprecationProvider = ({ renameFromRoot }) => [ + * renameFromRoot('oldplugin.key', 'newplugin.key'), + * ] + * ``` + */ + renameFromRoot(oldKey: string, newKey: string): ConfigDeprecation; + /** + * Remove a configuration property from inside a plugin's configuration path. + * Will log a deprecation warning if the unused key was found and deprecation applied. + * + * @example + * Flags 'myplugin.deprecatedKey' as unused + * ```typescript + * const provider: ConfigDeprecationProvider = ({ unused }) => [ + * unused('deprecatedKey'), + * ] + * ``` + */ + unused(unusedKey: string): ConfigDeprecation; + /** + * Remove a configuration property from the root configuration. + * Will log a deprecation warning if the unused key was found and deprecation applied. + * + * This should be only used when removing properties from outside of a plugin's configuration. + * To remove properties from inside a plugin's configuration, use 'unused' instead. + * + * @example + * Flags 'somepath.deprecatedProperty' as unused + * ```typescript + * const provider: ConfigDeprecationProvider = ({ unusedFromRoot }) => [ + * unusedFromRoot('somepath.deprecatedProperty'), + * ] + * ``` + */ + unusedFromRoot(unusedKey: string): ConfigDeprecation; +} + +/** @internal */ +export interface ConfigDeprecationWithContext { + deprecation: ConfigDeprecation; + path: string; +} diff --git a/src/core/server/config/index.ts b/src/core/server/config/index.ts index 491a24b2ab3d6..04dc402d35b22 100644 --- a/src/core/server/config/index.ts +++ b/src/core/server/config/index.ts @@ -18,9 +18,16 @@ */ export { ConfigService, IConfigService } from './config_service'; -export { RawConfigService } from './raw_config_service'; +export { RawConfigService, RawConfigurationProvider } from './raw_config_service'; export { Config, ConfigPath, isConfigPath, hasConfigPathIntersection } from './config'; export { ObjectToConfigAdapter } from './object_to_config_adapter'; export { CliArgs, Env } from './env'; +export { + ConfigDeprecation, + ConfigDeprecationLogger, + ConfigDeprecationProvider, + ConfigDeprecationFactory, + coreDeprecationProvider, +} from './deprecation'; export { EnvironmentMode, PackageInfo } from './types'; diff --git a/src/legacy/server/config/deprecation_warnings.js b/src/core/server/config/integration_tests/config_deprecation.test.mocks.ts similarity index 70% rename from src/legacy/server/config/deprecation_warnings.js rename to src/core/server/config/integration_tests/config_deprecation.test.mocks.ts index 06cd3ba7cf037..58b2da926b7c3 100644 --- a/src/legacy/server/config/deprecation_warnings.js +++ b/src/core/server/config/integration_tests/config_deprecation.test.mocks.ts @@ -17,10 +17,9 @@ * under the License. */ -import { transformDeprecations } from './transform_deprecations'; - -export function configDeprecationWarningsMixin(kbnServer, server) { - transformDeprecations(kbnServer.settings, (message) => { - server.log(['warning', 'config', 'deprecation'], message); - }); -} +import { loggingServiceMock } from '../../logging/logging_service.mock'; +export const mockLoggingService = loggingServiceMock.create(); +mockLoggingService.asLoggerFactory.mockImplementation(() => mockLoggingService); +jest.doMock('../../logging/logging_service', () => ({ + LoggingService: jest.fn(() => mockLoggingService), +})); diff --git a/src/core/server/config/integration_tests/config_deprecation.test.ts b/src/core/server/config/integration_tests/config_deprecation.test.ts new file mode 100644 index 0000000000000..e85f8567bfc68 --- /dev/null +++ b/src/core/server/config/integration_tests/config_deprecation.test.ts @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { mockLoggingService } from './config_deprecation.test.mocks'; +import { loggingServiceMock } from '../../logging/logging_service.mock'; +import * as kbnTestServer from '../../../../test_utils/kbn_server'; + +describe('configuration deprecations', () => { + let root: ReturnType; + + afterEach(async () => { + if (root) { + await root.shutdown(); + } + }); + + it('should not log deprecation warnings for default configuration', async () => { + root = kbnTestServer.createRoot(); + + await root.setup(); + + const logs = loggingServiceMock.collect(mockLoggingService); + expect(logs.warn).toMatchInlineSnapshot(`Array []`); + }); + + it('should log deprecation warnings for core deprecations', async () => { + root = kbnTestServer.createRoot({ + optimize: { + lazy: true, + lazyPort: 9090, + }, + }); + + await root.setup(); + + const logs = loggingServiceMock.collect(mockLoggingService); + expect(logs.warn).toMatchInlineSnapshot(` + Array [ + Array [ + "\\"optimize.lazy\\" is deprecated and has been replaced by \\"optimize.watch\\"", + ], + Array [ + "\\"optimize.lazyPort\\" is deprecated and has been replaced by \\"optimize.watchPort\\"", + ], + ] + `); + }); +}); diff --git a/src/core/server/config/raw_config_service.mock.ts b/src/core/server/config/raw_config_service.mock.ts new file mode 100644 index 0000000000000..fdcb17395aaad --- /dev/null +++ b/src/core/server/config/raw_config_service.mock.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RawConfigService } from './raw_config_service'; +import { Observable, of } from 'rxjs'; + +const createRawConfigServiceMock = ({ + rawConfig = {}, + rawConfig$ = undefined, +}: { rawConfig?: Record; rawConfig$?: Observable> } = {}) => { + const mocked: jest.Mocked> = { + loadConfig: jest.fn(), + stop: jest.fn(), + reloadConfig: jest.fn(), + getConfig$: jest.fn().mockReturnValue(rawConfig$ || of(rawConfig)), + }; + + return mocked; +}; + +export const rawConfigServiceMock = { + create: createRawConfigServiceMock, +}; diff --git a/src/core/server/config/raw_config_service.test.ts b/src/core/server/config/raw_config_service.test.ts index 361cef0d042ea..f02c31d4659ca 100644 --- a/src/core/server/config/raw_config_service.test.ts +++ b/src/core/server/config/raw_config_service.test.ts @@ -88,8 +88,8 @@ test('returns config at path as observable', async () => { .pipe(first()) .toPromise(); - expect(exampleConfig.get('key')).toEqual('value'); - expect(exampleConfig.getFlattenedPaths()).toEqual(['key']); + expect(exampleConfig.key).toEqual('value'); + expect(Object.keys(exampleConfig)).toEqual(['key']); }); test("pushes new configs when reloading even if config at path hasn't changed", async () => { @@ -110,19 +110,15 @@ test("pushes new configs when reloading even if config at path hasn't changed", configService.reloadConfig(); expect(valuesReceived).toMatchInlineSnapshot(` -Array [ - ObjectToConfigAdapter { - "rawConfig": Object { - "key": "value", - }, - }, - ObjectToConfigAdapter { - "rawConfig": Object { - "key": "value", - }, - }, -] -`); + Array [ + Object { + "key": "value", + }, + Object { + "key": "value", + }, + ] + `); }); test('pushes new config when reloading and config at path has changed', async () => { @@ -143,10 +139,10 @@ test('pushes new config when reloading and config at path has changed', async () configService.reloadConfig(); expect(valuesReceived).toHaveLength(2); - expect(valuesReceived[0].get('key')).toEqual('value'); - expect(valuesReceived[0].getFlattenedPaths()).toEqual(['key']); - expect(valuesReceived[1].get('key')).toEqual('new value'); - expect(valuesReceived[1].getFlattenedPaths()).toEqual(['key']); + expect(valuesReceived[0].key).toEqual('value'); + expect(Object.keys(valuesReceived[0])).toEqual(['key']); + expect(valuesReceived[1].key).toEqual('new value'); + expect(Object.keys(valuesReceived[1])).toEqual(['key']); }); test('completes config observables when stopped', done => { diff --git a/src/core/server/config/raw_config_service.ts b/src/core/server/config/raw_config_service.ts index b10137fb72f6a..728d793f494a9 100644 --- a/src/core/server/config/raw_config_service.ts +++ b/src/core/server/config/raw_config_service.ts @@ -22,10 +22,12 @@ import { Observable, ReplaySubject } from 'rxjs'; import { map } from 'rxjs/operators'; import typeDetect from 'type-detect'; -import { Config } from './config'; -import { ObjectToConfigAdapter } from './object_to_config_adapter'; import { getConfigFromFiles } from './read_config'; +type RawConfigAdapter = (rawConfig: Record) => Record; + +export type RawConfigurationProvider = Pick; + /** @internal */ export class RawConfigService { /** @@ -35,12 +37,11 @@ export class RawConfigService { */ private readonly rawConfigFromFile$: ReplaySubject> = new ReplaySubject(1); - private readonly config$: Observable; + private readonly config$: Observable>; constructor( public readonly configFiles: readonly string[], - configAdapter: (rawConfig: Record) => Config = rawConfig => - new ObjectToConfigAdapter(rawConfig) + configAdapter: RawConfigAdapter = rawConfig => rawConfig ) { this.config$ = this.rawConfigFromFile$.pipe( map(rawConfig => { diff --git a/src/core/server/http/http_service.test.ts b/src/core/server/http/http_service.test.ts index eee8094417260..a2546709a318c 100644 --- a/src/core/server/http/http_service.test.ts +++ b/src/core/server/http/http_service.test.ts @@ -24,7 +24,7 @@ import { BehaviorSubject } from 'rxjs'; import { HttpService } from '.'; import { HttpConfigType, config } from './http_config'; import { httpServerMock } from './http_server.mocks'; -import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../config'; +import { ConfigService, Env } from '../config'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { contextServiceMock } from '../context/context_service.mock'; import { getEnvOptions } from '../config/__mocks__/env'; @@ -35,11 +35,12 @@ const coreId = Symbol(); const createConfigService = (value: Partial = {}) => { const configService = new ConfigService( - new BehaviorSubject( - new ObjectToConfigAdapter({ - server: value, - }) - ), + { + getConfig$: () => + new BehaviorSubject({ + server: value, + }), + }, env, logger ); diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 51444a76f1737..c304958f78bb7 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -50,7 +50,16 @@ import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; export { bootstrap } from './bootstrap'; export { Capabilities, CapabilitiesProvider, CapabilitiesSwitcher } from './capabilities'; -export { ConfigPath, ConfigService, EnvironmentMode, PackageInfo } from './config'; +export { + ConfigPath, + ConfigService, + ConfigDeprecation, + ConfigDeprecationProvider, + ConfigDeprecationLogger, + ConfigDeprecationFactory, + EnvironmentMode, + PackageInfo, +} from './config'; export { IContextContainer, IContextProvider, diff --git a/src/core/server/legacy/config/ensure_valid_configuration.test.ts b/src/core/server/legacy/config/ensure_valid_configuration.test.ts index 2997a9c8e7aff..d8917b46eba62 100644 --- a/src/core/server/legacy/config/ensure_valid_configuration.test.ts +++ b/src/core/server/legacy/config/ensure_valid_configuration.test.ts @@ -50,7 +50,7 @@ describe('ensureValidConfiguration', () => { coreHandledConfigPaths: ['core', 'elastic'], pluginSpecs: 'pluginSpecs', disabledPluginSpecs: 'disabledPluginSpecs', - inputSettings: 'settings', + settings: 'settings', legacyConfig: 'pluginExtendedConfig', }); }); diff --git a/src/core/server/legacy/config/ensure_valid_configuration.ts b/src/core/server/legacy/config/ensure_valid_configuration.ts index 8c76d45887761..026683a7b7cb0 100644 --- a/src/core/server/legacy/config/ensure_valid_configuration.ts +++ b/src/core/server/legacy/config/ensure_valid_configuration.ts @@ -30,7 +30,7 @@ export async function ensureValidConfiguration( coreHandledConfigPaths: await configService.getUsedPaths(), pluginSpecs, disabledPluginSpecs, - inputSettings: settings, + settings, legacyConfig: pluginExtendedConfig, }); diff --git a/src/core/server/legacy/config/get_unused_config_keys.test.ts b/src/core/server/legacy/config/get_unused_config_keys.test.ts index 7b6be5368e769..bf011fa01a342 100644 --- a/src/core/server/legacy/config/get_unused_config_keys.test.ts +++ b/src/core/server/legacy/config/get_unused_config_keys.test.ts @@ -20,17 +20,10 @@ import { LegacyPluginSpec } from '../plugins/find_legacy_plugin_specs'; import { LegacyConfig } from './types'; import { getUnusedConfigKeys } from './get_unused_config_keys'; -// @ts-ignore -import { transformDeprecations } from '../../../../legacy/server/config/transform_deprecations'; - -jest.mock('../../../../legacy/server/config/transform_deprecations', () => ({ - transformDeprecations: jest.fn().mockImplementation(s => s), -})); describe('getUnusedConfigKeys', () => { beforeEach(() => { jest.resetAllMocks(); - transformDeprecations.mockImplementation((s: any) => s); }); const getConfig = (values: Record = {}): LegacyConfig => @@ -45,7 +38,7 @@ describe('getUnusedConfigKeys', () => { coreHandledConfigPaths: [], pluginSpecs: [], disabledPluginSpecs: [], - inputSettings: {}, + settings: {}, legacyConfig: getConfig(), }) ).toEqual([]); @@ -57,7 +50,7 @@ describe('getUnusedConfigKeys', () => { coreHandledConfigPaths: [], pluginSpecs: [], disabledPluginSpecs: [], - inputSettings: { + settings: { presentInBoth: true, alsoInBoth: 'someValue', }, @@ -75,7 +68,7 @@ describe('getUnusedConfigKeys', () => { coreHandledConfigPaths: [], pluginSpecs: [], disabledPluginSpecs: [], - inputSettings: { + settings: { presentInBoth: true, }, legacyConfig: getConfig({ @@ -92,7 +85,7 @@ describe('getUnusedConfigKeys', () => { coreHandledConfigPaths: [], pluginSpecs: [], disabledPluginSpecs: [], - inputSettings: { + settings: { presentInBoth: true, onlyInSetting: 'value', }, @@ -109,7 +102,7 @@ describe('getUnusedConfigKeys', () => { coreHandledConfigPaths: [], pluginSpecs: [], disabledPluginSpecs: [], - inputSettings: { + settings: { elasticsearch: { username: 'foo', password: 'bar', @@ -131,7 +124,7 @@ describe('getUnusedConfigKeys', () => { coreHandledConfigPaths: [], pluginSpecs: [], disabledPluginSpecs: [], - inputSettings: { + settings: { env: 'development', }, legacyConfig: getConfig({ @@ -149,7 +142,7 @@ describe('getUnusedConfigKeys', () => { coreHandledConfigPaths: [], pluginSpecs: [], disabledPluginSpecs: [], - inputSettings: { + settings: { prop: ['a', 'b', 'c'], }, legacyConfig: getConfig({ @@ -171,7 +164,7 @@ describe('getUnusedConfigKeys', () => { getConfigPrefix: () => 'foo.bar', } as unknown) as LegacyPluginSpec, ], - inputSettings: { + settings: { foo: { bar: { unused: true, @@ -194,7 +187,7 @@ describe('getUnusedConfigKeys', () => { coreHandledConfigPaths: ['core', 'foo.bar'], pluginSpecs: [], disabledPluginSpecs: [], - inputSettings: { + settings: { core: { prop: 'value', }, @@ -209,46 +202,6 @@ describe('getUnusedConfigKeys', () => { }); describe('using deprecation', () => { - it('calls transformDeprecations with the settings', async () => { - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - pluginSpecs: [], - disabledPluginSpecs: [], - inputSettings: { - prop: 'settings', - }, - legacyConfig: getConfig({ - prop: 'config', - }), - }); - expect(transformDeprecations).toHaveBeenCalledTimes(1); - expect(transformDeprecations).toHaveBeenCalledWith({ - prop: 'settings', - }); - }); - - it('uses the transformed settings', async () => { - transformDeprecations.mockImplementation((settings: Record) => { - delete settings.deprecated; - settings.updated = 'new value'; - return settings; - }); - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - pluginSpecs: [], - disabledPluginSpecs: [], - inputSettings: { - onlyInSettings: 'bar', - deprecated: 'value', - }, - legacyConfig: getConfig({ - updated: 'config', - }), - }) - ).toEqual(['onlyInSettings']); - }); - it('should use the plugin deprecations provider', async () => { expect( await getUnusedConfigKeys({ @@ -262,7 +215,7 @@ describe('getUnusedConfigKeys', () => { } as unknown) as LegacyPluginSpec, ], disabledPluginSpecs: [], - inputSettings: { + settings: { foo: { foo: 'dolly', foo1: 'bar', diff --git a/src/core/server/legacy/config/get_unused_config_keys.ts b/src/core/server/legacy/config/get_unused_config_keys.ts index 22e7c50c0bc25..73cc7d8c50474 100644 --- a/src/core/server/legacy/config/get_unused_config_keys.ts +++ b/src/core/server/legacy/config/get_unused_config_keys.ts @@ -19,8 +19,6 @@ import { difference, get, set } from 'lodash'; // @ts-ignore -import { transformDeprecations } from '../../../../legacy/server/config/transform_deprecations'; -// @ts-ignore import { getTransform } from '../../../../legacy/deprecation/index'; import { unset, getFlattenedObject } from '../../../../legacy/utils'; import { hasConfigPathIntersection } from '../../config'; @@ -33,18 +31,15 @@ export async function getUnusedConfigKeys({ coreHandledConfigPaths, pluginSpecs, disabledPluginSpecs, - inputSettings, + settings, legacyConfig, }: { coreHandledConfigPaths: string[]; pluginSpecs: LegacyPluginSpec[]; disabledPluginSpecs: LegacyPluginSpec[]; - inputSettings: Record; + settings: Record; legacyConfig: LegacyConfig; }) { - // transform deprecated core settings - const settings = transformDeprecations(inputSettings); - // transform deprecated plugin settings for (let i = 0; i < pluginSpecs.length; i++) { const spec = pluginSpecs[i]; diff --git a/src/core/server/legacy/config/index.ts b/src/core/server/legacy/config/index.ts index a837b8639939e..c3f308fd6d903 100644 --- a/src/core/server/legacy/config/index.ts +++ b/src/core/server/legacy/config/index.ts @@ -19,4 +19,10 @@ export { ensureValidConfiguration } from './ensure_valid_configuration'; export { LegacyObjectToConfigAdapter } from './legacy_object_to_config_adapter'; -export { LegacyConfig } from './types'; +export { convertLegacyDeprecationProvider } from './legacy_deprecation_adapters'; +export { + LegacyConfig, + LegacyConfigDeprecation, + LegacyConfigDeprecationFactory, + LegacyConfigDeprecationProvider, +} from './types'; diff --git a/src/core/server/legacy/config/legacy_deprecation_adapters.test.ts b/src/core/server/legacy/config/legacy_deprecation_adapters.test.ts new file mode 100644 index 0000000000000..144e057c118f7 --- /dev/null +++ b/src/core/server/legacy/config/legacy_deprecation_adapters.test.ts @@ -0,0 +1,106 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { convertLegacyDeprecationProvider } from './legacy_deprecation_adapters'; +import { LegacyConfigDeprecationProvider } from './types'; +import { ConfigDeprecation } from '../../config'; +import { configDeprecationFactory } from '../../config/deprecation/deprecation_factory'; +import { applyDeprecations } from '../../config/deprecation/apply_deprecations'; + +jest.spyOn(configDeprecationFactory, 'unusedFromRoot'); +jest.spyOn(configDeprecationFactory, 'renameFromRoot'); + +const executeHandlers = (handlers: ConfigDeprecation[]) => { + handlers.forEach(handler => { + handler({}, '', () => null); + }); +}; + +describe('convertLegacyDeprecationProvider', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns the same number of handlers', async () => { + const legacyProvider: LegacyConfigDeprecationProvider = ({ rename, unused }) => [ + rename('a', 'b'), + unused('c'), + unused('d'), + ]; + + const migrated = await convertLegacyDeprecationProvider(legacyProvider); + const handlers = migrated(configDeprecationFactory); + expect(handlers).toHaveLength(3); + }); + + it('invokes the factory "unusedFromRoot" when using legacy "unused"', async () => { + const legacyProvider: LegacyConfigDeprecationProvider = ({ rename, unused }) => [ + rename('a', 'b'), + unused('c'), + unused('d'), + ]; + + const migrated = await convertLegacyDeprecationProvider(legacyProvider); + const handlers = migrated(configDeprecationFactory); + executeHandlers(handlers); + + expect(configDeprecationFactory.unusedFromRoot).toHaveBeenCalledTimes(2); + expect(configDeprecationFactory.unusedFromRoot).toHaveBeenCalledWith('c'); + expect(configDeprecationFactory.unusedFromRoot).toHaveBeenCalledWith('d'); + }); + + it('invokes the factory "renameFromRoot" when using legacy "rename"', async () => { + const legacyProvider: LegacyConfigDeprecationProvider = ({ rename, unused }) => [ + rename('a', 'b'), + unused('c'), + rename('d', 'e'), + ]; + + const migrated = await convertLegacyDeprecationProvider(legacyProvider); + const handlers = migrated(configDeprecationFactory); + executeHandlers(handlers); + + expect(configDeprecationFactory.renameFromRoot).toHaveBeenCalledTimes(2); + expect(configDeprecationFactory.renameFromRoot).toHaveBeenCalledWith('a', 'b'); + expect(configDeprecationFactory.renameFromRoot).toHaveBeenCalledWith('d', 'e'); + }); + + it('properly works in a real use case', async () => { + const legacyProvider: LegacyConfigDeprecationProvider = ({ rename, unused }) => [ + rename('old', 'new'), + unused('unused'), + unused('notpresent'), + ]; + + const convertedProvider = await convertLegacyDeprecationProvider(legacyProvider); + const handlers = convertedProvider(configDeprecationFactory); + + const rawConfig = { + old: 'oldvalue', + unused: 'unused', + goodValue: 'good', + }; + + const migrated = applyDeprecations( + rawConfig, + handlers.map(handler => ({ deprecation: handler, path: '' })) + ); + expect(migrated).toEqual({ new: 'oldvalue', goodValue: 'good' }); + }); +}); diff --git a/src/core/server/legacy/config/legacy_deprecation_adapters.ts b/src/core/server/legacy/config/legacy_deprecation_adapters.ts new file mode 100644 index 0000000000000..b0e3bc37e1510 --- /dev/null +++ b/src/core/server/legacy/config/legacy_deprecation_adapters.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ConfigDeprecation, ConfigDeprecationProvider } from '../../config/deprecation'; +import { LegacyConfigDeprecation, LegacyConfigDeprecationProvider } from './index'; +import { configDeprecationFactory } from '../../config/deprecation/deprecation_factory'; + +const convertLegacyDeprecation = ( + legacyDeprecation: LegacyConfigDeprecation +): ConfigDeprecation => (config, fromPath, logger) => { + legacyDeprecation(config, logger); + return config; +}; + +const legacyUnused = (unusedKey: string): LegacyConfigDeprecation => (settings, log) => { + const deprecation = configDeprecationFactory.unusedFromRoot(unusedKey); + deprecation(settings, '', log); +}; + +const legacyRename = (oldKey: string, newKey: string): LegacyConfigDeprecation => ( + settings, + log +) => { + const deprecation = configDeprecationFactory.renameFromRoot(oldKey, newKey); + deprecation(settings, '', log); +}; + +/** + * Async deprecation provider converter for legacy deprecation implementation + * + * @internal + */ +export const convertLegacyDeprecationProvider = async ( + legacyProvider: LegacyConfigDeprecationProvider +): Promise => { + const legacyDeprecations = await legacyProvider({ + rename: legacyRename, + unused: legacyUnused, + }); + return () => legacyDeprecations.map(convertLegacyDeprecation); +}; diff --git a/src/core/server/legacy/config/legacy_object_to_config_adapter.ts b/src/core/server/legacy/config/legacy_object_to_config_adapter.ts index 8035596bb6072..75e1813f8c1f6 100644 --- a/src/core/server/legacy/config/legacy_object_to_config_adapter.ts +++ b/src/core/server/legacy/config/legacy_object_to_config_adapter.ts @@ -17,7 +17,8 @@ * under the License. */ -import { ConfigPath, ObjectToConfigAdapter } from '../../config'; +import { ConfigPath } from '../../config'; +import { ObjectToConfigAdapter } from '../../config/object_to_config_adapter'; /** * Represents logging config supported by the legacy platform. diff --git a/src/core/server/legacy/config/types.ts b/src/core/server/legacy/config/types.ts index cc4a6ac11fee4..24869e361c39c 100644 --- a/src/core/server/legacy/config/types.ts +++ b/src/core/server/legacy/config/types.ts @@ -26,3 +26,33 @@ export interface LegacyConfig { get(key?: string): T; has(key: string): boolean; } + +/** + * Representation of a legacy configuration deprecation factory used for + * legacy plugin deprecations. + * + * @internal + */ +export interface LegacyConfigDeprecationFactory { + rename(oldKey: string, newKey: string): LegacyConfigDeprecation; + unused(unusedKey: string): LegacyConfigDeprecation; +} + +/** + * Representation of a legacy configuration deprecation. + * + * @internal + */ +export type LegacyConfigDeprecation = ( + settings: Record, + log: (msg: string) => void +) => void; + +/** + * Representation of a legacy configuration deprecation provider. + * + * @internal + */ +export type LegacyConfigDeprecationProvider = ( + factory: LegacyConfigDeprecationFactory +) => LegacyConfigDeprecation[] | Promise; diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index 73b7ced60ee49..d4360c577d24c 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -21,13 +21,9 @@ import { BehaviorSubject, throwError } from 'rxjs'; jest.mock('../../../legacy/server/kbn_server'); jest.mock('../../../cli/cluster/cluster_manager'); -jest.mock('./plugins/find_legacy_plugin_specs.ts', () => ({ - findLegacyPluginSpecs: (settings: Record) => ({ - pluginSpecs: [], - pluginExtendedConfig: settings, - disabledPluginSpecs: [], - uiExports: [], - }), +jest.mock('./plugins/find_legacy_plugin_specs'); +jest.mock('./config/legacy_deprecation_adapters', () => ({ + convertLegacyDeprecationProvider: (provider: any) => Promise.resolve(provider), })); import { LegacyService, LegacyServiceSetupDeps, LegacyServiceStartDeps } from '.'; @@ -47,8 +43,10 @@ import { httpServiceMock } from '../http/http_service.mock'; import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mock'; +import { findLegacyPluginSpecs } from './plugins/find_legacy_plugin_specs'; const MockKbnServer: jest.Mock = KbnServer as any; +const findLegacyPluginSpecsMock: jest.Mock = findLegacyPluginSpecs as any; let coreId: symbol; let env: Env; @@ -66,6 +64,16 @@ beforeEach(() => { env = Env.createDefault(getEnvOptions()); configService = configServiceMock.create(); + findLegacyPluginSpecsMock.mockImplementation( + settings => + Promise.resolve({ + pluginSpecs: [], + pluginExtendedConfig: settings, + disabledPluginSpecs: [], + uiExports: [], + }) as any + ); + MockKbnServer.prototype.ready = jest.fn().mockReturnValue(Promise.resolve()); setupDeps = { @@ -115,6 +123,7 @@ beforeEach(() => { afterEach(() => { jest.clearAllMocks(); + findLegacyPluginSpecsMock.mockReset(); }); describe('once LegacyService is set up with connection info', () => { @@ -382,3 +391,52 @@ test('Cannot start without setup phase', async () => { `"Legacy service is not setup yet."` ); }); + +describe('#discoverPlugins()', () => { + it('calls findLegacyPluginSpecs with correct parameters', async () => { + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); + + await legacyService.discoverPlugins(); + expect(findLegacyPluginSpecs).toHaveBeenCalledTimes(1); + expect(findLegacyPluginSpecs).toHaveBeenCalledWith(expect.any(Object), logger); + }); + + it(`register legacy plugin's deprecation providers`, async () => { + findLegacyPluginSpecsMock.mockImplementation( + settings => + Promise.resolve({ + pluginSpecs: [ + { + getDeprecationsProvider: () => undefined, + }, + { + getDeprecationsProvider: () => 'providerA', + }, + { + getDeprecationsProvider: () => 'providerB', + }, + ], + pluginExtendedConfig: settings, + disabledPluginSpecs: [], + uiExports: [], + }) as any + ); + + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); + + await legacyService.discoverPlugins(); + expect(configService.addDeprecationProvider).toHaveBeenCalledTimes(2); + expect(configService.addDeprecationProvider).toHaveBeenCalledWith('', 'providerA'); + expect(configService.addDeprecationProvider).toHaveBeenCalledWith('', 'providerB'); + }); +}); diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index fcf0c45c17db8..4c2e57dc69b29 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -23,7 +23,7 @@ import { CoreService } from '../../types'; import { CoreSetup, CoreStart } from '../'; import { InternalCoreSetup, InternalCoreStart } from '../internal_types'; import { SavedObjectsLegacyUiExports } from '../types'; -import { Config } from '../config'; +import { Config, ConfigDeprecationProvider } from '../config'; import { CoreContext } from '../core_context'; import { DevConfig, DevConfigType } from '../dev'; import { BasePathProxyServer, HttpConfig, HttpConfigType } from '../http'; @@ -32,7 +32,7 @@ import { PluginsServiceSetup, PluginsServiceStart } from '../plugins'; import { findLegacyPluginSpecs } from './plugins'; import { LegacyPluginSpec } from './plugins/find_legacy_plugin_specs'; import { PathConfigType } from '../path'; -import { LegacyConfig } from './config'; +import { LegacyConfig, convertLegacyDeprecationProvider } from './config'; interface LegacyKbnServer { applyLoggingConfiguration: (settings: Readonly>) => void; @@ -157,6 +157,18 @@ export class LegacyService implements CoreService { uiExports, }; + const deprecationProviders = await pluginSpecs + .map(spec => spec.getDeprecationsProvider()) + .reduce(async (providers, current) => { + if (current) { + return [...(await providers), await convertLegacyDeprecationProvider(current)]; + } + return providers; + }, Promise.resolve([] as ConfigDeprecationProvider[])); + deprecationProviders.forEach(provider => + this.coreContext.configService.addDeprecationProvider('', provider) + ); + this.legacyRawConfig = pluginExtendedConfig; // check for unknown uiExport types diff --git a/src/core/server/legacy/logging/legacy_logging_server.ts b/src/core/server/legacy/logging/legacy_logging_server.ts index 0fe305fe77471..57706bcac2232 100644 --- a/src/core/server/legacy/logging/legacy_logging_server.ts +++ b/src/core/server/legacy/logging/legacy_logging_server.ts @@ -20,7 +20,7 @@ import { ServerExtType } from 'hapi'; import Podium from 'podium'; // @ts-ignore: implicit any for JS file -import { Config, transformDeprecations } from '../../../../legacy/server/config'; +import { Config } from '../../../../legacy/server/config'; // @ts-ignore: implicit any for JS file import { setupLogging } from '../../../../legacy/server/logging'; import { LogLevel } from '../../logging/log_level'; @@ -99,7 +99,7 @@ export class LegacyLoggingServer { ops: { interval: 2147483647 }, }; - setupLogging(this, Config.withDefaultSchema(transformDeprecations(config))); + setupLogging(this, Config.withDefaultSchema(config)); } public register({ plugin: { register }, options }: PluginRegisterParams): Promise { diff --git a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts index 08ec1b004d7b4..0a49154801e56 100644 --- a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts +++ b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts @@ -27,7 +27,7 @@ import { import { LoggerFactory } from '../../logging'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { collectUiExports as collectLegacyUiExports } from '../../../../legacy/ui/ui_exports/collect_ui_exports'; -import { LegacyConfig } from '../config'; +import { LegacyConfig, LegacyConfigDeprecationProvider } from '../config'; export interface LegacyPluginPack { getPath(): string; @@ -37,6 +37,7 @@ export interface LegacyPluginSpec { getId: () => unknown; getExpectedKibanaVersion: () => string; getConfigPrefix: () => string; + getDeprecationsProvider: () => LegacyConfigDeprecationProvider | undefined; } export async function findLegacyPluginSpecs(settings: unknown, loggerFactory: LoggerFactory) { diff --git a/src/core/server/logging/logging_service.mock.ts b/src/core/server/logging/logging_service.mock.ts index b5f522ca36a5f..50e6edc227bb5 100644 --- a/src/core/server/logging/logging_service.mock.ts +++ b/src/core/server/logging/logging_service.mock.ts @@ -45,7 +45,7 @@ const createLoggingServiceMock = () => { context, ...mockLog, })); - mocked.asLoggerFactory.mockImplementation(() => createLoggingServiceMock()); + mocked.asLoggerFactory.mockImplementation(() => mocked); mocked.stop.mockResolvedValue(); return mocked; }; diff --git a/src/core/server/plugins/discovery/plugins_discovery.test.ts b/src/core/server/plugins/discovery/plugins_discovery.test.ts index 224259bc121ec..bf55fc7caae4c 100644 --- a/src/core/server/plugins/discovery/plugins_discovery.test.ts +++ b/src/core/server/plugins/discovery/plugins_discovery.test.ts @@ -20,9 +20,9 @@ import { mockPackage, mockReaddir, mockReadFile, mockStat } from './plugins_discovery.test.mocks'; import { resolve } from 'path'; -import { BehaviorSubject } from 'rxjs'; import { first, map, toArray } from 'rxjs/operators'; -import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../../config'; +import { ConfigService, Env } from '../../config'; +import { rawConfigServiceMock } from '../../config/raw_config_service.mock'; import { getEnvOptions } from '../../config/__mocks__/env'; import { loggingServiceMock } from '../../logging/logging_service.mock'; import { PluginWrapper } from '../plugin'; @@ -115,9 +115,7 @@ test('properly iterates through plugin search locations', async () => { }) ); const configService = new ConfigService( - new BehaviorSubject( - new ObjectToConfigAdapter({ plugins: { paths: [TEST_EXTRA_PLUGIN_PATH] } }) - ), + rawConfigServiceMock.create({ rawConfig: { plugins: { paths: [TEST_EXTRA_PLUGIN_PATH] } } }), env, logger ); diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts index 547ac08cca76d..3fcd7fbbbe1ff 100644 --- a/src/core/server/plugins/plugin_context.test.ts +++ b/src/core/server/plugins/plugin_context.test.ts @@ -18,12 +18,12 @@ */ import { duration } from 'moment'; -import { BehaviorSubject } from 'rxjs'; import { first } from 'rxjs/operators'; import { createPluginInitializerContext } from './plugin_context'; import { CoreContext } from '../core_context'; -import { Env, ObjectToConfigAdapter } from '../config'; +import { Env } from '../config'; import { loggingServiceMock } from '../logging/logging_service.mock'; +import { rawConfigServiceMock } from '../config/raw_config_service.mock'; import { getEnvOptions } from '../config/__mocks__/env'; import { PluginManifest } from './types'; import { Server } from '../server'; @@ -54,9 +54,9 @@ describe('Plugin Context', () => { beforeEach(async () => { coreId = Symbol('core'); env = Env.createDefault(getEnvOptions()); - const config$ = new BehaviorSubject(new ObjectToConfigAdapter({})); + const config$ = rawConfigServiceMock.create({ rawConfig: {} }); server = new Server(config$, env, logger); - await server.setupConfigSchemas(); + await server.setupCoreConfig(); coreContext = { coreId, env, logger, configService: server.configService }; }); diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index df5473bc97d99..6768e85c8db17 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -23,7 +23,8 @@ import { resolve, join } from 'path'; import { BehaviorSubject, from } from 'rxjs'; import { schema } from '@kbn/config-schema'; -import { Config, ConfigPath, ConfigService, Env, ObjectToConfigAdapter } from '../config'; +import { ConfigPath, ConfigService, Env } from '../config'; +import { rawConfigServiceMock } from '../config/raw_config_service.mock'; import { getEnvOptions } from '../config/__mocks__/env'; import { coreMock } from '../mocks'; import { loggingServiceMock } from '../logging/logging_service.mock'; @@ -38,7 +39,7 @@ import { DiscoveredPlugin } from './types'; const MockPluginsSystem: jest.Mock = PluginsSystem as any; let pluginsService: PluginsService; -let config$: BehaviorSubject; +let config$: BehaviorSubject>; let configService: ConfigService; let coreId: symbol; let env: Env; @@ -109,10 +110,9 @@ describe('PluginsService', () => { coreId = Symbol('core'); env = Env.createDefault(getEnvOptions()); - config$ = new BehaviorSubject( - new ObjectToConfigAdapter({ plugins: { initialize: true } }) - ); - configService = new ConfigService(config$, env, logger); + config$ = new BehaviorSubject>({ plugins: { initialize: true } }); + const rawConfigService = rawConfigServiceMock.create({ rawConfig$: config$ }); + configService = new ConfigService(rawConfigService, env, logger); await configService.setSchema(config.path, config.schema); pluginsService = new PluginsService({ coreId, env, logger, configService }); @@ -388,6 +388,40 @@ describe('PluginsService', () => { await pluginsService.discover(); expect(configService.setSchema).toBeCalledWith('path', configSchema); }); + + it('registers plugin config deprecation provider in config service', async () => { + const configSchema = schema.string(); + jest.spyOn(configService, 'setSchema').mockImplementation(() => Promise.resolve()); + jest.spyOn(configService, 'addDeprecationProvider'); + + const deprecationProvider = () => []; + jest.doMock( + join('path-with-provider', 'server'), + () => ({ + config: { + schema: configSchema, + deprecations: deprecationProvider, + }, + }), + { + virtual: true, + } + ); + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([ + createPlugin('some-id', { + path: 'path-with-provider', + configPath: 'config-path', + }), + ]), + }); + await pluginsService.discover(); + expect(configService.addDeprecationProvider).toBeCalledWith( + 'config-path', + deprecationProvider + ); + }); }); describe('#generateUiPluginsConfigs()', () => { @@ -499,9 +533,7 @@ describe('PluginsService', () => { mockPluginSystem.uiPlugins.mockReturnValue(new Map()); - config$.next( - new ObjectToConfigAdapter({ plugins: { initialize: true }, plugin1: { enabled: false } }) - ); + config$.next({ plugins: { initialize: true }, plugin1: { enabled: false } }); await pluginsService.discover(); const { uiPlugins } = await pluginsService.setup({} as any); diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 3f9999aad4ab9..5a50cf8ea8ba2 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -196,6 +196,12 @@ export class PluginsService implements CoreService = Type; /** - * Describes a plugin configuration schema and capabilities. + * Describes a plugin configuration properties. * * @example * ```typescript @@ -56,12 +56,20 @@ export type PluginConfigSchema = Type; * uiProp: true, * }, * schema: configSchema, + * deprecations: ({ rename, unused }) => [ + * rename('securityKey', 'secret'), + * unused('deprecatedProperty'), + * ], * }; * ``` * * @public */ export interface PluginConfigDescriptor { + /** + * Provider for the {@link ConfigDeprecation} to apply to the plugin configuration. + */ + deprecations?: ConfigDeprecationProvider; /** * List of configuration properties that will be available on the client-side plugin. */ diff --git a/src/core/server/root/index.test.mocks.ts b/src/core/server/root/index.test.mocks.ts index 5754e5a5b9321..1d3add66d7c22 100644 --- a/src/core/server/root/index.test.mocks.ts +++ b/src/core/server/root/index.test.mocks.ts @@ -29,8 +29,14 @@ jest.doMock('../config/config_service', () => ({ ConfigService: jest.fn(() => configService), })); +import { rawConfigServiceMock } from '../config/raw_config_service.mock'; +export const rawConfigService = rawConfigServiceMock.create(); +jest.doMock('../config/raw_config_service', () => ({ + RawConfigService: jest.fn(() => rawConfigService), +})); + export const mockServer = { - setupConfigSchemas: jest.fn(), + setupCoreConfig: jest.fn(), setup: jest.fn(), stop: jest.fn(), configService, diff --git a/src/core/server/root/index.test.ts b/src/core/server/root/index.test.ts index 4eba2133dce28..3b187aac022c3 100644 --- a/src/core/server/root/index.test.ts +++ b/src/core/server/root/index.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { configService, logger, mockServer } from './index.test.mocks'; +import { rawConfigService, configService, logger, mockServer } from './index.test.mocks'; import { BehaviorSubject } from 'rxjs'; import { filter, first } from 'rxjs/operators'; @@ -26,13 +26,13 @@ import { Env } from '../config'; import { getEnvOptions } from '../config/__mocks__/env'; const env = new Env('.', getEnvOptions()); -const config$ = configService.getConfig$(); let mockConsoleError: jest.SpyInstance; beforeEach(() => { jest.spyOn(global.process, 'exit').mockReturnValue(undefined as never); mockConsoleError = jest.spyOn(console, 'error').mockReturnValue(undefined); + rawConfigService.getConfig$.mockReturnValue(new BehaviorSubject({ someValue: 'foo' })); configService.atPath.mockReturnValue(new BehaviorSubject({ someValue: 'foo' })); }); @@ -40,7 +40,7 @@ afterEach(() => { jest.restoreAllMocks(); logger.asLoggerFactory.mockClear(); logger.stop.mockClear(); - configService.getConfig$.mockClear(); + rawConfigService.getConfig$.mockClear(); logger.upgrade.mockReset(); configService.atPath.mockReset(); @@ -49,7 +49,7 @@ afterEach(() => { }); test('sets up services on "setup"', async () => { - const root = new Root(config$, env); + const root = new Root(rawConfigService, env); expect(logger.upgrade).not.toHaveBeenCalled(); expect(mockServer.setup).not.toHaveBeenCalled(); @@ -65,7 +65,7 @@ test('upgrades logging configuration after setup', async () => { const mockLoggingConfig$ = new BehaviorSubject({ someValue: 'foo' }); configService.atPath.mockReturnValue(mockLoggingConfig$); - const root = new Root(config$, env); + const root = new Root(rawConfigService, env); await root.setup(); expect(logger.upgrade).toHaveBeenCalledTimes(1); @@ -80,7 +80,7 @@ test('upgrades logging configuration after setup', async () => { test('stops services on "shutdown"', async () => { const mockOnShutdown = jest.fn(); - const root = new Root(config$, env, mockOnShutdown); + const root = new Root(rawConfigService, env, mockOnShutdown); await root.setup(); @@ -98,7 +98,7 @@ test('stops services on "shutdown"', async () => { test('stops services on "shutdown" an calls `onShutdown` with error passed to `shutdown`', async () => { const mockOnShutdown = jest.fn(); - const root = new Root(config$, env, mockOnShutdown); + const root = new Root(rawConfigService, env, mockOnShutdown); await root.setup(); @@ -117,7 +117,7 @@ test('stops services on "shutdown" an calls `onShutdown` with error passed to `s test('fails and stops services if server setup fails', async () => { const mockOnShutdown = jest.fn(); - const root = new Root(config$, env, mockOnShutdown); + const root = new Root(rawConfigService, env, mockOnShutdown); const serverError = new Error('server failed'); mockServer.setup.mockRejectedValue(serverError); @@ -136,7 +136,7 @@ test('fails and stops services if server setup fails', async () => { test('fails and stops services if initial logger upgrade fails', async () => { const mockOnShutdown = jest.fn(); - const root = new Root(config$, env, mockOnShutdown); + const root = new Root(rawConfigService, env, mockOnShutdown); const loggingUpgradeError = new Error('logging config upgrade failed'); logger.upgrade.mockImplementation(() => { @@ -167,7 +167,7 @@ test('stops services if consequent logger upgrade fails', async () => { const mockLoggingConfig$ = new BehaviorSubject({ someValue: 'foo' }); configService.atPath.mockReturnValue(mockLoggingConfig$); - const root = new Root(config$, env, mockOnShutdown); + const root = new Root(rawConfigService, env, mockOnShutdown); await root.setup(); expect(mockOnShutdown).not.toHaveBeenCalled(); diff --git a/src/core/server/root/index.ts b/src/core/server/root/index.ts index ac6ef79483280..eecc6399366dc 100644 --- a/src/core/server/root/index.ts +++ b/src/core/server/root/index.ts @@ -17,10 +17,10 @@ * under the License. */ -import { ConnectableObservable, Observable, Subscription } from 'rxjs'; +import { ConnectableObservable, Subscription } from 'rxjs'; import { first, map, publishReplay, switchMap, tap } from 'rxjs/operators'; -import { Config, Env } from '../config'; +import { Env, RawConfigurationProvider } from '../config'; import { Logger, LoggerFactory, LoggingConfigType, LoggingService } from '../logging'; import { Server } from '../server'; @@ -35,19 +35,19 @@ export class Root { private loggingConfigSubscription?: Subscription; constructor( - config$: Observable, + rawConfigProvider: RawConfigurationProvider, env: Env, private readonly onShutdown?: (reason?: Error | string) => void ) { this.loggingService = new LoggingService(); this.logger = this.loggingService.asLoggerFactory(); this.log = this.logger.get('root'); - this.server = new Server(config$, env, this.logger); + this.server = new Server(rawConfigProvider, env, this.logger); } public async setup() { try { - await this.server.setupConfigSchemas(); + await this.server.setupCoreConfig(); await this.setupLogging(); this.log.debug('setting up root'); return await this.server.setup(); diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 142332d613dc9..18e76324ff309 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -502,15 +502,34 @@ export class ClusterClient implements IClusterClient { close(): void; } +// @public +export type ConfigDeprecation = (config: Record, fromPath: string, logger: ConfigDeprecationLogger) => Record; + +// @public +export interface ConfigDeprecationFactory { + rename(oldKey: string, newKey: string): ConfigDeprecation; + renameFromRoot(oldKey: string, newKey: string): ConfigDeprecation; + unused(unusedKey: string): ConfigDeprecation; + unusedFromRoot(unusedKey: string): ConfigDeprecation; +} + +// @public +export type ConfigDeprecationLogger = (message: string) => void; + +// @public +export type ConfigDeprecationProvider = (factory: ConfigDeprecationFactory) => ConfigDeprecation[]; + // @public (undocumented) export type ConfigPath = string | string[]; // @internal (undocumented) export class ConfigService { - // Warning: (ae-forgotten-export) The symbol "Config" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "RawConfigurationProvider" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Env" needs to be exported by the entry point index.d.ts - constructor(config$: Observable, env: Env, logger: LoggerFactory); + constructor(rawConfigProvider: RawConfigurationProvider, env: Env, logger: LoggerFactory); + addDeprecationProvider(path: ConfigPath, provider: ConfigDeprecationProvider): void; atPath(path: ConfigPath): Observable; + // Warning: (ae-forgotten-export) The symbol "Config" needs to be exported by the entry point index.d.ts getConfig$(): Observable; // (undocumented) getUnusedPaths(): Promise; @@ -520,6 +539,7 @@ export class ConfigService { isEnabledAtPath(path: ConfigPath): Promise; optionalAtPath(path: ConfigPath): Observable; setSchema(path: ConfigPath, schema: Type): Promise; + validate(): Promise; } // @public @@ -1024,6 +1044,7 @@ export interface Plugin { + deprecations?: ConfigDeprecationProvider; exposeToBrowser?: { [P in keyof T]?: boolean; }; @@ -1792,9 +1813,9 @@ export const validBodyOutput: readonly ["data", "stream"]; // // src/core/server/http/router/response.ts:316:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts // src/core/server/plugins/plugins_service.ts:43:5 - (ae-forgotten-export) The symbol "InternalPluginInfo" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:213:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:213:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:214:3 - (ae-forgotten-export) The symbol "ElasticsearchConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:215:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:221:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:221:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:222:3 - (ae-forgotten-export) The symbol "ElasticsearchConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:223:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 7b91fb7257957..d593a6275fa4c 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -29,14 +29,16 @@ import { } from './server.test.mocks'; import { BehaviorSubject } from 'rxjs'; -import { Env, Config, ObjectToConfigAdapter } from './config'; +import { Env } from './config'; import { Server } from './server'; import { getEnvOptions } from './config/__mocks__/env'; import { loggingServiceMock } from './logging/logging_service.mock'; +import { rawConfigServiceMock } from './config/raw_config_service.mock'; const env = new Env('.', getEnvOptions()); const logger = loggingServiceMock.create(); +const rawConfigService = rawConfigServiceMock.create({}); beforeEach(() => { mockConfigService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); @@ -47,9 +49,8 @@ afterEach(() => { jest.clearAllMocks(); }); -const config$ = new BehaviorSubject(new ObjectToConfigAdapter({})); test('sets up services on "setup"', async () => { - const server = new Server(config$, env, logger); + const server = new Server(rawConfigService, env, logger); expect(mockHttpService.setup).not.toHaveBeenCalled(); expect(mockElasticsearchService.setup).not.toHaveBeenCalled(); @@ -67,7 +68,7 @@ test('sets up services on "setup"', async () => { }); test('injects legacy dependency to context#setup()', async () => { - const server = new Server(config$, env, logger); + const server = new Server(rawConfigService, env, logger); const pluginA = Symbol(); const pluginB = Symbol(); @@ -89,7 +90,7 @@ test('injects legacy dependency to context#setup()', async () => { }); test('runs services on "start"', async () => { - const server = new Server(config$, env, logger); + const server = new Server(rawConfigService, env, logger); expect(mockHttpService.setup).not.toHaveBeenCalled(); expect(mockLegacyService.start).not.toHaveBeenCalled(); @@ -109,13 +110,13 @@ test('runs services on "start"', async () => { test('does not fail on "setup" if there are unused paths detected', async () => { mockConfigService.getUnusedPaths.mockResolvedValue(['some.path', 'another.path']); - const server = new Server(config$, env, logger); + const server = new Server(rawConfigService, env, logger); await expect(server.setup()).resolves.toBeDefined(); }); test('stops services on "stop"', async () => { - const server = new Server(config$, env, logger); + const server = new Server(rawConfigService, env, logger); await server.setup(); @@ -134,26 +135,25 @@ test('stops services on "stop"', async () => { expect(mockSavedObjectsService.stop).toHaveBeenCalledTimes(1); }); -test(`doesn't setup core services if services config validation fails`, async () => { - mockConfigService.setSchema.mockImplementation(() => { - throw new Error('invalid config'); +test(`doesn't setup core services if config validation fails`, async () => { + mockConfigService.validate.mockImplementationOnce(() => { + return Promise.reject(new Error('invalid config')); }); - const server = new Server(config$, env, logger); - await expect(server.setupConfigSchemas()).rejects.toThrowErrorMatchingInlineSnapshot( - `"invalid config"` - ); + const server = new Server(rawConfigService, env, logger); + await expect(server.setup()).rejects.toThrowErrorMatchingInlineSnapshot(`"invalid config"`); + expect(mockHttpService.setup).not.toHaveBeenCalled(); expect(mockElasticsearchService.setup).not.toHaveBeenCalled(); expect(mockPluginsService.setup).not.toHaveBeenCalled(); expect(mockLegacyService.setup).not.toHaveBeenCalled(); }); -test(`doesn't setup core services if config validation fails`, async () => { +test(`doesn't setup core services if legacy config validation fails`, async () => { mockEnsureValidConfiguration.mockImplementation(() => { throw new Error('Unknown configuration keys'); }); - const server = new Server(config$, env, logger); + const server = new Server(rawConfigService, env, logger); await expect(server.setup()).rejects.toThrowErrorMatchingInlineSnapshot( `"Unknown configuration keys"` diff --git a/src/core/server/server.ts b/src/core/server/server.ts index e7166f30caa34..e7bc57ea5fb94 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -16,11 +16,17 @@ * specific language governing permissions and limitations * under the License. */ -import { Observable } from 'rxjs'; + import { take } from 'rxjs/operators'; import { Type } from '@kbn/config-schema'; -import { ConfigService, Env, Config, ConfigPath } from './config'; +import { + ConfigService, + Env, + ConfigPath, + RawConfigurationProvider, + coreDeprecationProvider, +} from './config'; import { ElasticsearchService } from './elasticsearch'; import { HttpService, InternalHttpServiceSetup } from './http'; import { LegacyService, ensureValidConfiguration } from './legacy'; @@ -44,6 +50,7 @@ import { InternalCoreSetup } from './internal_types'; import { CapabilitiesService } from './capabilities'; const coreId = Symbol('core'); +const rootConfigPath = ''; export class Server { public readonly configService: ConfigService; @@ -58,12 +65,12 @@ export class Server { private readonly uiSettings: UiSettingsService; constructor( - public readonly config$: Observable, + rawConfigProvider: RawConfigurationProvider, public readonly env: Env, private readonly logger: LoggerFactory ) { this.log = this.logger.get('server'); - this.configService = new ConfigService(config$, env, logger); + this.configService = new ConfigService(rawConfigProvider, env, logger); const core = { coreId, configService: this.configService, env, logger }; this.context = new ContextService(core); @@ -84,6 +91,7 @@ export class Server { const legacyPlugins = await this.legacy.discoverPlugins(); // Immediately terminate in case of invalid configuration + await this.configService.validate(); await ensureValidConfiguration(this.configService, legacyPlugins); const contextServiceSetup = this.context.setup({ @@ -207,7 +215,7 @@ export class Server { ); } - public async setupConfigSchemas() { + public async setupCoreConfig() { const schemas: Array<[ConfigPath, Type]> = [ [pathConfig.path, pathConfig.schema], [elasticsearchConfig.path, elasticsearchConfig.schema], @@ -220,6 +228,8 @@ export class Server { [uiSettingsConfig.path, uiSettingsConfig.schema], ]; + this.configService.addDeprecationProvider(rootConfigPath, coreDeprecationProvider); + for (const [path, schema] of schemas) { await this.configService.setSchema(path, schema); } diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index 98f0800feae79..b51cc4ef56410 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -25,3 +25,4 @@ export * from './map_to_object'; export * from './merge'; export * from './pick'; export * from './url'; +export * from './unset'; diff --git a/src/core/utils/unset.test.ts b/src/core/utils/unset.test.ts new file mode 100644 index 0000000000000..c0112e729811f --- /dev/null +++ b/src/core/utils/unset.test.ts @@ -0,0 +1,104 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { unset } from './unset'; + +describe('unset', () => { + it('deletes a property from an object', () => { + const obj = { + a: 'a', + b: 'b', + c: 'c', + }; + unset(obj, 'a'); + expect(obj).toEqual({ + b: 'b', + c: 'c', + }); + }); + + it('does nothing if the property is not present', () => { + const obj = { + a: 'a', + b: 'b', + c: 'c', + }; + unset(obj, 'd'); + expect(obj).toEqual({ + a: 'a', + b: 'b', + c: 'c', + }); + }); + + it('handles nested paths', () => { + const obj = { + foo: { + bar: { + one: 'one', + two: 'two', + }, + hello: 'dolly', + }, + some: { + things: 'here', + }, + }; + unset(obj, 'foo.bar.one'); + expect(obj).toEqual({ + foo: { + bar: { + two: 'two', + }, + hello: 'dolly', + }, + some: { + things: 'here', + }, + }); + }); + + it('does nothing if nested paths does not exist', () => { + const obj = { + foo: { + bar: { + one: 'one', + two: 'two', + }, + hello: 'dolly', + }, + some: { + things: 'here', + }, + }; + unset(obj, 'foo.nothere.baz'); + expect(obj).toEqual({ + foo: { + bar: { + one: 'one', + two: 'two', + }, + hello: 'dolly', + }, + some: { + things: 'here', + }, + }); + }); +}); diff --git a/src/core/utils/unset.ts b/src/core/utils/unset.ts new file mode 100644 index 0000000000000..8008d4ee08ba3 --- /dev/null +++ b/src/core/utils/unset.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { get } from './get'; + +/** + * Unset a (potentially nested) key from given object. + * This mutates the original object. + * + * @example + * ``` + * unset(myObj, 'someRootProperty'); + * unset(myObj, 'some.nested.path'); + * ``` + */ +export function unset(obj: OBJ, atPath: string) { + const paths = atPath + .split('.') + .map(s => s.trim()) + .filter(v => v !== ''); + if (paths.length === 0) { + return; + } + if (paths.length === 1) { + delete obj[paths[0]]; + return; + } + const property = paths.pop() as string; + const parent = get(obj, paths as any) as any; + if (parent !== undefined) { + delete parent[property]; + } +} diff --git a/src/legacy/plugin_discovery/find_plugin_specs.js b/src/legacy/plugin_discovery/find_plugin_specs.js index faccdf396df04..e2a70b94c0010 100644 --- a/src/legacy/plugin_discovery/find_plugin_specs.js +++ b/src/legacy/plugin_discovery/find_plugin_specs.js @@ -21,7 +21,7 @@ import * as Rx from 'rxjs'; import { distinct, toArray, mergeMap, share, shareReplay, filter, last, map, tap } from 'rxjs/operators'; import { realpathSync } from 'fs'; -import { transformDeprecations, Config } from '../server/config'; +import { Config } from '../server/config'; import { extendConfigService, @@ -40,9 +40,7 @@ import { } from './errors'; export function defaultConfig(settings) { - return Config.withDefaultSchema( - transformDeprecations(settings) - ); + return Config.withDefaultSchema(settings); } function bufferAllResults(observable) { diff --git a/src/legacy/plugin_discovery/plugin_config/settings.js b/src/legacy/plugin_discovery/plugin_config/settings.js index d7d32ca04976a..44ecb5718fe21 100644 --- a/src/legacy/plugin_discovery/plugin_config/settings.js +++ b/src/legacy/plugin_discovery/plugin_config/settings.js @@ -19,7 +19,6 @@ import { get } from 'lodash'; -import * as serverConfig from '../../server/config'; import { getTransform } from '../../deprecation'; /** @@ -33,7 +32,7 @@ import { getTransform } from '../../deprecation'; */ export async function getSettings(spec, rootSettings, logDeprecation) { const prefix = spec.getConfigPrefix(); - const rawSettings = get(serverConfig.transformDeprecations(rootSettings), prefix); + const rawSettings = get(rootSettings, prefix); const transform = await getTransform(spec); return transform(rawSettings, logDeprecation); } diff --git a/src/legacy/server/config/__tests__/deprecation_warnings.js b/src/legacy/server/config/__tests__/deprecation_warnings.js deleted file mode 100644 index f49a1b6df45e2..0000000000000 --- a/src/legacy/server/config/__tests__/deprecation_warnings.js +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { spawn } from 'child_process'; - -import expect from '@kbn/expect'; - -const RUN_KBN_SERVER_STARTUP = require.resolve('./fixtures/run_kbn_server_startup'); -const SETUP_NODE_ENV = require.resolve('../../../../setup_node_env'); -const SECOND = 1000; - -describe('config/deprecation warnings', function () { - this.timeout(65 * SECOND); - - let stdio = ''; - let proc = null; - - before(async () => { - proc = spawn(process.execPath, [ - '-r', SETUP_NODE_ENV, - RUN_KBN_SERVER_STARTUP - ], { - stdio: ['ignore', 'pipe', 'pipe'], - env: { - ...process.env, - CREATE_SERVER_OPTS: JSON.stringify({ - logging: { - quiet: false, - silent: false - }, - uiSettings: { - enabled: true - } - }) - } - }); - - // Either time out in 60 seconds, or resolve once the line is in our buffer - return Promise.race([ - new Promise((resolve) => setTimeout(resolve, 60 * SECOND)), - new Promise((resolve, reject) => { - proc.stdout.on('data', (chunk) => { - stdio += chunk.toString('utf8'); - if (chunk.toString('utf8').includes('deprecation')) { - resolve(); - } - }); - - proc.stderr.on('data', (chunk) => { - stdio += chunk.toString('utf8'); - if (chunk.toString('utf8').includes('deprecation')) { - resolve(); - } - }); - - proc.on('exit', (code) => { - proc = null; - if (code > 0) { - reject(new Error(`Kibana server exited with ${code} -- stdout:\n\n${stdio}\n`)); - } else { - resolve(); - } - }); - }) - ]); - }); - - after(() => { - if (proc) { - proc.kill('SIGKILL'); - } - }); - - it('logs deprecation warnings when using outdated config', async () => { - const deprecationLines = stdio - .split('\n') - .map(json => { - try { - // in dev mode kibana might log things like node.js warnings which - // are not JSON, ignore the lines that don't parse as JSON - return JSON.parse(json); - } catch (error) { - return null; - } - }) - .filter(Boolean) - .filter(line => - line.type === 'log' && - line.tags.includes('deprecation') && - line.tags.includes('warning') - ); - - try { - expect(deprecationLines).to.have.length(1); - expect(deprecationLines[0]).to.have.property('message', 'uiSettings.enabled is deprecated and is no longer used'); - } catch (error) { - throw new Error(`Expected stdio to include deprecation message about uiSettings.enabled\n\nstdio:\n${stdio}\n\n`); - } - }); -}); diff --git a/src/legacy/server/config/index.js b/src/legacy/server/config/index.js index 7cc53ae1c74fb..0a83ecb1c58e1 100644 --- a/src/legacy/server/config/index.js +++ b/src/legacy/server/config/index.js @@ -17,5 +17,4 @@ * under the License. */ -export { transformDeprecations } from './transform_deprecations'; export { Config } from './config'; diff --git a/src/legacy/server/config/transform_deprecations.js b/src/legacy/server/config/transform_deprecations.js deleted file mode 100644 index 430564cde7814..0000000000000 --- a/src/legacy/server/config/transform_deprecations.js +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _, { set } from 'lodash'; -import { createTransform, Deprecations } from '../../deprecation'; -import { unset } from '../../utils'; - -const { rename, unused } = Deprecations; - -const savedObjectsIndexCheckTimeout = (settings, log) => { - if (_.has(settings, 'savedObjects.indexCheckTimeout')) { - log('savedObjects.indexCheckTimeout is no longer necessary.'); - - if (Object.keys(settings.savedObjects).length > 1) { - delete settings.savedObjects.indexCheckTimeout; - } else { - delete settings.savedObjects; - } - } -}; - -const rewriteBasePath = (settings, log) => { - if (_.has(settings, 'server.basePath') && !_.has(settings, 'server.rewriteBasePath')) { - log( - 'You should set server.basePath along with server.rewriteBasePath. Starting in 7.0, Kibana ' + - 'will expect that all requests start with server.basePath rather than expecting you to rewrite ' + - 'the requests in your reverse proxy. Set server.rewriteBasePath to false to preserve the ' + - 'current behavior and silence this warning.' - ); - } -}; - -const loggingTimezone = (settings, log) => { - if (_.has(settings, 'logging.useUTC')) { - const timezone = settings.logging.useUTC ? 'UTC' : false; - set('logging.timezone', timezone); - unset(settings, 'logging.useUTC'); - log(`Config key "logging.useUTC" is deprecated. It has been replaced with "logging.timezone"`); - } -}; - -const configPath = (settings, log) => { - if (_.has(process, 'env.CONFIG_PATH')) { - log(`Environment variable CONFIG_PATH is deprecated. It has been replaced with KIBANA_PATH_CONF pointing to a config folder`); - } -}; - -const dataPath = (settings, log) => { - if (_.has(process, 'env.DATA_PATH')) { - log(`Environment variable "DATA_PATH" will be removed. It has been replaced with kibana.yml setting "path.data"`); - } -}; - -const NONCE_STRING = `{nonce}`; -// Policies that should include the 'self' source -const SELF_POLICIES = Object.freeze(['script-src', 'style-src']); -const SELF_STRING = `'self'`; - -const cspRules = (settings, log) => { - const rules = _.get(settings, 'csp.rules'); - if (!rules) { - return; - } - - const parsed = new Map(rules.map(ruleStr => { - const parts = ruleStr.split(/\s+/); - return [parts[0], parts.slice(1)]; - })); - - settings.csp.rules = [...parsed].map(([policy, sourceList]) => { - if (sourceList.find(source => source.includes(NONCE_STRING))) { - log(`csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in ${policy}`); - sourceList = sourceList.filter(source => !source.includes(NONCE_STRING)); - - // Add 'self' if not present - if (!sourceList.find(source => source.includes(SELF_STRING))) { - sourceList.push(SELF_STRING); - } - } - - if (SELF_POLICIES.includes(policy) && !sourceList.find(source => source.includes(SELF_STRING))) { - log(`csp.rules must contain the 'self' source. Automatically adding to ${policy}.`); - sourceList.push(SELF_STRING); - } - - return `${policy} ${sourceList.join(' ')}`.trim(); - }); -}; - -const deprecations = [ - //server - unused('server.xsrf.token'), - unused('uiSettings.enabled'), - rename('optimize.lazy', 'optimize.watch'), - rename('optimize.lazyPort', 'optimize.watchPort'), - rename('optimize.lazyHost', 'optimize.watchHost'), - rename('optimize.lazyPrebuild', 'optimize.watchPrebuild'), - rename('optimize.lazyProxyTimeout', 'optimize.watchProxyTimeout'), - rename('xpack.telemetry.enabled', 'telemetry.enabled'), - rename('xpack.telemetry.config', 'telemetry.config'), - rename('xpack.telemetry.banner', 'telemetry.banner'), - rename('xpack.telemetry.url', 'telemetry.url'), - savedObjectsIndexCheckTimeout, - rewriteBasePath, - loggingTimezone, - configPath, - dataPath, - cspRules -]; - -export const transformDeprecations = createTransform(deprecations); diff --git a/src/legacy/server/config/transform_deprecations.test.js b/src/legacy/server/config/transform_deprecations.test.js deleted file mode 100644 index f8cf38efc8bd8..0000000000000 --- a/src/legacy/server/config/transform_deprecations.test.js +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import sinon from 'sinon'; -import { transformDeprecations } from './transform_deprecations'; - -describe('server/config', function () { - describe('transformDeprecations', function () { - describe('savedObjects.indexCheckTimeout', () => { - it('removes the indexCheckTimeout and savedObjects properties', () => { - const settings = { - savedObjects: { - indexCheckTimeout: 123, - }, - }; - - expect(transformDeprecations(settings)).toEqual({}); - }); - - it('keeps the savedObjects property if it has other keys', () => { - const settings = { - savedObjects: { - indexCheckTimeout: 123, - foo: 'bar', - }, - }; - - expect(transformDeprecations(settings)).toEqual({ - savedObjects: { - foo: 'bar', - }, - }); - }); - - it('logs that the setting is no longer necessary', () => { - const settings = { - savedObjects: { - indexCheckTimeout: 123, - }, - }; - - const log = sinon.spy(); - transformDeprecations(settings, log); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, sinon.match('savedObjects.indexCheckTimeout')); - }); - }); - - describe('csp.rules', () => { - describe('with nonce source', () => { - it('logs a warning', () => { - const settings = { - csp: { - rules: [`script-src 'self' 'nonce-{nonce}'`], - }, - }; - - const log = jest.fn(); - transformDeprecations(settings, log); - expect(log.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in script-src", - ], - ] - `); - }); - - it('replaces a nonce', () => { - expect( - transformDeprecations({ csp: { rules: [`script-src 'nonce-{nonce}'`] } }, jest.fn()).csp - .rules - ).toEqual([`script-src 'self'`]); - expect( - transformDeprecations( - { csp: { rules: [`script-src 'unsafe-eval' 'nonce-{nonce}'`] } }, - jest.fn() - ).csp.rules - ).toEqual([`script-src 'unsafe-eval' 'self'`]); - }); - - it('removes a quoted nonce', () => { - expect( - transformDeprecations( - { csp: { rules: [`script-src 'self' 'nonce-{nonce}'`] } }, - jest.fn() - ).csp.rules - ).toEqual([`script-src 'self'`]); - expect( - transformDeprecations( - { csp: { rules: [`script-src 'nonce-{nonce}' 'self'`] } }, - jest.fn() - ).csp.rules - ).toEqual([`script-src 'self'`]); - }); - - it('removes a non-quoted nonce', () => { - expect( - transformDeprecations( - { csp: { rules: [`script-src 'self' nonce-{nonce}`] } }, - jest.fn() - ).csp.rules - ).toEqual([`script-src 'self'`]); - expect( - transformDeprecations( - { csp: { rules: [`script-src nonce-{nonce} 'self'`] } }, - jest.fn() - ).csp.rules - ).toEqual([`script-src 'self'`]); - }); - - it('removes a strange nonce', () => { - expect( - transformDeprecations( - { csp: { rules: [`script-src 'self' blah-{nonce}-wow`] } }, - jest.fn() - ).csp.rules - ).toEqual([`script-src 'self'`]); - }); - - it('removes multiple nonces', () => { - expect( - transformDeprecations( - { - csp: { - rules: [ - `script-src 'nonce-{nonce}' 'self' blah-{nonce}-wow`, - `style-src 'nonce-{nonce}' 'self'`, - ], - }, - }, - jest.fn() - ).csp.rules - ).toEqual([`script-src 'self'`, `style-src 'self'`]); - }); - }); - - describe('without self source', () => { - it('logs a warning', () => { - const log = jest.fn(); - transformDeprecations({ csp: { rules: [`script-src 'unsafe-eval'`] } }, log); - expect(log.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "csp.rules must contain the 'self' source. Automatically adding to script-src.", - ], - ] - `); - }); - - it('adds self', () => { - expect( - transformDeprecations({ csp: { rules: [`script-src 'unsafe-eval'`] } }, jest.fn()).csp - .rules - ).toEqual([`script-src 'unsafe-eval' 'self'`]); - }); - }); - - it('does not add self to other policies', () => { - expect( - transformDeprecations({ csp: { rules: [`worker-src blob:`] } }, jest.fn()).csp.rules - ).toEqual([`worker-src blob:`]); - }); - }); - }); -}); diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index 87fb9dc8b9fec..ecd4dcfa14eb5 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -31,8 +31,6 @@ import { loggingMixin } from './logging'; import warningsMixin from './warnings'; import { statusMixin } from './status'; import pidMixin from './pid'; -import { configDeprecationWarningsMixin } from './config/deprecation_warnings'; -import { transformDeprecations } from './config/transform_deprecations'; import configCompleteMixin from './config/complete'; import optimizeMixin from '../../optimize'; import * as Plugins from './plugins'; @@ -89,7 +87,6 @@ export default class KbnServer { // adds methods for extending this.server serverExtensionsMixin, loggingMixin, - configDeprecationWarningsMixin, warningsMixin, statusMixin, @@ -198,7 +195,7 @@ export default class KbnServer { applyLoggingConfiguration(settings) { const config = new Config( this.config.getSchema(), - transformDeprecations(settings) + settings ); const loggingOptions = loggingConfiguration(config); diff --git a/src/plugins/testbed/server/index.ts b/src/plugins/testbed/server/index.ts index 07fda4eb98727..1b4de8f8f5c95 100644 --- a/src/plugins/testbed/server/index.ts +++ b/src/plugins/testbed/server/index.ts @@ -41,6 +41,11 @@ export const config: PluginConfigDescriptor = { uiProp: true, }, schema: configSchema, + deprecations: ({ rename, unused, renameFromRoot }) => [ + rename('securityKey', 'secret'), + renameFromRoot('oldtestbed.uiProp', 'testbed.uiProp'), + unused('deprecatedProperty'), + ], }; class Plugin { diff --git a/src/test_utils/kbn_server.ts b/src/test_utils/kbn_server.ts index cda7e005bf75e..502e2c088c931 100644 --- a/src/test_utils/kbn_server.ts +++ b/src/test_utils/kbn_server.ts @@ -33,7 +33,6 @@ import { resolve } from 'path'; import { BehaviorSubject } from 'rxjs'; import supertest from 'supertest'; import { CliArgs, Env } from '../core/server/config'; -import { LegacyObjectToConfigAdapter } from '../core/server/legacy'; import { Root } from '../core/server/root'; import KbnServer from '../legacy/server/kbn_server'; import { CallCluster } from '../legacy/core_plugins/elasticsearch'; @@ -84,9 +83,9 @@ export function createRootWithSettings( }); return new Root( - new BehaviorSubject( - new LegacyObjectToConfigAdapter(defaultsDeep({}, settings, DEFAULTS_SETTINGS)) - ), + { + getConfig$: () => new BehaviorSubject(defaultsDeep({}, settings, DEFAULTS_SETTINGS)), + }, env ); } From ef6638b78440ec1501efc2b9b1995ceef647793a Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Fri, 13 Dec 2019 13:54:31 +0100 Subject: [PATCH 31/36] [SIEM] Cleanup react-beautiful-dnd jest warnings (#52468) (#52981) --- x-pack/legacy/plugins/siem/package.json | 2 +- .../public/components/bytes/index.test.tsx | 4 +- .../certificate_fingerprint/index.test.tsx | 3 +- .../drag_and_drop/draggable_wrapper.test.tsx | 5 +- .../drag_and_drop/droppable_wrapper.test.tsx | 5 +- .../components/draggables/index.test.tsx | 26 ++++--- .../public/components/duration/index.test.tsx | 4 +- .../point_tool_tip_content.test.tsx | 5 +- .../event_details/event_details.test.tsx | 5 +- .../event_fields_browser.test.tsx | 22 +++--- .../events_viewer/events_viewer.test.tsx | 4 +- .../components/events_viewer/index.test.tsx | 4 +- .../field_renderers/field_renderers.test.tsx | 30 ++++---- .../fields_browser/category.test.tsx | 3 +- .../fields_browser/field_items.test.tsx | 3 +- .../fields_browser/fields_pane.test.tsx | 4 +- .../components/fields_browser/index.test.tsx | 13 ++++ .../components/header_page/index.test.tsx | 5 +- .../siem/public/components/ip/index.test.tsx | 8 +- .../components/ja3_fingerprint/index.test.tsx | 4 +- .../components/last_event_time/index.test.tsx | 4 +- .../markdown/markdown_hint.test.tsx | 2 +- .../components/ml/entity_draggable.test.tsx | 5 +- .../ml/score/anomaly_score.test.tsx | 5 +- .../ml/score/anomaly_scores.test.tsx | 4 +- .../get_anomalies_host_table_columns.test.tsx | 4 +- ...t_anomalies_network_table_columns.test.tsx | 4 +- .../components/ml_popover/ml_popover.test.tsx | 5 +- .../public/components/netflow/index.test.tsx | 76 ++++++++++--------- .../hosts/first_last_seen_host/index.test.tsx | 24 ++++-- .../page/hosts/hosts_table/index.test.tsx | 27 +------ .../uncommon_process_table/index.test.tsx | 4 +- .../network/network_dns_table/index.test.tsx | 5 +- .../network/network_http_table/index.test.tsx | 4 +- .../index.test.tsx | 5 +- .../network_top_n_flow_table/index.test.tsx | 6 +- .../page/network/tls_table/index.test.tsx | 4 +- .../page/network/users_table/index.test.tsx | 4 +- .../public/components/port/index.test.tsx | 10 ++- .../source_destination/index.test.tsx | 5 +- .../source_destination_ip.test.tsx | 4 +- .../public/components/tables/helpers.test.tsx | 4 +- .../body/column_headers/index.test.tsx | 5 +- .../components/timeline/body/index.test.tsx | 6 +- .../timeline/body/renderers/args.test.tsx | 16 ++-- .../renderers/auditd/generic_details.test.tsx | 38 +++++----- .../auditd/generic_file_details.test.tsx | 38 +++++----- .../auditd/generic_row_renderer.test.tsx | 5 +- .../primary_secondary_user_info.test.tsx | 20 ++--- .../session_user_host_working_dir.test.tsx | 18 +++-- .../dns/dns_request_event_details.test.tsx | 6 +- .../dns_request_event_details_line.test.tsx | 32 ++++---- .../renderers/empty_column_renderer.test.tsx | 5 +- .../endgame_security_event_details.test.tsx | 12 +-- ...dgame_security_event_details_line.test.tsx | 42 +++++----- .../renderers/exit_code_draggable.test.tsx | 18 +++-- .../body/renderers/file_draggable.test.tsx | 16 ++-- .../body/renderers/formatted_field.test.tsx | 4 +- .../renderers/get_column_renderer.test.tsx | 5 +- .../body/renderers/get_row_renderer.test.tsx | 5 +- .../body/renderers/host_working_dir.test.tsx | 8 +- .../netflow/netflow_row_renderer.test.tsx | 5 +- .../parent_process_draggable.test.tsx | 16 ++-- .../renderers/plain_column_renderer.test.tsx | 5 +- .../body/renderers/process_draggable.test.tsx | 60 ++++++++------- .../body/renderers/process_hash.test.tsx | 16 ++-- .../suricata_signature.test.tsx.snap | 1 - .../suricata/suricata_details.test.tsx | 6 +- .../suricata/suricata_row_renderer.test.tsx | 4 +- .../suricata/suricata_signature.test.tsx | 12 +-- .../renderers/suricata/suricata_signature.tsx | 1 - .../renderers/system/generic_details.test.tsx | 38 +++++----- .../system/generic_file_details.test.tsx | 72 +++++++++--------- .../system/generic_row_renderer.test.tsx | 5 +- .../body/renderers/system/package.test.tsx | 10 ++- .../renderers/user_host_working_dir.test.tsx | 30 ++++---- .../body/renderers/zeek/zeek_details.test.tsx | 20 ++--- .../renderers/zeek/zeek_row_renderer.test.tsx | 4 +- .../renderers/zeek/zeek_signature.test.tsx | 23 +++--- .../data_providers/data_providers.test.tsx | 5 +- .../data_providers/providers.test.tsx | 5 +- .../components/timeline/header/index.test.tsx | 4 +- .../components/timeline/timeline.test.tsx | 5 +- .../truncatable_text/index.test.tsx | 3 +- .../pages/hosts/details/details_tabs.test.tsx | 3 +- .../pages/network/ip_details/index.test.tsx | 8 +- .../siem/public/utils/use_mount_appended.ts | 29 +++++++ yarn.lock | 8 +- 88 files changed, 637 insertions(+), 434 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/utils/use_mount_appended.ts diff --git a/x-pack/legacy/plugins/siem/package.json b/x-pack/legacy/plugins/siem/package.json index ef6431327b5ab..bf5d6d3a3089c 100644 --- a/x-pack/legacy/plugins/siem/package.json +++ b/x-pack/legacy/plugins/siem/package.json @@ -17,7 +17,7 @@ }, "dependencies": { "lodash": "^4.17.15", - "react-beautiful-dnd": "^12.1.1", + "react-beautiful-dnd": "^12.2.0", "react-markdown": "^4.0.6" } } diff --git a/x-pack/legacy/plugins/siem/public/components/bytes/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/bytes/index.test.tsx index c46fd88b57190..2321b06c07cc0 100644 --- a/x-pack/legacy/plugins/siem/public/components/bytes/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/bytes/index.test.tsx @@ -4,17 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount } from 'enzyme'; import * as React from 'react'; import { TestProviders } from '../../mock'; import { PreferenceFormattedBytes } from '../formatted_bytes'; +import { useMountAppended } from '../../utils/use_mount_appended'; import { Bytes } from '.'; jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('Bytes', () => { + const mount = useMountAppended(); + test('it renders the expected formatted bytes', () => { const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.test.tsx index 0433176475e0a..b0c165fedfffc 100644 --- a/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.test.tsx @@ -4,14 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount } from 'enzyme'; import * as React from 'react'; import { TestProviders } from '../../mock'; +import { useMountAppended } from '../../utils/use_mount_appended'; import { CertificateFingerprint } from '.'; describe('CertificateFingerprint', () => { + const mount = useMountAppended(); test('renders the expected label', () => { const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx index 008ece5c7e69c..4b546bca1f72e 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; @@ -14,10 +14,13 @@ import { TestProviders } from '../../mock'; import { mockDataProviders } from '../timeline/data_providers/mock/mock_data_providers'; import { DragDropContextWrapper } from './drag_drop_context_wrapper'; import { DraggableWrapper } from './draggable_wrapper'; +import { useMountAppended } from '../../utils/use_mount_appended'; describe('DraggableWrapper', () => { const dataProvider = mockDataProviders[0]; const message = 'draggable wrapper content'; + const mount = useMountAppended(); + describe('rendering', () => { test('it renders against the snapshot', () => { const wrapper = shallow( diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx index 39abbdd4d4e38..056669673bb9e 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; @@ -14,8 +14,11 @@ import { TestProviders } from '../../mock'; import { DragDropContextWrapper } from './drag_drop_context_wrapper'; import { DroppableWrapper } from './droppable_wrapper'; +import { useMountAppended } from '../../utils/use_mount_appended'; describe('DroppableWrapper', () => { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders against the snapshot', () => { const message = 'draggable wrapper content'; diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx index d3dcba9526bdd..f1ed533bef545 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx @@ -7,10 +7,10 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TestProviders } from '../../mock'; import { getEmptyString } from '../empty_value'; +import { useMountAppended } from '../../utils/use_mount_appended'; import { DefaultDraggable, @@ -20,6 +20,8 @@ import { } from '.'; describe('draggables', () => { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders the default DefaultDraggable', () => { const wrapper = shallow( @@ -99,7 +101,7 @@ describe('draggables', () => { describe('DefaultDraggable', () => { test('it works with just an id, field, and value and is some value', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -122,7 +124,7 @@ describe('draggables', () => { }); test('it renders a tooltip with the field name if a tooltip is not explicitly provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -137,7 +139,7 @@ describe('draggables', () => { }); test('it renders the tooltipContent when a string is provided as content', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the tooltipContent when an element is provided as content', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it does NOT render a tooltip when tooltipContent is null', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { describe('DraggableBadge', () => { test('it works with just an id, field, and value and is the default', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns Empty string text if value is an empty string', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders a tooltip with the field name if a tooltip is not explicitly provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the tooltipContent when a string is provided as content', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the tooltipContent when an element is provided as content', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it does NOT render a tooltip when tooltipContent is null', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + test('it renders the expected formatted duration', () => { const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx index 5e1eae1649b41..929b4983b5fd7 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; import { FeatureProperty } from '../types'; @@ -12,6 +12,7 @@ import { getRenderedFieldValue, PointToolTipContentComponent } from './point_too import { TestProviders } from '../../../mock'; import { getEmptyStringTag } from '../../empty_value'; import { HostDetailsLink, IPDetailsLink } from '../../links'; +import { useMountAppended } from '../../../utils/use_mount_appended'; jest.mock('../../search_bar', () => ({ siemFilterManager: { @@ -20,6 +21,8 @@ jest.mock('../../search_bar', () => ({ })); describe('PointToolTipContent', () => { + const mount = useMountAppended(); + const mockFeatureProps: FeatureProperty[] = [ { _propertyKey: 'host.name', diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx index d8c0e46d8480b..f1e96392d6afc 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; @@ -14,10 +14,13 @@ import { TestProviders } from '../../mock/test_providers'; import { EventDetails } from './event_details'; import { mockBrowserFields } from '../../containers/source/mock'; import { defaultHeaders } from '../../mock/header'; +import { useMountAppended } from '../../utils/use_mount_appended'; jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('EventDetails', () => { + const mount = useMountAppended(); + describe('rendering', () => { test('should match snapshot', () => { const wrapper = shallow( diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx index f2fac81669d16..2c28ab8696f0e 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx @@ -5,7 +5,6 @@ */ import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { mockDetailItemData, mockDetailItemDataId } from '../../mock/mock_detail_item'; import { TestProviders } from '../../mock/test_providers'; @@ -13,14 +12,17 @@ import { TestProviders } from '../../mock/test_providers'; import { EventFieldsBrowser } from './event_fields_browser'; import { mockBrowserFields } from '../../containers/source/mock'; import { defaultHeaders } from '../../mock/header'; +import { useMountAppended } from '../../utils/use_mount_appended'; jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('EventFieldsBrowser', () => { + const mount = useMountAppended(); + describe('column headers', () => { ['Field', 'Value', 'Description'].forEach(header => { test(`it renders the ${header} column header`, () => { - const wrapper = mountWithIntl( + const wrapper = mount( { describe('filter input', () => { test('it renders a filter input with the expected placeholder', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { test('it renders an UNchecked checkbox for a field that is not a member of columnHeaders', () => { const field = 'agent.id'; - const wrapper = mountWithIntl( + const wrapper = mount( { test('it renders an checked checkbox for a field that is a member of columnHeaders', () => { const field = '@timestamp'; - const wrapper = mountWithIntl( + const wrapper = mount( { const field = '@timestamp'; const toggleColumn = jest.fn(); - const wrapper = mountWithIntl( + const wrapper = mount( { describe('field type icon', () => { test('it renders the expected icon type for the data provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { describe('field', () => { test('it renders the field name for the data provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { describe('value', () => { test('it renders the expected value for the data provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { describe('description', () => { test('it renders the expected field description the data provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + test('it renders the "Showing..." subtitle with the expected event count', async () => { const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx index d311394870683..e46153c18c2b5 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount } from 'enzyme'; import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; @@ -12,6 +11,7 @@ import { useKibanaCore } from '../../lib/compose/kibana_core'; import { wait } from '../../lib/helpers'; import { mockIndexPattern, TestProviders } from '../../mock'; import { mockUiSettings } from '../../mock/ui_settings'; +import { useMountAppended } from '../../utils/use_mount_appended'; import { mockEventViewerResponse } from './mock'; import { StatefulEventsViewer } from '.'; @@ -40,6 +40,8 @@ const from = 1566943856794; const to = 1566857456791; describe('StatefulEventsViewer', () => { + const mount = useMountAppended(); + test('it renders the events viewer', async () => { const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx b/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx index 2d69db82405ba..e45f5dacb36a2 100644 --- a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; import { FlowTarget, GetIpOverviewQuery, HostEcsFields } from '../../graphql/types'; import { TestProviders } from '../../mock'; @@ -25,10 +24,13 @@ import { MoreContainer, } from './field_renderers'; import { mockData } from '../page/network/ip_overview/mock'; +import { useMountAppended } from '../../utils/use_mount_appended'; type AutonomousSystem = GetIpOverviewQuery.AutonomousSystem; describe('Field Renderers', () => { + const mount = useMountAppended(); + describe('#locationRenderer', () => { test('it renders correctly against snapshot', () => { const wrapper = shallow( @@ -184,7 +186,7 @@ describe('Field Renderers', () => { describe('#whoisRenderer', () => { test('it renders correctly against snapshot', () => { - const wrapper = shallowWithIntl(whoisRenderer('10.10.10.10')); + const wrapper = shallow(whoisRenderer('10.10.10.10')); expect(toJson(wrapper)).toMatchSnapshot(); }); @@ -192,9 +194,7 @@ describe('Field Renderers', () => { describe('#reputationRenderer', () => { test('it renders correctly against snapshot', () => { - const wrapper = shallowWithIntl( - {reputationRenderer('10.10.10.10')} - ); + const wrapper = shallow({reputationRenderer('10.10.10.10')}); expect(toJson(wrapper.find('DragDropContext'))).toMatchSnapshot(); }); @@ -202,7 +202,7 @@ describe('Field Renderers', () => { describe('DefaultFieldRenderer', () => { test('it should render a single item', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -211,7 +211,7 @@ describe('Field Renderers', () => { }); test('it should render two items', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it should render all items when the item count exactly equals displayCount', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it should render all items up to displayCount and the expected "+ n More" popover anchor text for items greater than displayCount', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { const rowItems = ['item1', 'item2', 'item3', 'item4', 'item5', 'item6', 'item7']; test('it should only render the items after overflowIndexStart', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it should render all the items when overflowIndexStart is zero', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it should have the overflow `auto` style to enable scrolling when necessary', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it should use the moreMaxHeight prop as the value for the max-height style', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { test('it should only invoke the optional render function, when provided, for the items after overflowIndexStart', () => { const render = jest.fn(); - mountWithIntl( + mount( { const timelineId = 'test'; const selectedCategoryId = 'client'; + const mount = useMountAppended(); test('it renders the category id as the value of the title', () => { const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.test.tsx index a569fc42e550f..6034f5a476443 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.test.tsx @@ -5,7 +5,6 @@ */ import { omit } from 'lodash/fp'; -import { mount } from 'enzyme'; import * as React from 'react'; import { mockBrowserFields } from '../../containers/source/mock'; @@ -17,6 +16,7 @@ import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../timeline/body/helpers'; import { Category } from './category'; import { getFieldColumns, getFieldItems } from './field_items'; import { FIELDS_PANE_WIDTH } from './helpers'; +import { useMountAppended } from '../../utils/use_mount_appended'; const selectedCategoryId = 'base'; const selectedCategoryFields = mockBrowserFields[selectedCategoryId].fields; @@ -37,6 +37,7 @@ const columnHeaders: ColumnHeader[] = [ describe('field_items', () => { const timelineId = 'test'; + const mount = useMountAppended(); describe('getFieldItems', () => { Object.keys(selectedCategoryFields!).forEach(fieldId => { diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.test.tsx index 2193d0c661fb7..68ba2e2774314 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.test.tsx @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount } from 'enzyme'; import * as React from 'react'; import { mockBrowserFields } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; +import { useMountAppended } from '../../utils/use_mount_appended'; import { FIELDS_PANE_WIDTH } from './helpers'; import { FieldsPane } from './fields_pane'; @@ -16,6 +16,8 @@ import { FieldsPane } from './fields_pane'; const timelineId = 'test'; describe('FieldsPane', () => { + const mount = useMountAppended(); + test('it renders the selected category', () => { const selectedCategory = 'auditd'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx index 54847cda281f4..8a01a01b1daae 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx @@ -14,6 +14,19 @@ import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './helpers'; import { StatefulFieldsBrowserComponent } from '.'; +// Suppress warnings about "react-beautiful-dnd" until we migrate to @testing-library/react +/* eslint-disable no-console */ +const originalError = console.error; +const originalWarn = console.warn; +beforeAll(() => { + console.warn = jest.fn(); + console.error = jest.fn(); +}); +afterAll(() => { + console.error = originalError; + console.warn = originalWarn; +}); + describe('StatefulFieldsBrowser', () => { const timelineId = 'test'; diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx index c20f3c7185e66..5644a344f91d6 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx @@ -5,17 +5,20 @@ */ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import React from 'react'; import { TestProviders } from '../../mock'; import '../../mock/ui_settings'; import { HeaderPage } from './index'; +import { useMountAppended } from '../../utils/use_mount_appended'; jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('HeaderPage', () => { + const mount = useMountAppended(); + test('it renders', () => { const wrapper = shallow( { + const mount = useMountAppended(); + test('renders correctly against snapshot', () => { const wrapper = shallow( @@ -22,7 +24,7 @@ describe('Port', () => { }); test('it renders the the ip address', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -37,7 +39,7 @@ describe('Port', () => { }); test('it hyperlinks to the network/ip page', () => { - const wrapper = mountWithIntl( + const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.test.tsx index 2cb564aeed710..3842b7be67876 100644 --- a/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.test.tsx @@ -4,14 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount } from 'enzyme'; import * as React from 'react'; import { TestProviders } from '../../mock'; +import { useMountAppended } from '../../utils/use_mount_appended'; import { Ja3Fingerprint } from '.'; describe('Ja3Fingerprint', () => { + const mount = useMountAppended(); + test('renders the expected label', () => { const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/last_event_time/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/last_event_time/index.test.tsx index c23a757647a42..e2ada4682fdec 100644 --- a/x-pack/legacy/plugins/siem/public/components/last_event_time/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/last_event_time/index.test.tsx @@ -10,12 +10,12 @@ import { getEmptyValue } from '../empty_value'; import { LastEventIndexKey } from '../../graphql/types'; import { mockLastEventTimeQuery } from '../../containers/events/last_event_time/mock'; +import { useMountAppended } from '../../utils/use_mount_appended'; import { useLastEventTimeQuery } from '../../containers/events/last_event_time'; import { TestProviders } from '../../mock'; import '../../mock/ui_settings'; import { LastEventTime } from '.'; -import { mount } from 'enzyme'; const mockUseLastEventTimeQuery: jest.Mock = useLastEventTimeQuery as jest.Mock; jest.mock('../../containers/events/last_event_time', () => ({ @@ -23,6 +23,8 @@ jest.mock('../../containers/events/last_event_time', () => ({ })); describe('Last Event Time Stat', () => { + const mount = useMountAppended(); + beforeEach(() => { mockUseLastEventTimeQuery.mockReset(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx b/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx index c3268270919e2..80ccd07c30249 100644 --- a/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx @@ -10,7 +10,7 @@ import * as React from 'react'; import { MarkdownHintComponent } from './markdown_hint'; -describe.skip('MarkdownHintComponent ', () => { +describe('MarkdownHintComponent ', () => { test('it has inline visibility when show is true', () => { const wrapper = mount(); diff --git a/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.test.tsx index c401075af42ce..562e3c15675a7 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.test.tsx @@ -6,11 +6,14 @@ import React from 'react'; import toJson from 'enzyme-to-json'; -import { shallow, mount } from 'enzyme'; +import { shallow } from 'enzyme'; import { EntityDraggableComponent } from './entity_draggable'; import { TestProviders } from '../../mock/test_providers'; +import { useMountAppended } from '../../utils/use_mount_appended'; describe('entity_draggable', () => { + const mount = useMountAppended(); + test('renders correctly against snapshot', () => { const wrapper = shallow( { let anomalies: Anomalies = cloneDeep(mockAnomalies); + const mount = useMountAppended(); + beforeEach(() => { anomalies = cloneDeep(mockAnomalies); }); diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx index 5bd11169e4840..f01df38138456 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash/fp'; import * as React from 'react'; @@ -13,6 +13,7 @@ import { mockAnomalies } from '../mock'; import { TestProviders } from '../../../mock/test_providers'; import { getEmptyValue } from '../../empty_value'; import { Anomalies } from '../types'; +import { useMountAppended } from '../../../utils/use_mount_appended'; const endDate: number = new Date('3000-01-01T00:00:00.000Z').valueOf(); const narrowDateRange = jest.fn(); @@ -21,6 +22,7 @@ jest.mock('../../../lib/settings/use_kibana_ui_setting'); describe('anomaly_scores', () => { let anomalies: Anomalies = cloneDeep(mockAnomalies); + const mount = useMountAppended(); beforeEach(() => { anomalies = cloneDeep(mockAnomalies); diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx index 04fed8e4fff3f..80980756d2130 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx @@ -10,8 +10,8 @@ import * as i18n from './translations'; import { AnomaliesByHost, Anomaly } from '../types'; import { Columns } from '../../paginated_table'; import { TestProviders } from '../../../mock'; -import { mount } from 'enzyme'; import React from 'react'; +import { useMountAppended } from '../../../utils/use_mount_appended'; const startDate = new Date(2001).valueOf(); const endDate = new Date(3000).valueOf(); @@ -19,6 +19,8 @@ const interval = 'days'; const narrowDateRange = jest.fn(); describe('get_anomalies_host_table_columns', () => { + const mount = useMountAppended(); + test('on hosts page, we expect to get all columns', () => { expect( getAnomaliesHostTableColumnsCurated( diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx index 768c7af8f4b2c..b27ccaf1ca7de 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx @@ -9,9 +9,9 @@ import { NetworkType } from '../../../store/network/model'; import * as i18n from './translations'; import { AnomaliesByNetwork, Anomaly } from '../types'; import { Columns } from '../../paginated_table'; -import { mount } from 'enzyme'; import React from 'react'; import { TestProviders } from '../../../mock'; +import { useMountAppended } from '../../../utils/use_mount_appended'; const startDate = new Date(2001).valueOf(); const endDate = new Date(3000).valueOf(); @@ -19,6 +19,8 @@ const interval = 'days'; const narrowDateRange = jest.fn(); describe('get_anomalies_network_table_columns', () => { + const mount = useMountAppended(); + test('on network page, we expect to get all columns', () => { expect( getAnomaliesNetworkTableColumnsCurated( diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.test.tsx index 4ea9e0cdafacb..1a8360fe82c58 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.test.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount } from 'enzyme'; import * as React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; + import { MlPopover } from './ml_popover'; jest.mock('../../lib/settings/use_kibana_ui_setting'); @@ -16,7 +17,7 @@ jest.mock('../ml/permissions/has_ml_admin_permissions', () => ({ describe('MlPopover', () => { test('shows upgrade popover on mouse click', () => { - const wrapper = mount(); + const wrapper = mountWithIntl(); // TODO: Update to use act() https://fb.me/react-wrap-tests-with-act wrapper diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/netflow/index.test.tsx index 2c5152535a370..2d8c201e41462 100644 --- a/x-pack/legacy/plugins/siem/public/components/netflow/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/netflow/index.test.tsx @@ -8,7 +8,6 @@ import toJson from 'enzyme-to-json'; import { get } from 'lodash/fp'; import * as React from 'react'; import { shallow } from 'enzyme'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { asArrayIfExists } from '../../lib/helpers'; import { getMockNetflowData } from '../../mock'; @@ -56,6 +55,7 @@ import { NETWORK_PROTOCOL_FIELD_NAME, NETWORK_TRANSPORT_FIELD_NAME, } from '../source_destination/field_names'; +import { useMountAppended } from '../../utils/use_mount_appended'; jest.mock('../../lib/settings/use_kibana_ui_setting'); @@ -121,13 +121,15 @@ const getNetflowInstance = () => ( ); describe('Netflow', () => { + const mount = useMountAppended(); + test('renders correctly against snapshot', () => { const wrapper = shallow(getNetflowInstance()); expect(toJson(wrapper)).toMatchSnapshot(); }); test('it renders a destination label', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -138,7 +140,7 @@ describe('Netflow', () => { }); test('it renders destination.bytes', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -149,7 +151,7 @@ describe('Netflow', () => { }); test('it renders destination.geo.continent_name', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -160,7 +162,7 @@ describe('Netflow', () => { }); test('it renders destination.geo.country_name', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -171,7 +173,7 @@ describe('Netflow', () => { }); test('it renders destination.geo.country_iso_code', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -182,7 +184,7 @@ describe('Netflow', () => { }); test('it renders destination.geo.region_name', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -193,7 +195,7 @@ describe('Netflow', () => { }); test('it renders destination.geo.city_name', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -204,7 +206,7 @@ describe('Netflow', () => { }); test('it renders the destination ip and port, separated with a colon', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -215,7 +217,7 @@ describe('Netflow', () => { }); test('it renders destination.packets', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -226,7 +228,7 @@ describe('Netflow', () => { }); test('it hyperlinks links destination.port to an external service that describes the purpose of the port', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -240,7 +242,7 @@ describe('Netflow', () => { }); test('it renders event.duration', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -251,7 +253,7 @@ describe('Netflow', () => { }); test('it renders event.end', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -262,7 +264,7 @@ describe('Netflow', () => { }); test('it renders event.start', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -273,7 +275,7 @@ describe('Netflow', () => { }); test('it renders network.bytes', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -284,7 +286,7 @@ describe('Netflow', () => { }); test('it renders network.community_id', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -295,7 +297,7 @@ describe('Netflow', () => { }); test('it renders network.direction', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -306,7 +308,7 @@ describe('Netflow', () => { }); test('it renders network.packets', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -317,7 +319,7 @@ describe('Netflow', () => { }); test('it renders network.protocol', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -328,7 +330,7 @@ describe('Netflow', () => { }); test('it renders process.name', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -339,7 +341,7 @@ describe('Netflow', () => { }); test('it renders a source label', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -350,7 +352,7 @@ describe('Netflow', () => { }); test('it renders source.bytes', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -361,7 +363,7 @@ describe('Netflow', () => { }); test('it renders source.geo.continent_name', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -372,7 +374,7 @@ describe('Netflow', () => { }); test('it renders source.geo.country_name', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -383,7 +385,7 @@ describe('Netflow', () => { }); test('it renders source.geo.country_iso_code', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -394,7 +396,7 @@ describe('Netflow', () => { }); test('it renders source.geo.region_name', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -405,7 +407,7 @@ describe('Netflow', () => { }); test('it renders source.geo.city_name', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -416,7 +418,7 @@ describe('Netflow', () => { }); test('it renders the source ip and port, separated with a colon', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -427,7 +429,7 @@ describe('Netflow', () => { }); test('it renders source.packets', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -438,7 +440,7 @@ describe('Netflow', () => { }); test('it hyperlinks tls.client_certificate.fingerprint.sha1 site to compare the fingerprint against a known set of signatures', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -452,7 +454,7 @@ describe('Netflow', () => { }); test('renders tls.client_certificate.fingerprint.sha1 text', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -464,7 +466,7 @@ describe('Netflow', () => { }); test('it hyperlinks tls.fingerprints.ja3.hash site to compare the fingerprint against a known set of signatures', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -475,7 +477,7 @@ describe('Netflow', () => { }); test('renders tls.fingerprints.ja3.hash text', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -486,7 +488,7 @@ describe('Netflow', () => { }); test('it hyperlinks tls.server_certificate.fingerprint.sha1 site to compare the fingerprint against a known set of signatures', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -500,7 +502,7 @@ describe('Netflow', () => { }); test('renders tls.server_certificate.fingerprint.sha1 text', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -512,7 +514,7 @@ describe('Netflow', () => { }); test('it renders network.transport', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -523,7 +525,7 @@ describe('Netflow', () => { }); test('it renders user.name', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx index 234d5ac959c8c..6c3ab04849236 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx @@ -7,7 +7,7 @@ import { cloneDeep } from 'lodash/fp'; import * as React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; -import { render } from '@testing-library/react'; +import { render, act } from '@testing-library/react'; import { mockFirstLastSeenHostQuery } from '../../../../containers/hosts/first_last_seen/mock'; import { wait } from '../../../../lib/helpers'; @@ -18,6 +18,16 @@ import { FirstLastSeenHost, FirstLastSeenHostType } from '.'; jest.mock('../../../../lib/settings/use_kibana_ui_setting'); +// Suppress warnings about "react-apollo" until we migrate to apollo@3 +/* eslint-disable no-console */ +const originalError = console.error; +beforeAll(() => { + console.error = jest.fn(); +}); +afterAll(() => { + console.error = originalError; +}); + describe('FirstLastSeen Component', () => { const firstSeen = 'Apr 8, 2019 @ 16:09:40.692'; const lastSeen = 'Apr 8, 2019 @ 18:35:45.064'; @@ -44,7 +54,7 @@ describe('FirstLastSeen Component', () => { ); - await wait(); + await act(() => wait()); expect(container.innerHTML).toBe( `
${firstSeen}
` @@ -59,7 +69,7 @@ describe('FirstLastSeen Component', () => {
); - await wait(); + await act(() => wait()); expect(container.innerHTML).toBe( `
${lastSeen}
` ); @@ -76,7 +86,7 @@ describe('FirstLastSeen Component', () => {
); - await wait(); + await act(() => wait()); expect(container.innerHTML).toBe( `
${lastSeen}
` @@ -94,7 +104,7 @@ describe('FirstLastSeen Component', () => {
); - await wait(); + await act(() => wait()); expect(container.innerHTML).toBe( `
${firstSeen}
` @@ -111,7 +121,7 @@ describe('FirstLastSeen Component', () => {
); - await wait(); + await act(() => wait()); expect(container.textContent).toBe('something-invalid'); }); @@ -125,7 +135,7 @@ describe('FirstLastSeen Component', () => {
); - await wait(); + await act(() => wait()); expect(container.textContent).toBe('something-invalid'); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx index c9fdce94f780a..8c27f86d78884 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; import * as React from 'react'; @@ -16,6 +16,7 @@ import { mockGlobalState, TestProviders, } from '../../../../mock'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { mockUiSettings } from '../../../../mock/ui_settings'; import { useKibanaCore } from '../../../../lib/compose/kibana_core'; import { createStore, hostsModel, State } from '../../../../store'; @@ -43,6 +44,7 @@ describe('Hosts Table', () => { const state: State = mockGlobalState; let store = createStore(state, apolloClientObservable); + const mount = useMountAppended(); beforeEach(() => { store = createStore(state, apolloClientObservable); @@ -71,28 +73,7 @@ describe('Hosts Table', () => { }); describe('Sorting on Table', () => { - let wrapper = mount( - - - - - - ); + let wrapper: ReturnType; beforeEach(() => { wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.test.tsx index 10d9eb618c766..28ddb1df12c3a 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; import * as React from 'react'; @@ -12,6 +12,7 @@ import * as React from 'react'; import { TestProviders } from '../../../../mock'; import { hostsModel } from '../../../../store'; import { getEmptyValue } from '../../../empty_value'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { getArgs, UncommonProcessTable, getUncommonColumnsCurated } from '.'; import { mockData } from './mock'; @@ -20,6 +21,7 @@ import * as i18n from './translations'; describe('Uncommon Process Table Component', () => { const loadPage = jest.fn(); + const mount = useMountAppended(); describe('rendering', () => { test('it renders the default Uncommon process table', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx index 8bf338d17c47b..0537b95ca6cf7 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; import * as React from 'react'; @@ -13,6 +13,7 @@ import { Provider as ReduxStoreProvider } from 'react-redux'; import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../../mock'; import { createStore, networkModel, State } from '../../../../store'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { NetworkDnsTable } from '.'; import { mockData } from './mock'; @@ -22,8 +23,8 @@ jest.mock('../../../../lib/settings/use_kibana_ui_setting'); describe('NetworkTopNFlow Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; - let store = createStore(state, apolloClientObservable); + const mount = useMountAppended(); beforeEach(() => { store = createStore(state, apolloClientObservable); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx index c92661a909a6e..50d64817f81f8 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; import * as React from 'react'; @@ -12,6 +12,7 @@ import { MockedProvider } from 'react-apollo/test-utils'; import { Provider as ReduxStoreProvider } from 'react-redux'; import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../../mock'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { createStore, networkModel, State } from '../../../../store'; import { NetworkHttpTable } from '.'; @@ -24,6 +25,7 @@ describe('NetworkHttp Table Component', () => { const state: State = mockGlobalState; let store = createStore(state, apolloClientObservable); + const mount = useMountAppended(); beforeEach(() => { store = createStore(state, apolloClientObservable); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx index ca7a3c0bb4387..eb4179a040431 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; import * as React from 'react'; @@ -18,14 +18,17 @@ import { mockIndexPattern, TestProviders, } from '../../../../mock'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { createStore, networkModel, State } from '../../../../store'; import { NetworkTopCountriesTable } from '.'; import { mockData } from './mock'; + jest.mock('../../../../lib/settings/use_kibana_ui_setting'); describe('NetworkTopCountries Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; + const mount = useMountAppended(); let store = createStore(state, apolloClientObservable); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx index 884825422beb0..3157847b32376 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; import * as React from 'react'; @@ -18,16 +18,20 @@ import { mockIndexPattern, TestProviders, } from '../../../../mock'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { createStore, networkModel, State } from '../../../../store'; import { NetworkTopNFlowTable } from '.'; import { mockData } from './mock'; + jest.mock('../../../../lib/settings/use_kibana_ui_setting'); + describe('NetworkTopNFlow Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; let store = createStore(state, apolloClientObservable); + const mount = useMountAppended(); beforeEach(() => { store = createStore(state, apolloClientObservable); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx index 8c397053380c5..4313c455a0df1 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; import * as React from 'react'; @@ -12,6 +12,7 @@ import { MockedProvider } from 'react-apollo/test-utils'; import { Provider as ReduxStoreProvider } from 'react-redux'; import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../../mock'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { createStore, networkModel, State } from '../../../../store'; import { TlsTable } from '.'; @@ -24,6 +25,7 @@ describe('Tls Table Component', () => { const state: State = mockGlobalState; let store = createStore(state, apolloClientObservable); + const mount = useMountAppended(); beforeEach(() => { store = createStore(state, apolloClientObservable); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx index d178164fd3fd7..d6b9ec24de0aa 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; import * as React from 'react'; @@ -13,6 +13,7 @@ import { Provider as ReduxStoreProvider } from 'react-redux'; import { FlowTarget } from '../../../../graphql/types'; import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../../mock'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { createStore, networkModel, State } from '../../../../store'; import { UsersTable } from '.'; @@ -31,6 +32,7 @@ describe('Users Table Component', () => { const state: State = mockGlobalState; let store = createStore(state, apolloClientObservable); + const mount = useMountAppended(); beforeEach(() => { store = createStore(state, apolloClientObservable); diff --git a/x-pack/legacy/plugins/siem/public/components/port/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/port/index.test.tsx index 3e4db72cf55a1..330385e39ca79 100644 --- a/x-pack/legacy/plugins/siem/public/components/port/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/port/index.test.tsx @@ -7,13 +7,15 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TestProviders } from '../../mock/test_providers'; +import { useMountAppended } from '../../utils/use_mount_appended'; import { Port } from '.'; describe('Port', () => { + const mount = useMountAppended(); + test('renders correctly against snapshot', () => { const wrapper = shallow( @@ -22,7 +24,7 @@ describe('Port', () => { }); test('it renders the port', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -37,7 +39,7 @@ describe('Port', () => { }); test('it hyperlinks links destination.port to an external service that describes the purpose of the port', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -54,7 +56,7 @@ describe('Port', () => { }); test('it renders an external link', () => { - const wrapper = mountWithIntl( + const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/source_destination/index.test.tsx index edce246aa61bd..1679795951a56 100644 --- a/x-pack/legacy/plugins/siem/public/components/source_destination/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/source_destination/index.test.tsx @@ -5,7 +5,7 @@ */ import numeral from '@elastic/numeral'; -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { get } from 'lodash/fp'; import * as React from 'react'; @@ -14,6 +14,7 @@ import { asArrayIfExists } from '../../lib/helpers'; import { getMockNetflowData } from '../../mock'; import { TestProviders } from '../../mock/test_providers'; import { ID_FIELD_NAME } from '../event_details/event_id'; +import { useMountAppended } from '../../utils/use_mount_appended'; import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../ip'; import { DESTINATION_PORT_FIELD_NAME, SOURCE_PORT_FIELD_NAME } from '../port'; import { @@ -98,6 +99,8 @@ const getSourceDestinationInstance = () => ( ); describe('SourceDestination', () => { + const mount = useMountAppended(); + test('renders correctly against snapshot', () => { const wrapper = shallow(
{getSourceDestinationInstance()}
); expect(toJson(wrapper)).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.test.tsx b/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.test.tsx index c9fff7328165c..d42d34c85a4da 100644 --- a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount } from 'enzyme'; import { get } from 'lodash/fp'; import * as React from 'react'; @@ -15,6 +14,7 @@ import { ID_FIELD_NAME } from '../event_details/event_id'; import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../ip'; import { DESTINATION_PORT_FIELD_NAME, SOURCE_PORT_FIELD_NAME } from '../port'; import * as i18n from '../timeline/body/renderers/translations'; +import { useMountAppended } from '../../utils/use_mount_appended'; import { getPorts, @@ -38,6 +38,8 @@ import { jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('SourceDestinationIp', () => { + const mount = useMountAppended(); + describe('#isIpFieldPopulated', () => { test('it returns true when type is `source` and sourceIp has an IP address', () => { expect( diff --git a/x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx index 9d9087d34a765..d864d4306b8ef 100644 --- a/x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx @@ -11,13 +11,15 @@ import { OverflowFieldComponent, } from './helpers'; import * as React from 'react'; -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { TestProviders } from '../../mock'; import { getEmptyValue } from '../empty_value'; +import { useMountAppended } from '../../utils/use_mount_appended'; describe('Table Helpers', () => { const items = ['item1', 'item2', 'item3']; + const mount = useMountAppended(); describe('#getRowItemDraggable', () => { test('it returns correctly against snapshot', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx index 370f864f51f3c..b537499739d58 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; @@ -14,6 +14,7 @@ import { Direction } from '../../../../graphql/types'; import { mockBrowserFields } from '../../../../../public/containers/source/mock'; import { Sort } from '../sort'; import { TestProviders } from '../../../../mock/test_providers'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { ColumnHeadersComponent } from '.'; @@ -26,6 +27,8 @@ jest.mock('../../../resize_handle/is_resizing', () => ({ })); describe('ColumnHeaders', () => { + const mount = useMountAppended(); + describe('rendering', () => { const sort: Sort = { columnId: 'fooColumn', diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx index 0fb4c4f375684..a4ed5571bb0da 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, ReactWrapper } from 'enzyme'; import * as React from 'react'; import { mockBrowserFields } from '../../../containers/source/mock'; @@ -16,6 +15,7 @@ import { Body, BodyProps } from '.'; import { columnRenderers, rowRenderers } from './renderers'; import { Sort } from './sort'; import { wait } from '../../../lib/helpers'; +import { useMountAppended } from '../../../utils/use_mount_appended'; jest.mock('../../../lib/settings/use_kibana_ui_setting'); @@ -40,6 +40,8 @@ jest.mock('../../../lib/helpers/scheduler', () => ({ })); describe('Body', () => { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders the column headers', () => { const wrapper = mount( @@ -205,7 +207,7 @@ describe('Body', () => { const dispatchAddNoteToEvent = jest.fn(); const dispatchOnPinEvent = jest.fn(); - const addaNoteToEvent = (wrapper: ReactWrapper, note: string) => { + const addaNoteToEvent = (wrapper: ReturnType, note: string) => { wrapper .find('[data-test-subj="add-note"]') .first() diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx index dbf6db6cd2bd9..ad904554e33ad 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx @@ -7,12 +7,14 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { TestProviders } from '../../../../mock'; import { ArgsComponent } from './args'; describe('Args', () => { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders against shallow snapshot', () => { const wrapper = shallow( @@ -27,7 +29,7 @@ describe('Args', () => { }); test('it returns an empty string when both args and process title are undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns an empty string when both args and process title are null', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns an empty string when args is an empty array, and title is an empty string', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -64,7 +66,7 @@ describe('Args', () => { }); test('it returns args when args are provided, and process title is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns process title when process title is provided, and args is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns both args and process title, when both are provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders the default AuditAcquiredCredsDetails', () => { // I cannot and do not want to use BrowserFields for the mocks for the snapshot tests as they are too heavy @@ -32,7 +34,7 @@ describe('GenericDetails', () => { }); test('it returns auditd if the data does contain auditd data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { describe('#AuditdConnectedToLine', () => { test('it returns pretty output if you send in all your happy path data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with username if username, primary, and secondary all equal each other ', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with username if primary and secondary equal unset', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with username if primary and secondary equal unset with different casing', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with username if primary and secondary are undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with "as" wording if username, primary, and secondary are all different', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with "as" wording if username and primary are the same but secondary is different', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with primary if username and secondary are unset with different casing', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with primary if username and secondary are undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns just a session if only given an id', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns only session and hostName if only hostname and an id is given', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns only a session and user name if only a user name and id is given', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns only a process name if only given a process name and id', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns session, user name, and process title if process title with id is given', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns only a working directory if that is all that is given with a id', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns only the session and args with id if that is all that is given (very unlikely situation)', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders the default GenericFileDetails', () => { // I cannot and do not want to use BrowserFields for the mocks for the snapshot tests as they are too heavy @@ -33,7 +35,7 @@ describe('GenericFileDetails', () => { }); test('it returns auditd if the data does contain auditd data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { describe('#AuditdGenericFileLine', () => { test('it returns pretty output if you send in all your happy path data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with username if username, primary, and secondary all equal each other ', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with username if primary and secondary equal unset', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with username if primary and secondary equal unset with different casing', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with username if primary and secondary are undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with "as" wording if username, primary, and secondary are all different', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with "as" wording if username and primary are the same but secondary is different', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with primary if username and secondary are unset with different casing', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with primary if username and secondary are undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns just session if only session id is given', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns only session and hostName if only hostname and an id is given', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns only a session and user name if only a user name and id is given', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns only a process name if only given a process name and id', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns session user name and title if process title with id is given', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns only a working directory if that is all that is given with a id', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns only the session and args with id if that is all that is given (very unlikely situation)', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + describe('#createGenericAuditRowRenderer', () => { let nonAuditd: Ecs; let auditd: Ecs; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx index b8da9d50402bf..a5b861be08e56 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx @@ -7,12 +7,14 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TestProviders } from '../../../../../mock'; import { PrimarySecondaryUserInfo, nilOrUnSet } from './primary_secondary_user_info'; +import { useMountAppended } from '../../../../../utils/use_mount_appended'; describe('UserPrimarySecondary', () => { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders the default PrimarySecondaryUserInfo', () => { const wrapper = shallow( @@ -28,7 +30,7 @@ describe('UserPrimarySecondary', () => { }); test('should render user name only if that is all that is present', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('should render user name only if the others are in unset mode', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('should render primary name only if that is all that is present', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('should render primary name only if the others are in unset mode', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('should render the secondary name only if that is all that is present', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('should render the secondary name only if the others are in unset mode', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('should render the user name if all three are the same', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('should render the primary with as if all three are different', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders the default SessionUserHostWorkingDir', () => { const wrapper = shallow( @@ -34,7 +36,7 @@ describe('SessionUserHostWorkingDir', () => { }); test('it renders with just eventId and contextId', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders with only eventId, contextId, session', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders with only eventId, contextId, session, hostName', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders with only eventId, contextId, session, hostName, userName', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders with only eventId, contextId, session, hostName, userName, primary', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders with only eventId, contextId, session, hostName, userName, primary, secondary', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders with everything as expected', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + test('it renders the expected text given an Endgame DNS request_event', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + test('it renders the expected text when all properties are provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when dnsQuestionName is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when dnsQuestionType is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when dnsResolvedIp is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when dnsResponseCode is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when eventCode is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when hostName is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when processExecutable is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when processName is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when processPid is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when userDomain is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when userName is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when winlogEventId is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when both eventCode and winlogEventId are NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { let mockDatum: TimelineNonEcsData[]; const _id = mockTimelineData[0]._id; + const mount = useMountAppended(); + beforeEach(() => { mockDatum = cloneDeep(mockTimelineData[0].data); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx index e1da17ad2904b..77569f07a23c2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx @@ -10,7 +10,6 @@ */ import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TestProviders } from '../../../../../mock'; import { mockBrowserFields } from '../../../../../../public/containers/source/mock'; @@ -20,12 +19,15 @@ import { mockEndgameUserLogon, mockEndgameUserLogoff, } from '../../../../../../public/mock/mock_endgame_ecs_data'; +import { useMountAppended } from '../../../../../utils/use_mount_appended'; import { EndgameSecurityEventDetails } from './endgame_security_event_details'; describe('EndgameSecurityEventDetails', () => { + const mount = useMountAppended(); + test('it renders the expected text given an Endgame Security user_logon event', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text given an Endgame Security admin_logon event', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text given an Endgame Security explicit_user_logon event', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text given an Endgame Security user_logoff event', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + test('it renders the expected text when all properties are provided and event action is admin_logon', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when all properties are provided and event action is explicit_user_logon', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when endgameLogonType is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when endgameSubjectDomainName is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when endgameSubjectLogonId is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when when endgameSubjectUserName is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when endgameTargetDomainName is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when endgameTargetLogonId is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when endgameTargetUserName is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when eventAction is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when eventCode is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when hostName is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when processExecutable is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when processName is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when processPid is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when userDomain is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when userName is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when winlogEventId is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when BOTH eventCode and winlogEventId are NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + test('it renders the expected text and exit code, when both text and an endgameExitCode are provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -22,7 +24,7 @@ describe('ExitCodeDraggable', () => { }); test('it returns an empty string when text is provided, but endgameExitCode is undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns an empty string when text is provided, but endgameExitCode is null', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns an empty string when text is provided, but endgameExitCode is an empty string', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -59,7 +61,7 @@ describe('ExitCodeDraggable', () => { }); test('it renders just the exit code when text is undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -68,7 +70,7 @@ describe('ExitCodeDraggable', () => { }); test('it renders just the exit code when text is null', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -77,7 +79,7 @@ describe('ExitCodeDraggable', () => { }); test('it renders just the exit code when text is an empty string', () => { - const wrapper = mountWithIntl( + const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx index 80be9fd339f51..ff63d02acc37c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx @@ -5,15 +5,17 @@ */ import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TestProviders } from '../../../../mock'; import { FileDraggable } from './file_draggable'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; describe('FileDraggable', () => { + const mount = useMountAppended(); + test('it prefers fileName and filePath over endgameFileName and endgameFilePath when all of them are provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns an empty string when none of the files or paths are provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders just the endgameFileName if only endgameFileName is provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders "in endgameFilePath" if only endgameFilePath is provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders just the filename if only fileName is provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders "in filePath" if only filePath is provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { const theme = () => ({ eui: euiDarkVars, darkMode: true }); + const mount = useMountAppended(); test('renders correctly against snapshot', () => { const wrapper = shallow( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.test.tsx index 1b22d318e19cd..d445ec2859e2c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash/fp'; import * as React from 'react'; @@ -18,10 +18,13 @@ import { defaultHeaders } from '../column_headers/default_headers'; import { columnRenderers } from '.'; import { getColumnRenderer } from './get_column_renderer'; import { getValues, findItem, deleteItemIdx } from './helpers'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; describe('get_column_renderer', () => { let nonSuricata: TimelineNonEcsData[]; const _id = mockTimelineData[0]._id; + const mount = useMountAppended(); + beforeEach(() => { nonSuricata = cloneDeep(mockTimelineData[0].data); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx index f7a5462be6e8f..bea525116021d 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash'; import * as React from 'react'; @@ -13,6 +13,7 @@ import { mockBrowserFields } from '../../../../containers/source/mock'; import { Ecs } from '../../../../graphql/types'; import { mockTimelineData } from '../../../../mock'; import { TestProviders } from '../../../../mock/test_providers'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { rowRenderers } from '.'; import { getRowRenderer } from './get_row_renderer'; @@ -23,6 +24,8 @@ describe('get_column_renderer', () => { let zeek: Ecs; let system: Ecs; let auditd: Ecs; + const mount = useMountAppended(); + beforeEach(() => { nonSuricata = cloneDeep(mockTimelineData[0].ecs); suricata = cloneDeep(mockTimelineData[2].ecs); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.test.tsx index 449ed37dbeb30..6c58b1ec6f35c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.test.tsx @@ -4,15 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; - -import * as React from 'react'; +import React from 'react'; import { mockTimelineData, TestProviders } from '../../../../mock'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { HostWorkingDir } from './host_working_dir'; describe('HostWorkingDir', () => { + const mount = useMountAppended(); + test('renders correctly against snapshot', () => { const wrapper = shallow( { + const mount = useMountAppended(); + test('renders correctly against snapshot', () => { const browserFields: BrowserFields = {}; const children = netflowRowRenderer.renderRow({ diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx index 9f6a8676694e4..80ae10a48415c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx @@ -5,15 +5,17 @@ */ import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TestProviders } from '../../../../mock'; import { ParentProcessDraggable } from './parent_process_draggable'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; describe('ParentProcessDraggable', () => { + const mount = useMountAppended(); + test('displays the text, endgameParentProcessName, and processPpid when they are all provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('displays nothing when the text is provided, but endgameParentProcessName and processPpid are both undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('displays the text and processPpid when endgameParentProcessName is undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('displays the processPpid when both endgameParentProcessName and text are undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('displays the text and endgameParentProcessName when processPpid is undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('displays the endgameParentProcessName when both processPpid and text are undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + describe('rendering', () => { let mockDatum: TimelineNonEcsData[]; const _id = mockTimelineData[0]._id; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx index b087351107c77..ff1cb60db0d93 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx @@ -7,12 +7,14 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TestProviders } from '../../../../mock'; import { ProcessDraggable, ProcessDraggableWithNonExistentProcess } from './process_draggable'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; describe('ProcessDraggable', () => { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders against shallow snapshot', () => { const wrapper = shallow( @@ -60,7 +62,7 @@ describe('ProcessDraggable', () => { }); test('it returns process name if that is all that is passed in', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns process executable if that is all that is passed in', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns process pid if that is all that is passed in', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns just process name if process.pid and endgame.pid are NaN', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns just process executable if process.pid and endgame.pid are NaN', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns process executable if everything else is an empty string or NaN', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns endgame.process_name if everything else is an empty string or NaN', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns endgame.process_name and endgame.pid if everything else is an empty string or undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns process pid if everything else is an empty string', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns endgame.pid if everything else is an empty string', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns pid and process name if everything is filled', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns process pid and executable and if process name and endgame process name are null', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns endgame pid and executable and if process name and endgame process name are null', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns process pid and executable and if process name is undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns process pid and executable if process name is an empty string', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it prefers process.name when process.executable and endgame.process_name are also provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it falls back to rendering process.executable when process.name is NOT provided, but process.executable and endgame.process_name are provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it falls back to rendering endgame.process_name when process.name and process.executable are NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it prefers process.pid when endgame.pid is also provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it falls back to rendering endgame.pid when process.pid is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); describe('ProcessDraggableWithNonExistentProcess', () => { + const mount = useMountAppended(); + test('it renders the expected text when all fields are undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when just endgamePid is provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when just endgameProcessName is provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when just processExecutable is provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when just processName is provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when just processPid is provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when all values are provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + test('displays the processHashMd5, processHashSha1, and processHashSha256 when they are all provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('displays nothing when processHashMd5, processHashSha1, and processHashSha256 are all undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('displays just processHashMd5 when the other hashes are undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('displays just processHashSha1 when the other hashes are undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('displays just processHashSha256 when the other hashes are undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx index 20b64661b6a00..3f77726474c56 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx @@ -7,14 +7,16 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { mockBrowserFields } from '../../../../../containers/source/mock'; import { mockTimelineData } from '../../../../../mock'; import { TestProviders } from '../../../../../mock/test_providers'; +import { useMountAppended } from '../../../../../utils/use_mount_appended'; import { SuricataDetails } from './suricata_details'; describe('SuricataDetails', () => { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders the default SuricataDetails', () => { const wrapper = shallow( @@ -28,7 +30,7 @@ describe('SuricataDetails', () => { }); test('it returns text if the data does contain suricata data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); let nonSuricata: Ecs; let suricata: Ecs; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx index 2278ed135e5e2..4eefb4b0bc8b9 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx @@ -7,9 +7,9 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TestProviders } from '../../../../../mock'; +import { useMountAppended } from '../../../../../utils/use_mount_appended'; import { SuricataSignature, Tokens, @@ -18,6 +18,8 @@ import { } from './suricata_signature'; describe('SuricataSignature', () => { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders the default SuricataSignature', () => { const wrapper = shallow( @@ -39,7 +41,7 @@ describe('SuricataSignature', () => { }); test('should render a single if it is present', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
@@ -48,7 +50,7 @@ describe('SuricataSignature', () => { }); test('should render the multiple tokens if they are present', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
@@ -59,7 +61,7 @@ describe('SuricataSignature', () => { describe('DraggableSignatureId', () => { test('it renders the default SuricataSignature', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -68,7 +70,7 @@ describe('SuricataSignature', () => { }); test('it renders a tooltip for the signature field', () => { - const wrapper = mountWithIntl( + const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx index b85bef4d5ac36..632e8ff35950e 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx @@ -115,7 +115,6 @@ export const SuricataSignature = React.memo<{ data-test-subj="draggable-signature-link" field={SURICATA_SIGNATURE_FIELD_NAME} id={`suricata-signature-default-draggable-${contextId}-${id}-${SURICATA_SIGNATURE_FIELD_NAME}`} - name={name} value={signature} >
diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.test.tsx index ee58111dd5709..54f5b2f165287 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.test.tsx @@ -7,14 +7,16 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { BrowserFields } from '../../../../../containers/source'; import { mockBrowserFields } from '../../../../../containers/source/mock'; import { mockTimelineData, TestProviders } from '../../../../../mock'; import { SystemGenericDetails, SystemGenericLine } from './generic_details'; +import { useMountAppended } from '../../../../../utils/use_mount_appended'; describe('SystemGenericDetails', () => { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders the default SystemGenericDetails', () => { // I cannot and do not want to use BrowserFields for the mocks for the snapshot tests as they are too heavy @@ -32,7 +34,7 @@ describe('SystemGenericDetails', () => { }); test('it returns system rendering if the data does contain system data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { describe('#SystemGenericLine', () => { test('it returns pretty output if you send in all your happy path data', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns nothing if data is all null', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return only the host name', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature, text', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature, text, userDomain, username', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ + const mount = useMountAppended(); + describe('rendering', () => { test('it renders the default SystemGenericDetails', () => { // I cannot and do not want to use BrowserFields for the mocks for the snapshot tests as they are too heavy @@ -32,7 +34,7 @@ describe('SystemGenericFileDetails', () => { }); test('it returns system rendering if the data does contain system data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { describe('#SystemGenericFileLine', () => { test('it returns pretty output if you send in all your happy path data', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns nothing if data is all null', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return only the host name', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory, process-title', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory, process-title, args', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it renders a FileDraggable when endgameFileName and endgameFilePath are provided, but fileName and filePath are NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it prefers to render fileName and filePath over endgameFileName and endgameFilePath respectfully when all of those fields are provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ ['file_create_event', 'created', 'file_delete_event', 'deleted'].forEach(eventAction => { test(`it renders the text "via" when eventAction is ${eventAction}`, () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ test('it does NOT render the text "via" when eventAction is not a whitelisted action', () => { const eventAction = 'a_non_whitelisted_event_action'; - const wrapper = mountWithIntl( + const wrapper = mount(
{ test('it renders a ParentProcessDraggable when eventAction is NOT "process_stopped" and NOT "termination_event"', () => { const eventAction = 'something_else'; - const wrapper = mountWithIntl( + const wrapper = mount(
{ test('it does NOT render a ParentProcessDraggable when eventAction is "process_stopped"', () => { const eventAction = 'process_stopped'; - const wrapper = mountWithIntl( + const wrapper = mount(
{ test('it does NOT render a ParentProcessDraggable when eventAction is "termination_event"', () => { const eventAction = 'termination_event'; - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns renders the message when showMessage is true', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it does NOT render the message when showMessage is false', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it renders a ProcessDraggableWithNonExistentProcess when endgamePid and endgameProcessName are provided, but processPid and processName are NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it prefers to render processName and processPid over endgameProcessName and endgamePid respectfully when all of those fields are provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ + const mount = useMountAppended(); + describe('#createGenericSystemRowRenderer', () => { let nonSystem: Ecs; let system: Ecs; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx index a1121f5f43847..167abe2185bcc 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx @@ -7,12 +7,14 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TestProviders } from '../../../../../mock'; +import { useMountAppended } from '../../../../../utils/use_mount_appended'; import { Package } from './package'; describe('Package', () => { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders against shallow snapshot', () => { const wrapper = shallow( @@ -54,7 +56,7 @@ describe('Package', () => { }); test('it returns just the package name', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns just the package name and package summary', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns just the package name, package summary, package version', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ + const mount = useMountAppended(); + describe('rendering', () => { test('it renders against shallow snapshot', () => { const wrapper = shallow( @@ -57,7 +59,7 @@ describe('UserHostWorkingDir', () => { }); test('it returns userDomain if that is the only attribute defined', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns userName if that is the only attribute defined', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns hostName if that is the only attribute defined', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns "in" + workingDirectory if that is the only attribute defined', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns userName and workingDirectory', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns hostName and workingDirectory', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns userName, userDomain, hostName', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns hostName and userName with the default hostNameSeparator "@", when hostNameSeparator is NOT specified as a prop', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns hostName and userName with an overridden hostNameSeparator, when hostNameSeparator is specified as a prop', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it renders a draggable `user.domain` field (by default) when userDomain is provided, and userDomainField is NOT specified as a prop', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it renders a draggable with an overridden field name when userDomain is provided, and userDomainField is also specified as a prop', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it renders a draggable `user.name` field (by default) when userName is provided, and userNameField is NOT specified as a prop', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it renders a draggable with an overridden field name when userName is provided, and userNameField is also specified as a prop', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ + const mount = useMountAppended(); + describe('rendering', () => { test('it renders the default ZeekDetails', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns zeek.connection if the data does contain zeek.connection data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns zeek.dns if the data does contain zeek.dns data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns zeek.http if the data does contain zeek.http data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns zeek.notice if the data does contain zeek.notice data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns zeek.ssl if the data does contain zeek.ssl data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns zeek.files if the data does contain zeek.files data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns null for text if the data contains no zeek data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); let nonZeek: Ecs; let zeek: Ecs; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx index e442f884a8e4b..4ef2bb89e05ca 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx @@ -8,10 +8,10 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash/fp'; import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { Ecs } from '../../../../../graphql/types'; import { mockTimelineData, TestProviders } from '../../../../../mock'; +import { useMountAppended } from '../../../../../utils/use_mount_appended'; import { ZeekSignature, extractStateValue, @@ -27,6 +27,7 @@ import { } from './zeek_signature'; describe('ZeekSignature', () => { + const mount = useMountAppended(); let zeek: Ecs; beforeEach(() => { @@ -70,7 +71,7 @@ describe('ZeekSignature', () => { describe('#TotalVirusLinkSha', () => { test('should return null if value is null', () => { - const wrapper = mountWithIntl(); + const wrapper = mount(); expect( wrapper .find('TotalVirusLinkSha') @@ -80,19 +81,19 @@ describe('ZeekSignature', () => { }); test('should render value', () => { - const wrapper = mountWithIntl(); + const wrapper = mount(); expect(wrapper.text()).toEqual('abc'); }); test('should render link with sha', () => { - const wrapper = mountWithIntl(); + const wrapper = mount(); expect(wrapper.find('a').prop('href')).toEqual('https://www.virustotal.com/#/search/abcdefg'); }); }); describe('#Link', () => { test('should return null if value is null', () => { - const wrapper = mountWithIntl(); + const wrapper = mount(); expect( wrapper .find('Link') @@ -102,12 +103,12 @@ describe('ZeekSignature', () => { }); test('should render value', () => { - const wrapper = mountWithIntl(); + const wrapper = mount(); expect(wrapper.text()).toEqual('abc'); }); test('should render value and link', () => { - const wrapper = mountWithIntl(); + const wrapper = mount(); expect(wrapper.find('a').prop('href')).toEqual( 'https://www.google.com/search?q=somethingelse' ); @@ -116,7 +117,7 @@ describe('ZeekSignature', () => { describe('DraggableZeekElement', () => { test('it returns null if value is null', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -130,7 +131,7 @@ describe('ZeekSignature', () => { }); test('it renders the default ZeekSignature', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -139,7 +140,7 @@ describe('ZeekSignature', () => { }); test('it renders with a custom string renderer', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { describe('#TagTooltip', () => { test('it renders the name of the field in a tooltip', () => { const field = 'zeek.notice'; - const wrapper = mountWithIntl( + const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/data_providers.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/data_providers.test.tsx index 43a7e87a15419..d67c6c9648a15 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/data_providers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/data_providers.test.tsx @@ -4,17 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; import { TestProviders } from '../../../mock/test_providers'; +import { useMountAppended } from '../../../utils/use_mount_appended'; import { DataProviders } from '.'; import { DataProvider } from './data_provider'; import { mockDataProviders } from './mock/mock_data_providers'; describe('DataProviders', () => { + const mount = useMountAppended(); + describe('rendering', () => { const dropMessage = ['Drop', 'query', 'build', 'here']; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx index d6e092550473d..c9454846c5548 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; @@ -15,9 +15,12 @@ import { TimelineContext } from '../timeline_context'; import { mockDataProviders } from './mock/mock_data_providers'; import { getDraggableId, Providers } from './providers'; import { DELETE_CLASS_NAME, ENABLE_CLASS_NAME, EXCLUDE_CLASS_NAME } from './provider_item_actions'; +import { useMountAppended } from '../../../utils/use_mount_appended'; describe('Providers', () => { const mockTimelineContext: boolean = true; + const mount = useMountAppended(); + describe('rendering', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx index 83564fbdc0988..977764803acbb 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; @@ -14,6 +14,7 @@ import { mockIndexPattern } from '../../../mock'; import { TestProviders } from '../../../mock/test_providers'; import { mockUiSettings } from '../../../mock/ui_settings'; import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; +import { useMountAppended } from '../../../utils/use_mount_appended'; import { TimelineHeaderComponent } from '.'; @@ -26,6 +27,7 @@ mockUseKibanaCore.mockImplementation(() => ({ describe('Header', () => { const indexPattern = mockIndexPattern; + const mount = useMountAppended(); describe('rendering', () => { test('renders correctly against snapshot', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx index ebf4ceceafe34..180af88f21e4d 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; @@ -26,6 +26,7 @@ import { import { TimelineComponent } from './timeline'; import { Sort } from './body/sort'; import { mockDataProviders } from './data_providers/mock/mock_data_providers'; +import { useMountAppended } from '../../utils/use_mount_appended'; const testFlyoutHeight = 980; @@ -50,6 +51,8 @@ describe('Timeline', () => { { request: { query: timelineQuery }, result: { data: { events: mockTimelineData } } }, ]; + const mount = useMountAppended(); + describe('rendering', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( diff --git a/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx index 2d9813206bb1e..8c5a08fdf5e21 100644 --- a/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx @@ -10,8 +10,7 @@ import * as React from 'react'; import { TruncatableText } from '.'; -// No style rules found on passed Component -describe.skip('TruncatableText', () => { +describe('TruncatableText', () => { test('renders correctly against snapshot', () => { const wrapper = shallow({'Hiding in plain sight'}); expect(toJson(wrapper)).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx index a9a0693a25447..febf9630e968a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount } from 'enzyme'; import React from 'react'; import { IIndexPattern } from 'src/plugins/data/public'; import { MemoryRouter } from 'react-router-dom'; @@ -17,6 +16,7 @@ import { SetAbsoluteRangeDatePicker } from './types'; import { hostDetailsPagePath } from '../types'; import { type } from './utils'; import { useKibanaCore } from '../../../lib/compose/kibana_core'; +import { useMountAppended } from '../../../utils/use_mount_appended'; jest.mock('../../../lib/settings/use_kibana_ui_setting'); @@ -52,6 +52,7 @@ describe('body', () => { anomalies: 'AnomaliesQueryTabBody', events: 'EventsQueryTabBody', }; + const mount = useMountAppended(); Object.entries(scenariosMap).forEach(([path, componentName]) => test(`it should pass expected object properties to ${componentName}`, () => { diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx index 4dc25c4ea27ad..2bc9b791e2fb1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash/fp'; import * as React from 'react'; @@ -18,6 +18,7 @@ import { mocksSource } from '../../../containers/source/mock'; import { FlowTarget } from '../../../graphql/types'; import { useKibanaCore } from '../../../lib/compose/kibana_core'; import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../mock'; +import { useMountAppended } from '../../../utils/use_mount_appended'; import { mockUiSettings } from '../../../mock/ui_settings'; import { createStore, State } from '../../../store'; import { InputsModelId } from '../../../store/inputs/constants'; @@ -112,6 +113,8 @@ jest.mock('ui/documentation_links', () => ({ })); describe('Ip Details', () => { + const mount = useMountAppended(); + beforeAll(() => { (global as GlobalWithFetch).fetch = jest.fn().mockImplementationOnce(() => Promise.resolve({ @@ -126,14 +129,15 @@ describe('Ip Details', () => { afterAll(() => { delete (global as GlobalWithFetch).fetch; }); - const state: State = mockGlobalState; + const state: State = mockGlobalState; let store = createStore(state, apolloClientObservable); beforeEach(() => { store = createStore(state, apolloClientObservable); localSource = cloneDeep(mocksSource); }); + test('it renders', () => { const wrapper = shallow(); expect(wrapper.find('[data-test-subj="ip-details-page"]').exists()).toBe(true); diff --git a/x-pack/legacy/plugins/siem/public/utils/use_mount_appended.ts b/x-pack/legacy/plugins/siem/public/utils/use_mount_appended.ts new file mode 100644 index 0000000000000..7b83f77a72023 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/utils/use_mount_appended.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount } from 'enzyme'; + +type WrapperOf any> = (...args: Parameters) => ReturnType; // eslint-disable-line +export type MountAppended = WrapperOf; + +export const useMountAppended = () => { + let root: HTMLElement; + + beforeEach(() => { + root = document.createElement('div'); + root.id = 'root'; + document.body.appendChild(root); + }); + + afterEach(() => { + document.body.removeChild(root); + }); + + const mountAppended: MountAppended = (node, options) => + mount(node, { ...options, attachTo: root }); + + return mountAppended; +}; diff --git a/yarn.lock b/yarn.lock index 420b22a664505..3c08a6fe8f131 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22941,10 +22941,10 @@ react-beautiful-dnd@^10.1.0: redux "^4.0.1" tiny-invariant "^1.0.4" -react-beautiful-dnd@^12.1.1: - version "12.1.1" - resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-12.1.1.tgz#810f9b9d94f667b15b253793e853d016a0f3f07c" - integrity sha512-w/mpIXMEXowc53PCEnMoFyAEYFgxMfygMK5msLo5ifJ2/CiSACLov9A79EomnPF7zno3N207QGXsraBxAJnyrw== +react-beautiful-dnd@^12.2.0: + version "12.2.0" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-12.2.0.tgz#e5f6222f9e7934c6ed4ee09024547f9e353ae423" + integrity sha512-s5UrOXNDgeEC+sx65IgbeFlqKKgK3c0UfbrJLWufP34WBheyu5kJ741DtJbsSgPKyNLkqfswpMYr0P8lRj42cA== dependencies: "@babel/runtime-corejs2" "^7.6.3" css-box-model "^1.2.0" From 6d048b4e6680a4e8d4666c10dd115aa0016795fe Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 13 Dec 2019 14:06:37 +0100 Subject: [PATCH 32/36] Migrate graph server and licensing handling (#52279) (#52979) --- src/legacy/plugin_discovery/types.ts | 1 + .../ui/public/new_platform/new_platform.ts | 2 + x-pack/.i18nrc.json | 2 +- .../plugins/graph/{index.js => index.ts} | 23 ++-- x-pack/legacy/plugins/graph/public/app.js | 19 +-- .../public/hacks/toggle_app_link_in_nav.js | 19 --- x-pack/legacy/plugins/graph/public/index.ts | 4 +- x-pack/legacy/plugins/graph/public/plugin.ts | 7 +- .../plugins/graph/public/register_feature.js | 25 ---- .../legacy/plugins/graph/public/render_app.ts | 22 +++- .../plugins/graph/server/init_server.js | 26 ---- .../server/lib/__tests__/check_license.js | 120 ------------------ .../plugins/graph/server/lib/check_license.js | 60 --------- .../lib/es/call_es_graph_explore_api.js | 40 ------ .../graph/server/lib/es/call_es_search_api.js | 22 ---- .../plugins/graph/server/lib/es/index.js | 8 -- .../legacy/plugins/graph/server/lib/index.js | 17 --- .../server/lib/pre/get_call_cluster_pre.js | 13 -- .../plugins/graph/server/lib/pre/index.js | 8 -- .../server/lib/pre/verify_api_access_pre.js | 19 --- .../graph/server/routes/graph_explore.js | 37 ------ .../graph/server/routes/search_proxy.js | 43 ------- x-pack/plugins/graph/common/check_license.ts | 68 ++++++++++ x-pack/plugins/graph/kibana.json | 9 ++ .../graph/public/index.ts} | 4 +- x-pack/plugins/graph/public/plugin.ts | 54 ++++++++ .../graph/public/services/toggle_nav_link.ts | 26 ++++ .../graph/server/index.ts} | 5 +- .../plugins/graph/server/lib/license_state.ts | 43 +++++++ x-pack/plugins/graph/server/plugin.ts | 32 +++++ x-pack/plugins/graph/server/routes/explore.ts | 79 ++++++++++++ x-pack/plugins/graph/server/routes/search.ts | 61 +++++++++ .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 34 files changed, 419 insertions(+), 503 deletions(-) rename x-pack/legacy/plugins/graph/{index.js => index.ts} (83%) delete mode 100644 x-pack/legacy/plugins/graph/public/hacks/toggle_app_link_in_nav.js delete mode 100644 x-pack/legacy/plugins/graph/public/register_feature.js delete mode 100644 x-pack/legacy/plugins/graph/server/init_server.js delete mode 100644 x-pack/legacy/plugins/graph/server/lib/__tests__/check_license.js delete mode 100644 x-pack/legacy/plugins/graph/server/lib/check_license.js delete mode 100644 x-pack/legacy/plugins/graph/server/lib/es/call_es_graph_explore_api.js delete mode 100644 x-pack/legacy/plugins/graph/server/lib/es/call_es_search_api.js delete mode 100644 x-pack/legacy/plugins/graph/server/lib/es/index.js delete mode 100644 x-pack/legacy/plugins/graph/server/lib/index.js delete mode 100644 x-pack/legacy/plugins/graph/server/lib/pre/get_call_cluster_pre.js delete mode 100644 x-pack/legacy/plugins/graph/server/lib/pre/index.js delete mode 100644 x-pack/legacy/plugins/graph/server/lib/pre/verify_api_access_pre.js delete mode 100644 x-pack/legacy/plugins/graph/server/routes/graph_explore.js delete mode 100644 x-pack/legacy/plugins/graph/server/routes/search_proxy.js create mode 100644 x-pack/plugins/graph/common/check_license.ts create mode 100644 x-pack/plugins/graph/kibana.json rename x-pack/{legacy/plugins/graph/server/index.js => plugins/graph/public/index.ts} (73%) create mode 100644 x-pack/plugins/graph/public/plugin.ts create mode 100644 x-pack/plugins/graph/public/services/toggle_nav_link.ts rename x-pack/{legacy/plugins/graph/server/routes/index.js => plugins/graph/server/index.ts} (70%) create mode 100644 x-pack/plugins/graph/server/lib/license_state.ts create mode 100644 x-pack/plugins/graph/server/plugin.ts create mode 100644 x-pack/plugins/graph/server/routes/explore.ts create mode 100644 x-pack/plugins/graph/server/routes/search.ts diff --git a/src/legacy/plugin_discovery/types.ts b/src/legacy/plugin_discovery/types.ts index c32418e1aeb62..fe886b9d17811 100644 --- a/src/legacy/plugin_discovery/types.ts +++ b/src/legacy/plugin_discovery/types.ts @@ -69,6 +69,7 @@ export interface LegacyPluginOptions { noParse: string[]; home: string[]; mappings: any; + migrations: any; savedObjectSchemas: SavedObjectsSchemaDefinition; savedObjectsManagement: SavedObjectsManagementDefinition; visTypes: string[]; diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index d80d11e1b1bdd..547d22c58cfc1 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -32,6 +32,7 @@ import { DevToolsSetup, DevToolsStart } from '../../../../plugins/dev_tools/publ import { KibanaLegacySetup, KibanaLegacyStart } from '../../../../plugins/kibana_legacy/public'; import { HomePublicPluginSetup, HomePublicPluginStart } from '../../../../plugins/home/public'; import { SharePluginSetup, SharePluginStart } from '../../../../plugins/share/public'; +import { LicensingPluginSetup } from '../../../../../x-pack/plugins/licensing/common/types'; export interface PluginsSetup { data: ReturnType; @@ -43,6 +44,7 @@ export interface PluginsSetup { dev_tools: DevToolsSetup; kibana_legacy: KibanaLegacySetup; share: SharePluginSetup; + licensing: LicensingPluginSetup; } export interface PluginsStart { diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 6d0da2f0b693d..34509e33bf137 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -11,7 +11,7 @@ "xpack.dashboardMode": "legacy/plugins/dashboard_mode", "xpack.features": "plugins/features", "xpack.fileUpload": "legacy/plugins/file_upload", - "xpack.graph": "legacy/plugins/graph", + "xpack.graph": ["legacy/plugins/graph", "plugins/graph"], "xpack.grokDebugger": "legacy/plugins/grokdebugger", "xpack.idxMgmt": "legacy/plugins/index_management", "xpack.indexLifecycleMgmt": "legacy/plugins/index_lifecycle_management", diff --git a/x-pack/legacy/plugins/graph/index.js b/x-pack/legacy/plugins/graph/index.ts similarity index 83% rename from x-pack/legacy/plugins/graph/index.js rename to x-pack/legacy/plugins/graph/index.ts index 9ece9966b7da4..601a239574e6b 100644 --- a/x-pack/legacy/plugins/graph/index.js +++ b/x-pack/legacy/plugins/graph/index.ts @@ -7,11 +7,12 @@ import { resolve } from 'path'; import { i18n } from '@kbn/i18n'; +// @ts-ignore import migrations from './migrations'; -import { initServer } from './server'; import mappings from './mappings.json'; +import { LegacyPluginInitializer } from '../../../../src/legacy/plugin_discovery/types'; -export function graph(kibana) { +export const graph: LegacyPluginInitializer = kibana => { return new kibana.Plugin({ id: 'graph', configPrefix: 'xpack.graph', @@ -26,17 +27,17 @@ export function graph(kibana) { main: 'plugins/graph/index', }, styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: ['plugins/graph/hacks/toggle_app_link_in_nav'], - home: ['plugins/graph/register_feature'], mappings, migrations, }, - config(Joi) { + config(Joi: any) { return Joi.object({ enabled: Joi.boolean().default(true), canEditDrillDownUrls: Joi.boolean().default(true), - savePolicy: Joi.string().valid(['config', 'configAndDataWithConsent', 'configAndData', 'none']).default('configAndData'), + savePolicy: Joi.string() + .valid(['config', 'configAndDataWithConsent', 'configAndData', 'none']) + .default('configAndData'), }).default(); }, @@ -45,7 +46,7 @@ export function graph(kibana) { const config = server.config(); return { graphSavePolicy: config.get('xpack.graph.savePolicy'), - canEditDrillDownUrls: config.get('xpack.graph.canEditDrillDownUrls') + canEditDrillDownUrls: config.get('xpack.graph.canEditDrillDownUrls'), }; }); @@ -72,11 +73,9 @@ export function graph(kibana) { read: ['index-pattern', 'graph-workspace'], }, ui: [], - } - } + }, + }, }); - - initServer(server); }, }); -} +}; diff --git a/x-pack/legacy/plugins/graph/public/app.js b/x-pack/legacy/plugins/graph/public/app.js index aa08841e03f52..353123524335b 100644 --- a/x-pack/legacy/plugins/graph/public/app.js +++ b/x-pack/legacy/plugins/graph/public/app.js @@ -13,7 +13,6 @@ import { isColorDark, hexToRgb } from '@elastic/eui'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; -import { addAppRedirectMessageToUrl } from 'ui/notify'; import appTemplate from './angular/templates/index.html'; import listingTemplate from './angular/templates/listing_ng_wrapper.html'; @@ -39,10 +38,10 @@ import { hasFieldsSelector } from './state_management'; import { formatHttpError } from './helpers/format_http_error'; +import { checkLicense } from '../../../../plugins/graph/common/check_license'; export function initGraphApp(angularModule, deps) { const { - xpackInfo, chrome, savedGraphWorkspaces, toastNotifications, @@ -63,17 +62,6 @@ export function initGraphApp(angularModule, deps) { const app = angularModule; - function checkLicense(kbnBaseUrl) { - const licenseAllowsToShowThisPage = xpackInfo.get('features.graph.showAppLink') && - xpackInfo.get('features.graph.enableAppLink'); - if (!licenseAllowsToShowThisPage) { - const message = xpackInfo.get('features.graph.message'); - const newUrl = addAppRedirectMessageToUrl(addBasePath(kbnBaseUrl), message); - window.location.href = newUrl; - throw new Error('Graph license error'); - } - } - app.directive('vennDiagram', function (reactDirective) { return reactDirective(VennDiagram); }); @@ -123,7 +111,6 @@ export function initGraphApp(angularModule, deps) { template: listingTemplate, badge: getReadonlyBadge, controller($location, $scope) { - checkLicense(kbnBaseUrl); const services = savedObjectRegistry.byLoaderPropertiesName; const graphService = services['Graph workspace']; @@ -164,7 +151,6 @@ export function initGraphApp(angularModule, deps) { ) : savedGraphWorkspaces.get(); }, - //Copied from example found in wizard.js ( Kibana TODO - can't indexPatterns: function () { return savedObjectsClient.find({ type: 'index-pattern', @@ -185,10 +171,8 @@ export function initGraphApp(angularModule, deps) { //======== Controller for basic UI ================== app.controller('graphuiPlugin', function ($scope, $route, $location, confirmModal) { - checkLicense(kbnBaseUrl); function handleError(err) { - checkLicense(kbnBaseUrl); const toastTitle = i18n.translate('xpack.graph.errorToastTitle', { defaultMessage: 'Graph Error', description: '"Graph" is a product name and should not be translated.', @@ -206,7 +190,6 @@ export function initGraphApp(angularModule, deps) { } async function handleHttpError(error) { - checkLicense(kbnBaseUrl); toastNotifications.addDanger(formatHttpError(error)); } diff --git a/x-pack/legacy/plugins/graph/public/hacks/toggle_app_link_in_nav.js b/x-pack/legacy/plugins/graph/public/hacks/toggle_app_link_in_nav.js deleted file mode 100644 index 5b1eb6181fd61..0000000000000 --- a/x-pack/legacy/plugins/graph/public/hacks/toggle_app_link_in_nav.js +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { npStart } from 'ui/new_platform'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; - -const navLinkUpdates = {}; -navLinkUpdates.hidden = true; -const showAppLink = xpackInfo.get('features.graph.showAppLink', false); -navLinkUpdates.hidden = !showAppLink; -if (showAppLink) { - navLinkUpdates.disabled = !xpackInfo.get('features.graph.enableAppLink', false); - navLinkUpdates.tooltip = xpackInfo.get('features.graph.message'); -} - -npStart.core.chrome.navLinks.update('graph', navLinkUpdates); diff --git a/x-pack/legacy/plugins/graph/public/index.ts b/x-pack/legacy/plugins/graph/public/index.ts index 712d08c106425..e0ce210328897 100644 --- a/x-pack/legacy/plugins/graph/public/index.ts +++ b/x-pack/legacy/plugins/graph/public/index.ts @@ -12,8 +12,6 @@ import 'ui/autoload/all'; import chrome from 'ui/chrome'; import { IPrivate } from 'ui/private'; // @ts-ignore -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -// @ts-ignore import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; import { npSetup, npStart } from 'ui/new_platform'; @@ -45,9 +43,9 @@ async function getAngularInjectedDependencies(): Promise; @@ -18,8 +19,8 @@ export interface GraphPluginStartDependencies { export interface GraphPluginSetupDependencies { __LEGACY: { Storage: any; - xpackInfo: any; }; + licensing: LicensingPluginSetup; } export interface GraphPluginStartDependencies { @@ -34,7 +35,7 @@ export class GraphPlugin implements Plugin { private savedObjectsClient: SavedObjectsClientContract | null = null; private angularDependencies: LegacyAngularInjectedDependencies | null = null; - setup(core: CoreSetup, { __LEGACY: { xpackInfo, Storage } }: GraphPluginSetupDependencies) { + setup(core: CoreSetup, { __LEGACY: { Storage }, licensing }: GraphPluginSetupDependencies) { core.application.register({ id: 'graph', title: 'Graph', @@ -42,10 +43,10 @@ export class GraphPlugin implements Plugin { const { renderApp } = await import('./render_app'); return renderApp({ ...params, + licensing, navigation: this.navigationStart!, npData: this.npDataStart!, savedObjectsClient: this.savedObjectsClient!, - xpackInfo, addBasePath: core.http.basePath.prepend, getBasePath: core.http.basePath.get, canEditDrillDownUrls: core.injectedMetadata.getInjectedVar( diff --git a/x-pack/legacy/plugins/graph/public/register_feature.js b/x-pack/legacy/plugins/graph/public/register_feature.js deleted file mode 100644 index b3e2efc990b78..0000000000000 --- a/x-pack/legacy/plugins/graph/public/register_feature.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - - - -import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; - -import { i18n } from '@kbn/i18n'; - -FeatureCatalogueRegistryProvider.register(() => { - return { - id: 'graph', - title: 'Graph', - description: i18n.translate('xpack.graph.pluginDescription', { - defaultMessage: 'Surface and analyze relevant relationships in your Elasticsearch data.', - }), - icon: 'graphApp', - path: '/app/graph', - showOnHomePage: true, - category: FeatureCatalogueCategory.DATA - }; -}); diff --git a/x-pack/legacy/plugins/graph/public/render_app.ts b/x-pack/legacy/plugins/graph/public/render_app.ts index 0f3c52d38a01c..2cad8f629f2ab 100644 --- a/x-pack/legacy/plugins/graph/public/render_app.ts +++ b/x-pack/legacy/plugins/graph/public/render_app.ts @@ -19,6 +19,8 @@ import { configureAppAngularModule } from 'ui/legacy_compat'; import { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_top_nav'; // @ts-ignore import { confirmModalFactory } from 'ui/modals/confirm_modal'; +// @ts-ignore +import { addAppRedirectMessageToUrl } from 'ui/notify'; // type imports import { @@ -36,6 +38,8 @@ import { IndexPatternsContract, } from '../../../../../src/plugins/data/public'; import { NavigationStart } from '../../../../../src/legacy/core_plugins/navigation/public'; +import { LicensingPluginSetup } from '../../../../plugins/licensing/common/types'; +import { checkLicense } from '../../../../plugins/graph/common/check_license'; /** * These are dependencies of the Graph app besides the base dependencies @@ -49,13 +53,13 @@ export interface GraphDependencies extends LegacyAngularInjectedDependencies { capabilities: Record>; coreStart: AppMountContext['core']; navigation: NavigationStart; + licensing: LicensingPluginSetup; chrome: ChromeStart; config: IUiSettingsClient; toastNotifications: ToastsStart; indexPatterns: IndexPatternsContract; npData: ReturnType; savedObjectsClient: SavedObjectsClientContract; - xpackInfo: { get(path: string): unknown }; addBasePath: (url: string) => string; getBasePath: () => string; Storage: any; @@ -82,9 +86,23 @@ export interface LegacyAngularInjectedDependencies { export const renderApp = ({ appBasePath, element, ...deps }: GraphDependencies) => { const graphAngularModule = createLocalAngularModule(deps.navigation); configureAppAngularModule(graphAngularModule, deps.coreStart as LegacyCoreStart, true); + + const licenseSubscription = deps.licensing.license$.subscribe(license => { + const info = checkLicense(license); + const licenseAllowsToShowThisPage = info.showAppLink && info.enableAppLink; + + if (!licenseAllowsToShowThisPage) { + const newUrl = addAppRedirectMessageToUrl(deps.addBasePath(deps.kbnBaseUrl), info.message); + window.location.href = newUrl; + } + }); + initGraphApp(graphAngularModule, deps); const $injector = mountGraphApp(appBasePath, element); - return () => $injector.get('$rootScope').$destroy(); + return () => { + licenseSubscription.unsubscribe(); + $injector.get('$rootScope').$destroy(); + }; }; const mainTemplate = (basePath: string) => `
diff --git a/x-pack/legacy/plugins/graph/server/init_server.js b/x-pack/legacy/plugins/graph/server/init_server.js deleted file mode 100644 index 4647fe9c2662a..0000000000000 --- a/x-pack/legacy/plugins/graph/server/init_server.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; - -import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status'; -import { checkLicense } from './lib'; -import { graphExploreRoute, searchProxyRoute } from './routes'; - -export function initServer(server) { - const graphPlugin = server.plugins.graph; - const xpackMainPlugin = server.plugins.xpack_main; - - mirrorPluginStatus(xpackMainPlugin, graphPlugin); - xpackMainPlugin.status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results - xpackMainPlugin.info.feature('graph').registerLicenseCheckResultsGenerator(checkLicense); - }); - - server.route(graphExploreRoute); - server.route(searchProxyRoute); -} diff --git a/x-pack/legacy/plugins/graph/server/lib/__tests__/check_license.js b/x-pack/legacy/plugins/graph/server/lib/__tests__/check_license.js deleted file mode 100644 index c341dfbc378ca..0000000000000 --- a/x-pack/legacy/plugins/graph/server/lib/__tests__/check_license.js +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { set } from 'lodash'; -import sinon from 'sinon'; -import { checkLicense } from '../check_license'; - -describe('check_license: ', function () { - - let mockLicenseInfo; - let licenseCheckResult; - - beforeEach(() => { - mockLicenseInfo = { - isAvailable: () => true - }; - }); - - describe('mockLicenseInfo is not set', () => { - beforeEach(() => { - mockLicenseInfo = null; - licenseCheckResult = checkLicense(mockLicenseInfo); - }); - - it ('should set showAppLink to true', () => { - expect(licenseCheckResult.showAppLink).to.be(true); - }); - - it ('should set enableAppLink to false', () => { - expect(licenseCheckResult.enableAppLink).to.be(false); - }); - }); - - describe('mockLicenseInfo is set but not available', () => { - beforeEach(() => { - mockLicenseInfo = { isAvailable: () => false }; - licenseCheckResult = checkLicense(mockLicenseInfo); - }); - - it ('should set showAppLink to true', () => { - expect(licenseCheckResult.showAppLink).to.be(true); - }); - - it ('should set enableAppLink to false', () => { - expect(licenseCheckResult.enableAppLink).to.be(false); - }); - }); - - describe('graph is disabled in Elasticsearch', () => { - beforeEach(() => { - set(mockLicenseInfo, 'feature', sinon.stub().withArgs('graph').returns({ isEnabled: () => false })); - licenseCheckResult = checkLicense(mockLicenseInfo); - }); - - it ('should set showAppLink to false', () => { - expect(licenseCheckResult.showAppLink).to.be(false); - }); - }); - - describe('graph is enabled in Elasticsearch', () => { - beforeEach(() => { - set(mockLicenseInfo, 'feature', sinon.stub().withArgs('graph').returns({ isEnabled: () => true })); - }); - - describe('& license is trial or platinum', () => { - beforeEach(() => { - set(mockLicenseInfo, 'license.isOneOf', sinon.stub().withArgs([ 'trial', 'platinum' ]).returns(true)); - set(mockLicenseInfo, 'license.getType', () => 'trial'); - }); - - describe('& license is active', () => { - beforeEach(() => { - set(mockLicenseInfo, 'license.isActive', () => true); - licenseCheckResult = checkLicense(mockLicenseInfo); - }); - - it ('should set showAppLink to true', () => { - expect(licenseCheckResult.showAppLink).to.be(true); - }); - - it ('should set enableAppLink to true', () => { - expect(licenseCheckResult.enableAppLink).to.be(true); - }); - }); - - describe('& license is expired', () => { - beforeEach(() => { - set(mockLicenseInfo, 'license.isActive', () => false); - licenseCheckResult = checkLicense(mockLicenseInfo); - }); - - it ('should set showAppLink to true', () => { - expect(licenseCheckResult.showAppLink).to.be(true); - }); - - it ('should set enableAppLink to false', () => { - expect(licenseCheckResult.enableAppLink).to.be(false); - }); - }); - - }); - - describe('& license is neither trial nor platinum', () => { - beforeEach(() => { - set(mockLicenseInfo, 'license.isOneOf', () => false); - set(mockLicenseInfo, 'license.getType', () => 'basic'); - set(mockLicenseInfo, 'license.isActive', () => true); - licenseCheckResult = checkLicense(mockLicenseInfo); - }); - - it ('should set showAppLink to false', () => { - expect(licenseCheckResult.showAppLink).to.be(false); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/graph/server/lib/check_license.js b/x-pack/legacy/plugins/graph/server/lib/check_license.js deleted file mode 100644 index 4ddac989a789a..0000000000000 --- a/x-pack/legacy/plugins/graph/server/lib/check_license.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export function checkLicense(xpackLicenseInfo) { - - if (!xpackLicenseInfo || !xpackLicenseInfo.isAvailable()) { - return { - showAppLink: true, - enableAppLink: false, - message: i18n.translate('xpack.graph.serverSideErrors.unavailableLicenseInformationErrorMessage', { - defaultMessage: 'Graph is unavailable - license information is not available at this time.', - }) - }; - } - - const graphFeature = xpackLicenseInfo.feature('graph'); - if (!graphFeature.isEnabled()) { - return { - showAppLink: false, - enableAppLink: false, - message: i18n.translate('xpack.graph.serverSideErrors.unavailableGraphErrorMessage', { - defaultMessage: 'Graph is unavailable', - }) - }; - } - - const isLicenseActive = xpackLicenseInfo.license.isActive(); - let message; - if (!isLicenseActive) { - message = i18n.translate('xpack.graph.serverSideErrors.expiredLicenseErrorMessage', { - defaultMessage: 'Graph is unavailable - license has expired.', - }); - } - - if (xpackLicenseInfo.license.isOneOf([ 'trial', 'platinum' ])) { - return { - showAppLink: true, - enableAppLink: isLicenseActive, - message - }; - } - - message = i18n.translate('xpack.graph.serverSideErrors.wrongLicenseTypeErrorMessage', { - defaultMessage: 'Graph is unavailable for the current {licenseType} license. Please upgrade your license.', - values: { - licenseType: xpackLicenseInfo.license.getType(), - }, - }); - - return { - showAppLink: false, - enableAppLink: false, - message - }; -} diff --git a/x-pack/legacy/plugins/graph/server/lib/es/call_es_graph_explore_api.js b/x-pack/legacy/plugins/graph/server/lib/es/call_es_graph_explore_api.js deleted file mode 100644 index a656a4349f61f..0000000000000 --- a/x-pack/legacy/plugins/graph/server/lib/es/call_es_graph_explore_api.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; -import { get } from 'lodash'; - -export async function callEsGraphExploreApi({ callCluster, index, query }) { - try { - return { - ok: true, - resp: await callCluster('transport.request', { - 'path': '/' + encodeURIComponent(index) + '/_graph/explore', - body: query, - method: 'POST', - query: {} - }) - }; - } catch (error) { - // Extract known reasons for bad choice of field - const relevantCause = [].concat(get(error, 'body.error.root_cause', []) || []) - .find(cause => { - return ( - cause.reason.includes('Fielddata is disabled on text fields') || - cause.reason.includes('No support for examining floating point') || - cause.reason.includes('Sample diversifying key must be a single valued-field') || - cause.reason.includes('Failed to parse query') || - cause.type == 'parsing_exception' - ); - }); - - if (relevantCause) { - throw Boom.badRequest(relevantCause.reason); - } - - throw Boom.boomify(error); - } -} diff --git a/x-pack/legacy/plugins/graph/server/lib/es/call_es_search_api.js b/x-pack/legacy/plugins/graph/server/lib/es/call_es_search_api.js deleted file mode 100644 index cdc355d2bdea7..0000000000000 --- a/x-pack/legacy/plugins/graph/server/lib/es/call_es_search_api.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; - -export async function callEsSearchApi({ callCluster, index, body, queryParams }) { - try { - return { - ok: true, - resp: await callCluster('search', { - ...queryParams, - index, - body - }) - }; - } catch (error) { - throw Boom.boomify(error, { statusCode: error.statusCode || 500 }); - } -} diff --git a/x-pack/legacy/plugins/graph/server/lib/es/index.js b/x-pack/legacy/plugins/graph/server/lib/es/index.js deleted file mode 100644 index b385a1be018c4..0000000000000 --- a/x-pack/legacy/plugins/graph/server/lib/es/index.js +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { callEsSearchApi } from './call_es_search_api'; -export { callEsGraphExploreApi } from './call_es_graph_explore_api'; diff --git a/x-pack/legacy/plugins/graph/server/lib/index.js b/x-pack/legacy/plugins/graph/server/lib/index.js deleted file mode 100644 index 6fe112004afbc..0000000000000 --- a/x-pack/legacy/plugins/graph/server/lib/index.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { checkLicense } from './check_license'; - -export { - callEsGraphExploreApi, - callEsSearchApi, -} from './es'; - -export { - getCallClusterPre, - verifyApiAccessPre, -} from './pre'; diff --git a/x-pack/legacy/plugins/graph/server/lib/pre/get_call_cluster_pre.js b/x-pack/legacy/plugins/graph/server/lib/pre/get_call_cluster_pre.js deleted file mode 100644 index 383170a13d4cf..0000000000000 --- a/x-pack/legacy/plugins/graph/server/lib/pre/get_call_cluster_pre.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const getCallClusterPre = { - assign: 'callCluster', - method(request) { - const cluster = request.server.plugins.elasticsearch.getCluster('data'); - return (...args) => cluster.callWithRequest(request, ...args); - } -}; diff --git a/x-pack/legacy/plugins/graph/server/lib/pre/index.js b/x-pack/legacy/plugins/graph/server/lib/pre/index.js deleted file mode 100644 index 8d2e15f612bf1..0000000000000 --- a/x-pack/legacy/plugins/graph/server/lib/pre/index.js +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { getCallClusterPre } from './get_call_cluster_pre'; -export { verifyApiAccessPre } from './verify_api_access_pre'; diff --git a/x-pack/legacy/plugins/graph/server/lib/pre/verify_api_access_pre.js b/x-pack/legacy/plugins/graph/server/lib/pre/verify_api_access_pre.js deleted file mode 100644 index a26e7247d6287..0000000000000 --- a/x-pack/legacy/plugins/graph/server/lib/pre/verify_api_access_pre.js +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; - -export function verifyApiAccessPre(request, h) { - const xpackInfo = request.server.plugins.xpack_main.info; - const graph = xpackInfo.feature('graph'); - const licenseCheckResults = graph.getLicenseCheckResults(); - - if (licenseCheckResults.showAppLink && licenseCheckResults.enableAppLink) { - return null; - } else { - throw Boom.forbidden(licenseCheckResults.message); - } -} diff --git a/x-pack/legacy/plugins/graph/server/routes/graph_explore.js b/x-pack/legacy/plugins/graph/server/routes/graph_explore.js deleted file mode 100644 index 7f77903dd7050..0000000000000 --- a/x-pack/legacy/plugins/graph/server/routes/graph_explore.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; - -import { - verifyApiAccessPre, - getCallClusterPre, - callEsGraphExploreApi, -} from '../lib'; - -export const graphExploreRoute = { - path: '/api/graph/graphExplore', - method: 'POST', - config: { - pre: [ - verifyApiAccessPre, - getCallClusterPre, - ], - validate: { - payload: Joi.object().keys({ - index: Joi.string().required(), - query: Joi.object().required().unknown(true) - }).default() - }, - handler(request) { - return callEsGraphExploreApi({ - callCluster: request.pre.callCluster, - index: request.payload.index, - query: request.payload.query, - }); - } - } -}; diff --git a/x-pack/legacy/plugins/graph/server/routes/search_proxy.js b/x-pack/legacy/plugins/graph/server/routes/search_proxy.js deleted file mode 100644 index 64fc44b1e3677..0000000000000 --- a/x-pack/legacy/plugins/graph/server/routes/search_proxy.js +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; -import Boom from 'boom'; - -import { - verifyApiAccessPre, - getCallClusterPre, - callEsSearchApi, -} from '../lib'; - -export const searchProxyRoute = { - path: '/api/graph/searchProxy', - method: 'POST', - config: { - pre: [ - getCallClusterPre, - verifyApiAccessPre, - ], - validate: { - payload: Joi.object().keys({ - index: Joi.string().required(), - body: Joi.object().unknown(true).default() - }).default() - }, - async handler(request) { - const includeFrozen = await request.getUiSettingsService().get('search:includeFrozen'); - return await callEsSearchApi({ - callCluster: request.pre.callCluster, - index: request.payload.index, - body: request.payload.body, - queryParams: { - rest_total_hits_as_int: true, - ignore_throttled: !includeFrozen, - } - }); - } - } -}; diff --git a/x-pack/plugins/graph/common/check_license.ts b/x-pack/plugins/graph/common/check_license.ts new file mode 100644 index 0000000000000..a918f53776b17 --- /dev/null +++ b/x-pack/plugins/graph/common/check_license.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { ILicense, LICENSE_CHECK_STATE } from '../../licensing/common/types'; +import { assertNever } from '../../../../src/core/utils'; + +export interface GraphLicenseInformation { + showAppLink: boolean; + enableAppLink: boolean; + message: string; +} + +export function checkLicense(license: ILicense | undefined): GraphLicenseInformation { + if (!license || !license.isAvailable) { + return { + showAppLink: true, + enableAppLink: false, + message: i18n.translate( + 'xpack.graph.serverSideErrors.unavailableLicenseInformationErrorMessage', + { + defaultMessage: + 'Graph is unavailable - license information is not available at this time.', + } + ), + }; + } + + const graphFeature = license.getFeature('graph'); + if (!graphFeature.isEnabled) { + return { + showAppLink: false, + enableAppLink: false, + message: i18n.translate('xpack.graph.serverSideErrors.unavailableGraphErrorMessage', { + defaultMessage: 'Graph is unavailable', + }), + }; + } + + const check = license.check('graph', 'platinum'); + + switch (check.state) { + case LICENSE_CHECK_STATE.Expired: + return { + showAppLink: true, + enableAppLink: false, + message: check.message || '', + }; + case LICENSE_CHECK_STATE.Invalid: + case LICENSE_CHECK_STATE.Unavailable: + return { + showAppLink: false, + enableAppLink: false, + message: check.message || '', + }; + case LICENSE_CHECK_STATE.Valid: + return { + showAppLink: true, + enableAppLink: true, + message: '', + }; + default: + return assertNever(check.state); + } +} diff --git a/x-pack/plugins/graph/kibana.json b/x-pack/plugins/graph/kibana.json new file mode 100644 index 0000000000000..0d0ddc55a391b --- /dev/null +++ b/x-pack/plugins/graph/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "graph", + "version": "8.0.0", + "kibanaVersion": "kibana", + "server": true, + "ui": true, + "requiredPlugins": ["licensing"], + "optionalPlugins": ["home"] +} diff --git a/x-pack/legacy/plugins/graph/server/index.js b/x-pack/plugins/graph/public/index.ts similarity index 73% rename from x-pack/legacy/plugins/graph/server/index.js rename to x-pack/plugins/graph/public/index.ts index be72e92176554..ac9ca960c0c7f 100644 --- a/x-pack/legacy/plugins/graph/server/index.js +++ b/x-pack/plugins/graph/public/index.ts @@ -4,4 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { initServer } from './init_server'; +import { GraphPlugin } from './plugin'; + +export const plugin = () => new GraphPlugin(); diff --git a/x-pack/plugins/graph/public/plugin.ts b/x-pack/plugins/graph/public/plugin.ts new file mode 100644 index 0000000000000..cb997ff427543 --- /dev/null +++ b/x-pack/plugins/graph/public/plugin.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { CoreSetup, CoreStart } from 'kibana/public'; +import { Plugin } from 'src/core/public'; +import { toggleNavLink } from './services/toggle_nav_link'; +import { LicensingPluginSetup } from '../../licensing/common/types'; +import { checkLicense } from '../common/check_license'; +import { + FeatureCatalogueCategory, + HomePublicPluginSetup, +} from '../../../../src/plugins/home/public'; + +export interface GraphPluginSetupDependencies { + licensing: LicensingPluginSetup; + home?: HomePublicPluginSetup; +} + +export class GraphPlugin implements Plugin { + private licensing: LicensingPluginSetup | null = null; + + setup(core: CoreSetup, { licensing, home }: GraphPluginSetupDependencies) { + this.licensing = licensing; + + if (home) { + home.featureCatalogue.register({ + id: 'graph', + title: 'Graph', + description: i18n.translate('xpack.graph.pluginDescription', { + defaultMessage: 'Surface and analyze relevant relationships in your Elasticsearch data.', + }), + icon: 'graphApp', + path: '/app/graph', + showOnHomePage: true, + category: FeatureCatalogueCategory.DATA, + }); + } + } + + start(core: CoreStart) { + if (this.licensing === null) { + throw new Error('Start called before setup'); + } + this.licensing.license$.subscribe(license => { + toggleNavLink(checkLicense(license), core.chrome.navLinks); + }); + } + + stop() {} +} diff --git a/x-pack/plugins/graph/public/services/toggle_nav_link.ts b/x-pack/plugins/graph/public/services/toggle_nav_link.ts new file mode 100644 index 0000000000000..be917677d311f --- /dev/null +++ b/x-pack/plugins/graph/public/services/toggle_nav_link.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ChromeNavLink, ChromeNavLinks } from 'kibana/public'; +import { GraphLicenseInformation } from '../../common/check_license'; + +type Mutable = { -readonly [P in keyof T]: T[P] }; +type ChromeNavLinkUpdate = Mutable>; + +export function toggleNavLink( + licenseInformation: GraphLicenseInformation, + navLinks: ChromeNavLinks +) { + const navLinkUpdates: ChromeNavLinkUpdate = { + hidden: !licenseInformation.showAppLink, + }; + if (licenseInformation.showAppLink) { + navLinkUpdates.disabled = !licenseInformation.enableAppLink; + navLinkUpdates.tooltip = licenseInformation.message; + } + + navLinks.update('graph', navLinkUpdates); +} diff --git a/x-pack/legacy/plugins/graph/server/routes/index.js b/x-pack/plugins/graph/server/index.ts similarity index 70% rename from x-pack/legacy/plugins/graph/server/routes/index.js rename to x-pack/plugins/graph/server/index.ts index 6201e885fe59c..ac9ca960c0c7f 100644 --- a/x-pack/legacy/plugins/graph/server/routes/index.js +++ b/x-pack/plugins/graph/server/index.ts @@ -4,5 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { graphExploreRoute } from './graph_explore'; -export { searchProxyRoute } from './search_proxy'; +import { GraphPlugin } from './plugin'; + +export const plugin = () => new GraphPlugin(); diff --git a/x-pack/plugins/graph/server/lib/license_state.ts b/x-pack/plugins/graph/server/lib/license_state.ts new file mode 100644 index 0000000000000..1f5744e41534d --- /dev/null +++ b/x-pack/plugins/graph/server/lib/license_state.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { Observable, Subscription } from 'rxjs'; +import { ILicense } from '../../../licensing/common/types'; +import { checkLicense, GraphLicenseInformation } from '../../common/check_license'; + +export class LicenseState { + private licenseInformation: GraphLicenseInformation = checkLicense(undefined); + private subscription: Subscription | null = null; + + private updateInformation(license: ILicense | undefined) { + this.licenseInformation = checkLicense(license); + } + + public start(license$: Observable) { + this.subscription = license$.subscribe(this.updateInformation.bind(this)); + } + + public stop() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public getLicenseInformation() { + return this.licenseInformation; + } +} + +export function verifyApiAccess(licenseState: LicenseState) { + const licenseCheckResults = licenseState.getLicenseInformation(); + + if (licenseCheckResults.showAppLink && licenseCheckResults.enableAppLink) { + return; + } + + throw Boom.forbidden(licenseCheckResults.message); +} diff --git a/x-pack/plugins/graph/server/plugin.ts b/x-pack/plugins/graph/server/plugin.ts new file mode 100644 index 0000000000000..4b5a17707641f --- /dev/null +++ b/x-pack/plugins/graph/server/plugin.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, CoreSetup } from 'src/core/server'; +import { LicensingPluginSetup } from '../../licensing/common/types'; +import { LicenseState } from './lib/license_state'; +import { registerSearchRoute } from './routes/search'; +import { registerExploreRoute } from './routes/explore'; + +export class GraphPlugin implements Plugin { + private licenseState: LicenseState | null = null; + + public async setup(core: CoreSetup, { licensing }: { licensing: LicensingPluginSetup }) { + const licenseState = new LicenseState(); + licenseState.start(licensing.license$); + this.licenseState = licenseState; + + const router = core.http.createRouter(); + registerSearchRoute({ licenseState, router }); + registerExploreRoute({ licenseState, router }); + } + + public start() {} + public stop() { + if (this.licenseState) { + this.licenseState.stop(); + } + } +} diff --git a/x-pack/plugins/graph/server/routes/explore.ts b/x-pack/plugins/graph/server/routes/explore.ts new file mode 100644 index 0000000000000..0a5b9f62f12a1 --- /dev/null +++ b/x-pack/plugins/graph/server/routes/explore.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import Boom from 'boom'; +import { get } from 'lodash'; +import { LicenseState, verifyApiAccess } from '../lib/license_state'; + +export function registerExploreRoute({ + router, + licenseState, +}: { + router: IRouter; + licenseState: LicenseState; +}) { + router.post( + { + path: '/api/graph/graphExplore', + validate: { + body: schema.object({ + index: schema.string(), + query: schema.object({}, { allowUnknowns: true }), + }), + }, + }, + router.handleLegacyErrors( + async ( + { + core: { + elasticsearch: { + dataClient: { callAsCurrentUser: callCluster }, + }, + }, + }, + request, + response + ) => { + verifyApiAccess(licenseState); + try { + return response.ok({ + body: { + resp: await callCluster('transport.request', { + path: '/' + encodeURIComponent(request.body.index) + '/_graph/explore', + body: request.body.query, + method: 'POST', + query: {}, + }), + }, + }); + } catch (error) { + // Extract known reasons for bad choice of field + const relevantCause = get( + error, + 'body.error.root_cause', + [] as Array<{ type: string; reason: string }> + ).find(cause => { + return ( + cause.reason.includes('Fielddata is disabled on text fields') || + cause.reason.includes('No support for examining floating point') || + cause.reason.includes('Sample diversifying key must be a single valued-field') || + cause.reason.includes('Failed to parse query') || + cause.type === 'parsing_exception' + ); + }); + + if (relevantCause) { + throw Boom.badRequest(relevantCause.reason); + } + + throw Boom.boomify(error); + } + } + ) + ); +} diff --git a/x-pack/plugins/graph/server/routes/search.ts b/x-pack/plugins/graph/server/routes/search.ts new file mode 100644 index 0000000000000..400cdc4e82b6e --- /dev/null +++ b/x-pack/plugins/graph/server/routes/search.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import Boom from 'boom'; +import { LicenseState, verifyApiAccess } from '../lib/license_state'; + +export function registerSearchRoute({ + router, + licenseState, +}: { + router: IRouter; + licenseState: LicenseState; +}) { + router.post( + { + path: '/api/graph/searchProxy', + validate: { + body: schema.object({ + index: schema.string(), + body: schema.object({}, { allowUnknowns: true }), + }), + }, + }, + router.handleLegacyErrors( + async ( + { + core: { + uiSettings: { client: uiSettings }, + elasticsearch: { + dataClient: { callAsCurrentUser: callCluster }, + }, + }, + }, + request, + response + ) => { + verifyApiAccess(licenseState); + const includeFrozen = await uiSettings.get('search:includeFrozen'); + try { + return response.ok({ + body: { + resp: await callCluster('search', { + index: request.body.index, + body: request.body.body, + rest_total_hits_as_int: true, + ignore_throttled: !includeFrozen, + }), + }, + }); + } catch (error) { + throw Boom.boomify(error, { statusCode: error.statusCode || 500 }); + } + } + ) + ); +} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 003c5c9ebc48b..f316352e759a7 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5175,10 +5175,8 @@ "xpack.graph.savedWorkspaces.graphWorkspacesLabel": "グラフワークスペース", "xpack.graph.saveWorkspace.successNotification.noDataSavedText": "構成が保存されましたが、データは保存されませんでした", "xpack.graph.saveWorkspace.successNotificationTitle": "「{workspaceTitle}」が保存されました", - "xpack.graph.serverSideErrors.expiredLicenseErrorMessage": "グラフを利用できません。ライセンスが期限切れです。", "xpack.graph.serverSideErrors.unavailableGraphErrorMessage": "グラフを利用できません", "xpack.graph.serverSideErrors.unavailableLicenseInformationErrorMessage": "グラフを利用できません。現在ライセンス情報が利用できません。", - "xpack.graph.serverSideErrors.wrongLicenseTypeErrorMessage": "現在の {licenseType} ライセンスではグラフを利用できません。ライセンスをアップグレードしてください。", "xpack.graph.settings.advancedSettings.certaintyInputHelpText": "関連用語が登録される前に証拠として必要なドキュメントの最低数です", "xpack.graph.settings.advancedSettings.certaintyInputLabel": "確実性", "xpack.graph.settings.advancedSettings.diversityFieldInputHelpText1": "ドキュメントのサンプルが 1 種類に偏らないように、バイアスの原因の認識に役立つフィールドを選択してください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 746af86eadb1b..d93d21804af0e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5177,10 +5177,8 @@ "xpack.graph.savedWorkspaces.graphWorkspacesLabel": "Graph 工作空间", "xpack.graph.saveWorkspace.successNotification.noDataSavedText": "配置会被保存,但不保存数据", "xpack.graph.saveWorkspace.successNotificationTitle": "已保存“{workspaceTitle}”", - "xpack.graph.serverSideErrors.expiredLicenseErrorMessage": "Graph 不可用 - 许可已过期。", "xpack.graph.serverSideErrors.unavailableGraphErrorMessage": "Graph 不可用", "xpack.graph.serverSideErrors.unavailableLicenseInformationErrorMessage": "Graph 不可用 - 许可信息当前不可用。", - "xpack.graph.serverSideErrors.wrongLicenseTypeErrorMessage": "当前{licenseType}许可的 Graph 不可用。请升级您的许可。", "xpack.graph.settings.advancedSettings.certaintyInputHelpText": "在引入相关字词之前作为证据所需的最小文档数量。", "xpack.graph.settings.advancedSettings.certaintyInputLabel": "确定性", "xpack.graph.settings.advancedSettings.diversityFieldInputHelpText1": "为避免文档示例过于雷同,请选取有助于识别偏差来源的字段。", From 43e6f0663cd03bdb80d1b755830cd3cea704891b Mon Sep 17 00:00:00 2001 From: Chris Davies Date: Fri, 13 Dec 2019 08:50:19 -0500 Subject: [PATCH 33/36] [Lens] Modify merge tables to use the same logic as auto date (#52931) (#52951) --- .../editor_frame_plugin/merge_tables.test.ts | 35 +++++++++++++++++++ .../editor_frame_plugin/merge_tables.ts | 12 +++---- .../public/indexpattern_plugin/auto_date.ts | 26 ++++++++++++-- 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.test.ts index 6f0af07265516..8f124c25542c7 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; import { mergeTables } from './merge_tables'; import { KibanaDatatable } from 'src/plugins/expressions/public'; @@ -69,4 +70,38 @@ describe('lens_merge_tables', () => { } `); }); + + it('should handle this week now/w', () => { + const { dateRange } = mergeTables.fn( + { + type: 'kibana_context', + timeRange: { + from: 'now/w', + to: 'now/w', + }, + }, + { layerIds: ['first', 'second'], tables: [] }, + {} + ); + + expect( + moment + .duration( + moment() + .startOf('week') + .diff(dateRange!.fromDate) + ) + .asDays() + ).toEqual(0); + + expect( + moment + .duration( + moment() + .endOf('week') + .diff(dateRange!.toDate) + ) + .asDays() + ).toEqual(0); + }); }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts index 5f47898b4f632..dc03be894a87c 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts @@ -5,9 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import dateMath from '@elastic/datemath'; import { ExpressionFunction, KibanaContext, KibanaDatatable } from 'src/plugins/expressions/public'; import { LensMultiTable } from '../types'; +import { toAbsoluteDates } from '../indexpattern_plugin/auto_date'; interface MergeTables { layerIds: string[]; @@ -58,15 +58,11 @@ function getDateRange(ctx?: KibanaContext | null) { return; } - const fromDate = dateMath.parse(ctx.timeRange.from); - const toDate = dateMath.parse(ctx.timeRange.to); + const dateRange = toAbsoluteDates({ fromDate: ctx.timeRange.from, toDate: ctx.timeRange.to }); - if (!fromDate || !toDate) { + if (!dateRange) { return; } - return { - fromDate: fromDate.toDate(), - toDate: toDate.toDate(), - }; + return dateRange; } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts index dec0adba98103..b62585f5da09a 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts @@ -16,16 +16,36 @@ interface LensAutoDateProps { aggConfigs: string; } -export function autoIntervalFromDateRange(dateRange?: DateRange, defaultValue: string = '1h') { +export function toAbsoluteDates(dateRange?: DateRange) { if (!dateRange) { + return; + } + + const fromDate = dateMath.parse(dateRange.fromDate); + const toDate = dateMath.parse(dateRange.toDate, { roundUp: true }); + + if (!fromDate || !toDate) { + return; + } + + return { + fromDate: fromDate.toDate(), + toDate: toDate.toDate(), + }; +} + +export function autoIntervalFromDateRange(dateRange?: DateRange, defaultValue: string = '1h') { + const dates = toAbsoluteDates(dateRange); + if (!dates) { return defaultValue; } const buckets = new TimeBuckets(); + buckets.setInterval('auto'); buckets.setBounds({ - min: dateMath.parse(dateRange.fromDate), - max: dateMath.parse(dateRange.toDate, { roundUp: true }), + min: dates.fromDate, + max: dates.toDate, }); return buckets.getInterval().expression; From 425fb13fd8f43b170eb120c9ad5cd2507725f645 Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Fri, 13 Dec 2019 09:29:44 -0500 Subject: [PATCH 34/36] Fix and turn back on legacy embeddable tests (#52945) (#52973) --- test/plugin_functional/config.js | 13 +------------ .../public/np_ready/public/legacy.ts | 2 ++ 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/test/plugin_functional/config.js b/test/plugin_functional/config.js index d8ce12d1fc612..4ca3d86f7043c 100644 --- a/test/plugin_functional/config.js +++ b/test/plugin_functional/config.js @@ -33,18 +33,7 @@ export default async function ({ readConfigFile }) { require.resolve('./test_suites/app_plugins'), require.resolve('./test_suites/custom_visualizations'), require.resolve('./test_suites/panel_actions'), - - /** - * @todo Work on re-enabling this test suite after this is merged. These tests pass - * locally but on CI they fail. The error on CI says "TypeError: Cannot read - * property 'overlays' of null". Possibly those are `overlays` from - * `npStart.core.overlays`, possibly `npStart.core` is `null` on CI, but - * available when this test suite is executed locally. - * - * See issue: https://github.com/elastic/kibana/issues/43087 - */ - // require.resolve('./test_suites/embeddable_explorer'), - + require.resolve('./test_suites/embeddable_explorer'), require.resolve('./test_suites/core_plugins'), ], services: { diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/legacy.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/legacy.ts index 1928d7ac72313..57b5ad086e6a7 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/legacy.ts +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/legacy.ts @@ -18,6 +18,8 @@ */ /* eslint-disable @kbn/eslint/no-restricted-paths */ import 'ui/autoload/all'; + +import 'uiExports/interpreter'; import 'uiExports/embeddableFactories'; import 'uiExports/embeddableActions'; From 9e1d91fcc5152914e28a5f32ff959610ef193174 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Fri, 13 Dec 2019 14:35:08 +0000 Subject: [PATCH 35/36] Move search bar to NP (#52622) (#52983) * Moved SearchBar component to NP * move search bar to NP * change import * Revert "change import" This reverts commit 52d990fbe4d16a794b3096477da0824a3cc5cef5. * Revert "move search bar to NP" This reverts commit 4196e8b4b9d65f76ea54fec4d8274eba816a4e3c. * move create search bar * Clean up jest mocks * update snapshots * Remove indexPatterns angular service * Deleted comment * Mock npStart.core.savedObjects * Removed export of SearchBarOwnProps * fix import * Fix typo * eslint --- src/legacy/core_plugins/data/public/index.ts | 7 +---- src/legacy/core_plugins/data/public/plugin.ts | 30 +++---------------- .../core_plugins/data/public/search/index.ts | 2 -- .../search/search_bar/components/index.tsx | 21 ------------- .../data/public/shim/legacy_module.ts | 29 ------------------ .../create_index_pattern_wizard/index.js | 4 +-- .../create_edit_field/create_edit_field.js | 4 ++- .../edit_index_pattern/edit_index_pattern.js | 6 ++-- .../management/sections/objects/_objects.js | 2 +- .../core_plugins/navigation/public/legacy.ts | 3 +- .../core_plugins/navigation/public/plugin.ts | 4 +-- .../top_nav_menu/create_top_nav_menu.tsx | 4 +-- .../public/top_nav_menu/top_nav_menu.test.tsx | 2 -- .../public/top_nav_menu/top_nav_menu.tsx | 4 +-- .../arg_value_suggestions.js | 8 ++--- .../new_platform/new_platform.karma_mock.js | 2 ++ .../saved_objects/__tests__/saved_object.js | 18 +++++++++-- .../index_patterns/index_pattern.test.ts | 15 ---------- src/plugins/data/public/mocks.ts | 1 + src/plugins/data/public/plugin.ts | 23 ++++++++++---- src/plugins/data/public/types.ts | 2 ++ src/plugins/data/public/ui/index.ts | 2 +- .../query_string_input.test.tsx.snap | 6 ++++ .../ui/search_bar}/create_search_bar.tsx | 28 ++++++++--------- .../data/public/ui}/search_bar/index.tsx | 2 +- .../public/ui/search_bar}/search_bar.test.tsx | 13 +++++--- .../data/public/ui/search_bar}/search_bar.tsx | 11 +++---- .../filter_editor/filter_editor.js | 4 +-- .../join_editor/resources/where_expression.js | 2 +- .../components/query_bar/index.test.tsx | 3 +- .../public/components/query_bar/index.tsx | 3 +- .../public/components/search_bar/index.tsx | 6 +--- 32 files changed, 105 insertions(+), 166 deletions(-) delete mode 100644 src/legacy/core_plugins/data/public/search/search_bar/components/index.tsx delete mode 100644 src/legacy/core_plugins/data/public/shim/legacy_module.ts rename src/{legacy/core_plugins/data/public/search/search_bar/components => plugins/data/public/ui/search_bar}/create_search_bar.tsx (82%) rename src/{legacy/core_plugins/data/public/search => plugins/data/public/ui}/search_bar/index.tsx (93%) rename src/{legacy/core_plugins/data/public/search/search_bar/components => plugins/data/public/ui/search_bar}/search_bar.test.tsx (96%) rename src/{legacy/core_plugins/data/public/search/search_bar/components => plugins/data/public/ui/search_bar}/search_bar.tsx (98%) diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index 481349463fc56..c9ce825f3596e 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -30,12 +30,7 @@ export function plugin() { export { DataStart }; export { Field, FieldType, IFieldList, IndexPattern } from './index_patterns'; -export { SearchBar, SearchBarProps } from './search'; -export { - SavedQueryAttributes, - SavedQuery, - SavedQueryTimeFilter, -} from '../../../../plugins/data/public'; +export { SavedQuery, SavedQueryTimeFilter } from '../../../../plugins/data/public'; /** @public static code */ export * from '../common'; diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts index a4fdfd7482f74..676904faf6f2f 100644 --- a/src/legacy/core_plugins/data/public/plugin.ts +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -18,10 +18,7 @@ */ import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; -import { createSearchBar, StatetfulSearchBarProps } from './search'; -import { Storage, IStorageWrapper } from '../../../../../src/plugins/kibana_utils/public'; import { DataPublicPluginStart } from '../../../../plugins/data/public'; -import { initLegacyModule } from './shim/legacy_module'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { setFieldFormats } from '../../../../plugins/data/public/services'; @@ -35,11 +32,8 @@ export interface DataPluginStartDependencies { * * @public */ -export interface DataStart { - ui: { - SearchBar: React.ComponentType; - }; -} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface DataStart {} /** * Data Plugin - public @@ -54,28 +48,12 @@ export interface DataStart { */ export class DataPlugin implements Plugin { - private storage!: IStorageWrapper; - - public setup(core: CoreSetup) { - this.storage = new Storage(window.localStorage); - } + public setup(core: CoreSetup) {} public start(core: CoreStart, { data }: DataPluginStartDependencies): DataStart { // This is required for when Angular code uses Field and FieldList. setFieldFormats(data.fieldFormats); - initLegacyModule(data.indexPatterns); - - const SearchBar = createSearchBar({ - core, - data, - storage: this.storage, - }); - - return { - ui: { - SearchBar, - }, - }; + return {}; } public stop() {} diff --git a/src/legacy/core_plugins/data/public/search/index.ts b/src/legacy/core_plugins/data/public/search/index.ts index 24ffa939a746e..9880b336e76e5 100644 --- a/src/legacy/core_plugins/data/public/search/index.ts +++ b/src/legacy/core_plugins/data/public/search/index.ts @@ -16,5 +16,3 @@ * specific language governing permissions and limitations * under the License. */ - -export * from './search_bar'; diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/index.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/index.tsx deleted file mode 100644 index accaac163acfc..0000000000000 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export * from './search_bar'; -export * from './create_search_bar'; diff --git a/src/legacy/core_plugins/data/public/shim/legacy_module.ts b/src/legacy/core_plugins/data/public/shim/legacy_module.ts deleted file mode 100644 index 1916097b0335c..0000000000000 --- a/src/legacy/core_plugins/data/public/shim/legacy_module.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { once } from 'lodash'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -import { IndexPatternsContract } from 'src/plugins/data/public'; - -/** @internal */ -export const initLegacyModule = once((indexPatterns: IndexPatternsContract): void => { - uiModules.get('kibana/index_patterns').value('indexPatterns', indexPatterns); -}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js index 833ca8467292e..fd92987614283 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js @@ -20,7 +20,7 @@ import { SavedObjectsClientProvider } from 'ui/saved_objects'; import uiRoutes from 'ui/routes'; import angularTemplate from './angular_template.html'; -import 'ui/index_patterns'; +import { npStart } from 'ui/new_platform'; import { setup as managementSetup } from '../../../../../../management/public/legacy'; import { getCreateBreadcrumbs } from '../breadcrumbs'; @@ -41,7 +41,7 @@ uiRoutes.when('/management/kibana/index_pattern', { const services = { config: $injector.get('config'), es: $injector.get('es'), - indexPatterns: $injector.get('indexPatterns'), + indexPatterns: npStart.plugins.data.indexPatterns, $http: $injector.get('$http'), savedObjectsClient: Private(SavedObjectsClientProvider), indexPatternCreationType, diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js index 5f0994abc11e4..f1c2d6598b134 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js @@ -23,6 +23,7 @@ import { docTitle } from 'ui/doc_title'; import { KbnUrlProvider } from 'ui/url'; import uiRoutes from 'ui/routes'; import { toastNotifications } from 'ui/notify'; +import { npStart } from 'ui/new_platform'; import template from './create_edit_field.html'; import { getEditFieldBreadcrumbs, getCreateFieldBreadcrumbs } from '../../breadcrumbs'; @@ -96,7 +97,8 @@ uiRoutes }); }, resolve: { - indexPattern: function ($route, Promise, redirectWhenMissing, indexPatterns) { + indexPattern: function ($route, Promise, redirectWhenMissing) { + const { indexPatterns } = npStart.plugins.data; return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)) .catch(redirectWhenMissing('/management/kibana/index_patterns')); } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js index dd3e6ccdc426c..c2f9ed13c75d1 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js @@ -36,6 +36,7 @@ import { IndexedFieldsTable } from './indexed_fields_table'; import { ScriptedFieldsTable } from './scripted_fields_table'; import { i18n } from '@kbn/i18n'; import { I18nContext } from 'ui/i18n'; +import { npStart } from 'ui/new_platform'; import { getEditBreadcrumbs } from '../breadcrumbs'; @@ -168,7 +169,8 @@ uiRoutes.when('/management/kibana/index_patterns/:indexPatternId', { template, k7Breadcrumbs: getEditBreadcrumbs, resolve: { - indexPattern: function ($route, Promise, redirectWhenMissing, indexPatterns) { + indexPattern: function ($route, Promise, redirectWhenMissing) { + const { indexPatterns } = npStart.plugins.data; return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch( redirectWhenMissing('/management/kibana/index_patterns') ); @@ -179,7 +181,7 @@ uiRoutes.when('/management/kibana/index_patterns/:indexPatternId', { uiModules .get('apps/management') .controller('managementIndexPatternsEdit', function ( - $scope, $location, $route, Promise, config, indexPatterns, Private, AppState, confirmModal) { + $scope, $location, $route, Promise, config, Private, AppState, confirmModal) { const $state = $scope.state = new AppState(); $scope.fieldWildcardMatcher = (...args) => fieldWildcardMatcher(...args, config.get('metaFields')); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js index 32cdce1bb4ac4..bd3aa20d097c9 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js @@ -36,7 +36,7 @@ const REACT_OBJECTS_TABLE_DOM_ELEMENT_ID = 'reactSavedObjectsTable'; function updateObjectsTable($scope, $injector) { const Private = $injector.get('Private'); - const indexPatterns = $injector.get('indexPatterns'); + const indexPatterns = npStart.plugins.data.indexPatterns; const $http = $injector.get('$http'); const kbnUrl = $injector.get('kbnUrl'); const config = $injector.get('config'); diff --git a/src/legacy/core_plugins/navigation/public/legacy.ts b/src/legacy/core_plugins/navigation/public/legacy.ts index 1783514e6fbdc..8a2fdb5ea4964 100644 --- a/src/legacy/core_plugins/navigation/public/legacy.ts +++ b/src/legacy/core_plugins/navigation/public/legacy.ts @@ -18,7 +18,6 @@ */ import { npSetup, npStart } from 'ui/new_platform'; -import { start as dataShim } from '../../data/public/legacy'; import { plugin } from '.'; const navPlugin = plugin(); @@ -26,5 +25,5 @@ const navPlugin = plugin(); export const setup = navPlugin.setup(npSetup.core); export const start = navPlugin.start(npStart.core, { - data: dataShim, + data: npStart.plugins.data, }); diff --git a/src/legacy/core_plugins/navigation/public/plugin.ts b/src/legacy/core_plugins/navigation/public/plugin.ts index 65a0902dec986..27533e6c8fcf6 100644 --- a/src/legacy/core_plugins/navigation/public/plugin.ts +++ b/src/legacy/core_plugins/navigation/public/plugin.ts @@ -18,10 +18,10 @@ */ import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; import { TopNavMenuExtensionsRegistry, TopNavMenuExtensionsRegistrySetup } from './top_nav_menu'; import { createTopNav } from './top_nav_menu/create_top_nav_menu'; import { TopNavMenuProps } from './top_nav_menu/top_nav_menu'; -import { DataStart } from '../../data/public'; /** * Interface for this plugin's returned `setup` contract. @@ -44,7 +44,7 @@ export interface NavigationStart { } export interface NavigationPluginStartDependencies { - data: DataStart; + data: DataPublicPluginStart; } export class NavigationPlugin implements Plugin { diff --git a/src/legacy/core_plugins/navigation/public/top_nav_menu/create_top_nav_menu.tsx b/src/legacy/core_plugins/navigation/public/top_nav_menu/create_top_nav_menu.tsx index fa0e5fcac7407..0b3b7b5b2a272 100644 --- a/src/legacy/core_plugins/navigation/public/top_nav_menu/create_top_nav_menu.tsx +++ b/src/legacy/core_plugins/navigation/public/top_nav_menu/create_top_nav_menu.tsx @@ -18,11 +18,11 @@ */ import React from 'react'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; import { TopNavMenuProps, TopNavMenu } from './top_nav_menu'; import { TopNavMenuData } from './top_nav_menu_data'; -import { DataStart } from '../../../../core_plugins/data/public'; -export function createTopNav(data: DataStart, extraConfig: TopNavMenuData[]) { +export function createTopNav(data: DataPublicPluginStart, extraConfig: TopNavMenuData[]) { return (props: TopNavMenuProps) => { const config = (props.config || []).concat(extraConfig); diff --git a/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx b/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx index 9077de8910327..4e2ea44bf7642 100644 --- a/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx +++ b/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx @@ -22,8 +22,6 @@ import { TopNavMenu } from './top_nav_menu'; import { TopNavMenuData } from './top_nav_menu_data'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -jest.mock('ui/new_platform'); - const mockTimeHistory = { add: () => {}, get: () => { diff --git a/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu.tsx b/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu.tsx index 14599e76470c0..d5f951ea3eeea 100644 --- a/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu.tsx +++ b/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu.tsx @@ -24,13 +24,13 @@ import { I18nProvider } from '@kbn/i18n/react'; import { TopNavMenuData } from './top_nav_menu_data'; import { TopNavMenuItem } from './top_nav_menu_item'; -import { SearchBarProps, DataStart } from '../../../../core_plugins/data/public'; +import { SearchBarProps, DataPublicPluginStart } from '../../../../../plugins/data/public'; export type TopNavMenuProps = Partial & { appName: string; config?: TopNavMenuData[]; showSearchBar?: boolean; - data?: DataStart; + data?: DataPublicPluginStart; }; /* diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_suggestions/arg_value_suggestions.js b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_suggestions/arg_value_suggestions.js index 81d459f70a5e1..dc3a5be40dff4 100644 --- a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_suggestions/arg_value_suggestions.js +++ b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_suggestions/arg_value_suggestions.js @@ -18,11 +18,11 @@ */ import _ from 'lodash'; -import { SavedObjectsClientProvider } from 'ui/saved_objects'; +import { npStart } from 'ui/new_platform'; -export function ArgValueSuggestionsProvider(Private, indexPatterns) { - - const savedObjectsClient = Private(SavedObjectsClientProvider); +export function ArgValueSuggestionsProvider() { + const { indexPatterns } = npStart.plugins.data; + const { client: savedObjectsClient } = npStart.core.savedObjects; async function getIndexPattern(functionArgs) { const indexPatternArg = functionArgs.find(argument => { diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index f8850d1691cdd..6a1ee9663e805 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -153,8 +153,10 @@ export const npStart = { getProvider: sinon.fake(), }, getSuggestions: sinon.fake(), + indexPatterns: sinon.fake(), ui: { IndexPatternSelect: mockComponent, + SearchBar: mockComponent, }, query: { filterManager: { diff --git a/src/legacy/ui/public/saved_objects/__tests__/saved_object.js b/src/legacy/ui/public/saved_objects/__tests__/saved_object.js index 55d0f800ae7ff..720fc63fb89e7 100644 --- a/src/legacy/ui/public/saved_objects/__tests__/saved_object.js +++ b/src/legacy/ui/public/saved_objects/__tests__/saved_object.js @@ -24,7 +24,7 @@ import Bluebird from 'bluebird'; import { createSavedObjectClass } from '../saved_object'; import StubIndexPattern from 'test_utils/stub_index_pattern'; -import { SavedObjectsClientProvider } from '../saved_objects_client_provider'; +import { npStart } from 'ui/new_platform'; import { InvalidJSONProperty } from '../../../../../plugins/kibana_utils/public'; import { mockUiSettings } from '../../new_platform/new_platform.karma_mock'; @@ -82,6 +82,10 @@ describe('Saved Object', function () { return savedObject.init(); } + function restoreIfWrapped(obj, fName) { + obj[fName].restore && obj[fName].restore(); + } + const mock409FetchError = { res: { status: 409 } }; @@ -96,13 +100,21 @@ describe('Saved Object', function () { }) ); - beforeEach(ngMock.inject(function (es, Private, $window) { - savedObjectsClientStub = Private(SavedObjectsClientProvider); + beforeEach(ngMock.inject(function (es, $window) { + savedObjectsClientStub = npStart.core.savedObjects.client; SavedObject = createSavedObjectClass({ savedObjectsClient: savedObjectsClientStub }); esDataStub = es; window = $window; })); + afterEach(ngMock.inject(function () { + restoreIfWrapped(savedObjectsClientStub, 'create'); + restoreIfWrapped(savedObjectsClientStub, 'get'); + restoreIfWrapped(savedObjectsClientStub, 'update'); + restoreIfWrapped(savedObjectsClientStub, 'find'); + restoreIfWrapped(savedObjectsClientStub, 'bulkGet'); + })); + describe('save', function () { describe('with confirmOverwrite', function () { function stubConfirmOverwrite() { diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts index f56f94fa8c260..1f83e4bd5b80c 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts @@ -33,8 +33,6 @@ import { setNotifications, setFieldFormats } from '../../services'; import { notificationServiceMock } from '../../../../../core/public/notifications/notifications_service.mock'; import { FieldFormatRegisty } from '../../field_formats_provider'; -jest.mock('ui/new_platform'); - jest.mock('../../../../kibana_utils/public', () => { const originalModule = jest.requireActual('../../../../kibana_utils/public'); @@ -50,19 +48,6 @@ jest.mock('../../../../kibana_utils/public', () => { }; }); -jest.mock('ui/notify', () => ({ - toastNotifications: { - addDanger: jest.fn(), - addError: jest.fn(), - }, -})); - -jest.mock('ui/saved_objects', () => { - return { - findObjectByTitle: jest.fn(), - }; -}); - let mockFieldsFetcherResponse: any[] = []; jest.mock('./_fields_fetcher', () => ({ diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 058e6c0e2f5c5..03d3dad61ed05 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -74,6 +74,7 @@ const createStartContract = (): Start => { query: queryStartMock, ui: { IndexPatternSelect: jest.fn(), + SearchBar: jest.fn(), }, indexPatterns: {} as IndexPatternsContract, }; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 2a37be7f3f46a..cd55048ca527f 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -18,7 +18,7 @@ */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { Storage } from '../../kibana_utils/public'; +import { Storage, IStorageWrapper } from '../../kibana_utils/public'; import { DataPublicPluginSetup, DataPublicPluginStart, @@ -35,24 +35,26 @@ import { IndexPatterns } from './index_patterns'; import { setNotifications, setFieldFormats, setOverlays, setIndexPatterns } from './services'; import { createFilterAction, GLOBAL_APPLY_FILTER_ACTION } from './actions'; import { APPLY_FILTER_TRIGGER } from '../../embeddable/public'; +import { createSearchBar } from './ui/search_bar/create_search_bar'; export class DataPublicPlugin implements Plugin { private readonly autocomplete = new AutocompleteProviderRegister(); private readonly searchService: SearchService; private readonly fieldFormatsService: FieldFormatsService; private readonly queryService: QueryService; + private readonly storage: IStorageWrapper; constructor(initializerContext: PluginInitializerContext) { this.searchService = new SearchService(initializerContext); this.queryService = new QueryService(); this.fieldFormatsService = new FieldFormatsService(); + this.storage = new Storage(window.localStorage); } public setup(core: CoreSetup, { uiActions }: DataSetupDependencies): DataPublicPluginSetup { - const storage = new Storage(window.localStorage); const queryService = this.queryService.setup({ uiSettings: core.uiSettings, - storage, + storage: this.storage, }); uiActions.registerAction( @@ -79,16 +81,27 @@ export class DataPublicPlugin implements Plugin; + SearchBar: React.ComponentType; }; } diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts index 8bfccd49bdff3..cd4ec3c3bf74b 100644 --- a/src/plugins/data/public/ui/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -21,8 +21,8 @@ export { SuggestionsComponent } from './typeahead/suggestions_component'; export { IndexPatternSelect } from './index_pattern_select'; export { FilterBar } from './filter_bar'; export { QueryStringInput } from './query_string_input/query_string_input'; +export { SearchBar, SearchBarProps } from './search_bar'; // temp export - will be removed as final components are migrated to NP -export { QueryBarTopRow } from './query_string_input/query_bar_top_row'; export { SavedQueryManagementComponent } from './saved_query_management'; export { SaveQueryForm, SavedQueryMeta } from './saved_query_form'; diff --git a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap index 80a5ede567054..4de883295bd8a 100644 --- a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap +++ b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap @@ -209,6 +209,7 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA }, "ui": Object { "IndexPatternSelect": [MockFunction], + "SearchBar": [MockFunction], }, }, "docLinks": Object { @@ -829,6 +830,7 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA }, "ui": Object { "IndexPatternSelect": [MockFunction], + "SearchBar": [MockFunction], }, }, "docLinks": Object { @@ -1437,6 +1439,7 @@ exports[`QueryStringInput Should pass the query language to the language switche }, "ui": Object { "IndexPatternSelect": [MockFunction], + "SearchBar": [MockFunction], }, }, "docLinks": Object { @@ -2054,6 +2057,7 @@ exports[`QueryStringInput Should pass the query language to the language switche }, "ui": Object { "IndexPatternSelect": [MockFunction], + "SearchBar": [MockFunction], }, }, "docLinks": Object { @@ -2662,6 +2666,7 @@ exports[`QueryStringInput Should render the given query 1`] = ` }, "ui": Object { "IndexPatternSelect": [MockFunction], + "SearchBar": [MockFunction], }, }, "docLinks": Object { @@ -3279,6 +3284,7 @@ exports[`QueryStringInput Should render the given query 1`] = ` }, "ui": Object { "IndexPatternSelect": [MockFunction], + "SearchBar": [MockFunction], }, }, "docLinks": Object { diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx similarity index 82% rename from src/legacy/core_plugins/data/public/search/search_bar/components/create_search_bar.tsx rename to src/plugins/data/public/ui/search_bar/create_search_bar.tsx index 125c6b8dad006..6f1be2825dd01 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -21,29 +21,29 @@ import React, { useState, useEffect } from 'react'; import { Subscription } from 'rxjs'; import { CoreStart } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; -import { SearchBar } from '../../../'; -import { SearchBarOwnProps } from '.'; -import { DataPublicPluginStart, esFilters } from '../../../../../../../plugins/data/public'; +import { KibanaContextProvider } from '../../../../kibana_react/public'; +import { DataPublicPluginStart, esFilters } from '../..'; +import { QueryStart } from '../../query'; +import { SearchBarOwnProps, SearchBar } from './search_bar'; interface StatefulSearchBarDeps { core: CoreStart; - data: DataPublicPluginStart; + data: Omit; storage: IStorageWrapper; } -export type StatetfulSearchBarProps = SearchBarOwnProps & { +export type StatefulSearchBarProps = SearchBarOwnProps & { appName: string; }; -const defaultFiltersUpdated = (data: DataPublicPluginStart) => { +const defaultFiltersUpdated = (query: QueryStart) => { return (filters: esFilters.Filter[]) => { - data.query.filterManager.setFilters(filters); + query.filterManager.setFilters(filters); }; }; -const defaultOnRefreshChange = (data: DataPublicPluginStart) => { - const { timefilter } = data.query.timefilter; +const defaultOnRefreshChange = (query: QueryStart) => { + const { timefilter } = query.timefilter; return (options: { isPaused: boolean; refreshInterval: number }) => { timefilter.setRefreshInterval({ value: options.refreshInterval, @@ -55,7 +55,7 @@ const defaultOnRefreshChange = (data: DataPublicPluginStart) => { export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) { // App name should come from the core application service. // Until it's available, we'll ask the user to provide it for the pre-wired component. - return (props: StatetfulSearchBarProps) => { + return (props: StatefulSearchBarProps) => { const { filterManager, timefilter } = data.query; const tfRefreshInterval = timefilter.timefilter.getRefreshInterval(); const fmFilters = filterManager.getFilters(); @@ -98,7 +98,7 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) isSubscribed = false; subscriptions.unsubscribe(); }; - }, []); + }, [filterManager, timefilter.timefilter]); return ( diff --git a/src/legacy/core_plugins/data/public/search/search_bar/index.tsx b/src/plugins/data/public/ui/search_bar/index.tsx similarity index 93% rename from src/legacy/core_plugins/data/public/search/search_bar/index.tsx rename to src/plugins/data/public/ui/search_bar/index.tsx index faf6e24aa6ed5..4aa7f5fe2b040 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/index.tsx +++ b/src/plugins/data/public/ui/search_bar/index.tsx @@ -17,4 +17,4 @@ * under the License. */ -export * from './components'; +export { SearchBar, SearchBarProps } from './search_bar'; diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx b/src/plugins/data/public/ui/search_bar/search_bar.test.tsx similarity index 96% rename from src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx rename to src/plugins/data/public/ui/search_bar/search_bar.test.tsx index 5752d6a502225..56d444761153f 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.test.tsx @@ -19,15 +19,15 @@ import React from 'react'; import { SearchBar } from './search_bar'; -import { IndexPattern } from '../../../index_patterns'; import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; import { I18nProvider } from '@kbn/i18n/react'; -import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { coreMock } from '../../../../../core/public/mocks'; const startMock = coreMock.createStart(); import { mount } from 'enzyme'; +import { IIndexPattern } from '../..'; const mockTimeHistory = { get: () => { @@ -35,9 +35,14 @@ const mockTimeHistory = { }, }; -jest.mock('../../../../../../../plugins/data/public', () => { +jest.mock('../..', () => { return { FilterBar: () =>
, + }; +}); + +jest.mock('../query_string_input/query_bar_top_row', () => { + return { QueryBarTopRow: () =>
, }; }); @@ -74,7 +79,7 @@ const mockIndexPattern = { searchable: true, }, ], -} as IndexPattern; +} as IIndexPattern; const kqlQuery = { query: 'response:200', diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx similarity index 98% rename from src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx rename to src/plugins/data/public/ui/search_bar/search_bar.tsx index f547fada4a3b1..ceaeb24e7fe7c 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -24,10 +24,7 @@ import React, { Component } from 'react'; import ResizeObserver from 'resize-observer-polyfill'; import { get, isEqual } from 'lodash'; -import { - withKibana, - KibanaReactContextValue, -} from '../../../../../../../plugins/kibana_react/public'; +import { withKibana, KibanaReactContextValue } from '../../../../kibana_react/public'; import { IDataPluginServices, TimeRange, @@ -37,12 +34,12 @@ import { TimeHistoryContract, FilterBar, SavedQuery, - SavedQueryAttributes, SavedQueryMeta, SaveQueryForm, SavedQueryManagementComponent, - QueryBarTopRow, -} from '../../../../../../../plugins/data/public'; + SavedQueryAttributes, +} from '../..'; +import { QueryBarTopRow } from '../query_string_input/query_bar_top_row'; interface SearchBarInjectedDeps { kibana: KibanaReactContextValue; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js index 941694c19ad56..fc47ea6c36a01 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js @@ -25,10 +25,8 @@ import { i18n } from '@kbn/i18n'; import { indexPatternService } from '../../../kibana_services'; import { GlobalFilterCheckbox } from '../../../components/global_filter_checkbox'; -import { start as data } from '../../../../../../../../src/legacy/core_plugins/data/public/legacy'; -const { SearchBar } = data.ui; - import { npStart } from 'ui/new_platform'; +const { SearchBar } = npStart.plugins.data.ui; export class FilterEditor extends Component { state = { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js index fb09ed342b8d3..4fc1eba1f1e2f 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js @@ -13,8 +13,8 @@ import { EuiFormHelpText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SearchBar } from 'plugins/data'; import { npStart } from 'ui/new_platform'; +const { SearchBar } = npStart.plugins.data.ui; export class WhereExpression extends Component { diff --git a/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx index ce102d7ade53b..10b769e2a791c 100644 --- a/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx @@ -7,8 +7,7 @@ import { mount } from 'enzyme'; import React from 'react'; -import { SearchBar } from '../../../../../../../src/legacy/core_plugins/data/public'; -import { FilterManager } from '../../../../../../../src/plugins/data/public'; +import { FilterManager, SearchBar } from '../../../../../../../src/plugins/data/public'; import { uiSettingsServiceMock } from '../../../../../../../src/core/public/ui_settings/ui_settings_service.mock'; import { useKibanaCore } from '../../lib/compose/kibana_core'; import { TestProviders, mockIndexPattern } from '../../mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx index bf440e238c2a3..9f706790bec67 100644 --- a/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx @@ -8,7 +8,6 @@ import { isEqual } from 'lodash/fp'; import React, { memo, useState, useEffect, useMemo, useCallback } from 'react'; import { IndexPattern } from 'ui/index_patterns'; -import { SavedQuery, SearchBar } from '../../../../../../../src/legacy/core_plugins/data/public'; import { esFilters, IIndexPattern, @@ -16,6 +15,8 @@ import { Query, TimeHistory, TimeRange, + SavedQuery, + SearchBar, SavedQueryTimeFilter, } from '../../../../../../../src/plugins/data/public'; import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; diff --git a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx index fa9ff1e16ddb7..3d02cff7b72e8 100644 --- a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx @@ -16,7 +16,6 @@ import { SavedQuery } from 'src/legacy/core_plugins/data/public'; import { OnTimeChangeProps } from '@elastic/eui'; import { npStart } from 'ui/new_platform'; -import { start as data } from '../../../../../../../src/legacy/core_plugins/data/public/legacy'; import { inputsActions } from '../../store/inputs'; import { InputsRange } from '../../store/inputs/model'; @@ -37,12 +36,9 @@ import { import { timelineActions, hostsActions, networkActions } from '../../store/actions'; import { TimeRange, Query, esFilters } from '../../../../../../../src/plugins/data/public'; -const { - ui: { SearchBar }, -} = data; - export const siemFilterManager = npStart.plugins.data.query.filterManager; export const savedQueryService = npStart.plugins.data.query.savedQueries; +const { SearchBar } = npStart.plugins.data.ui; interface SiemSearchBarRedux { end: number; From 938c70459a294d24fe79d0505e13d90a66ae38da Mon Sep 17 00:00:00 2001 From: Gil Raphaelli Date: Fri, 13 Dec 2019 10:05:54 -0500 Subject: [PATCH 36/36] update apm index pattern (#52938) --- .../kibana/server/tutorials/apm/index_pattern.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/legacy/core_plugins/kibana/server/tutorials/apm/index_pattern.json b/src/legacy/core_plugins/kibana/server/tutorials/apm/index_pattern.json index f54c2fa35f80d..aac00fd08848d 100644 --- a/src/legacy/core_plugins/kibana/server/tutorials/apm/index_pattern.json +++ b/src/legacy/core_plugins/kibana/server/tutorials/apm/index_pattern.json @@ -1,7 +1,7 @@ { "attributes": { - "fieldFormatMap": "{\"client.bytes\":{\"id\":\"bytes\"},\"client.nat.port\":{\"id\":\"string\"},\"client.port\":{\"id\":\"string\"},\"destination.bytes\":{\"id\":\"bytes\"},\"destination.nat.port\":{\"id\":\"string\"},\"destination.port\":{\"id\":\"string\"},\"event.duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"nanoseconds\",\"outputFormat\":\"asMilliseconds\",\"outputPrecision\":1}},\"event.sequence\":{\"id\":\"string\"},\"event.severity\":{\"id\":\"string\"},\"http.request.body.bytes\":{\"id\":\"bytes\"},\"http.request.bytes\":{\"id\":\"bytes\"},\"http.response.body.bytes\":{\"id\":\"bytes\"},\"http.response.bytes\":{\"id\":\"bytes\"},\"http.response.status_code\":{\"id\":\"string\"},\"network.bytes\":{\"id\":\"bytes\"},\"process.pgid\":{\"id\":\"string\"},\"process.pid\":{\"id\":\"string\"},\"process.ppid\":{\"id\":\"string\"},\"process.thread.id\":{\"id\":\"string\"},\"server.bytes\":{\"id\":\"bytes\"},\"server.nat.port\":{\"id\":\"string\"},\"server.port\":{\"id\":\"string\"},\"source.bytes\":{\"id\":\"bytes\"},\"source.nat.port\":{\"id\":\"string\"},\"source.port\":{\"id\":\"string\"},\"system.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.memory.actual.free\":{\"id\":\"bytes\"},\"system.memory.total\":{\"id\":\"bytes\"},\"system.process.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.process.memory.rss.bytes\":{\"id\":\"bytes\"},\"system.process.memory.size\":{\"id\":\"bytes\"},\"url.port\":{\"id\":\"string\"},\"view spans\":{\"id\":\"url\",\"params\":{\"labelTemplate\":\"View Spans\"}}}", - "fields": "[{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"@timestamp\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.availability_zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.machine.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.region\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.tag\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.runtime\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.data\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.ttl\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.header_flags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.op_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.resolved_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.response_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"ecs.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.dataset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.end\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.kind\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.outcome\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score_norm\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.sequence\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.timezone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.accessed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.ctime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.device\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.gid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.group\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.inode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mtime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.owner\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.method\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.referrer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.status_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.logger\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.application\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.community_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.direction\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.forwarded_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.iana_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.transport\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.state\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.fragment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.password\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.query\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.scheme\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.username\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.device.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"fields\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"timeseries.instance\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.image.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"docker.container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.containerized\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.build\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.codename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.namespace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.labels.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.annotations.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.replicaset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.deployment.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.statefulset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.image\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.event\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"timestamp.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.request.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.finished\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.response.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.environment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.sampled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.breakdown.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.subtype\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"parent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.listening\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version_major\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"experimental\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.culprit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.grouping_key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.handled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.logger_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.param_message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.total\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.actual.free\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.rss.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.bundle_filepath\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"view spans\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.start.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.sync\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.link\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.result\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks.*.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.span_count.dropped\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_id\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_index\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_score\",\"scripted\":false,\"searchable\":false,\"type\":\"number\"}]", + "fieldFormatMap": "{\"client.bytes\":{\"id\":\"bytes\"},\"client.nat.port\":{\"id\":\"string\"},\"client.port\":{\"id\":\"string\"},\"destination.bytes\":{\"id\":\"bytes\"},\"destination.nat.port\":{\"id\":\"string\"},\"destination.port\":{\"id\":\"string\"},\"event.duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"nanoseconds\",\"outputFormat\":\"asMilliseconds\",\"outputPrecision\":1}},\"event.sequence\":{\"id\":\"string\"},\"event.severity\":{\"id\":\"string\"},\"http.request.body.bytes\":{\"id\":\"bytes\"},\"http.request.bytes\":{\"id\":\"bytes\"},\"http.response.body.bytes\":{\"id\":\"bytes\"},\"http.response.bytes\":{\"id\":\"bytes\"},\"http.response.status_code\":{\"id\":\"string\"},\"log.syslog.facility.code\":{\"id\":\"string\"},\"log.syslog.priority\":{\"id\":\"string\"},\"network.bytes\":{\"id\":\"bytes\"},\"package.size\":{\"id\":\"string\"},\"process.pgid\":{\"id\":\"string\"},\"process.pid\":{\"id\":\"string\"},\"process.ppid\":{\"id\":\"string\"},\"process.thread.id\":{\"id\":\"string\"},\"server.bytes\":{\"id\":\"bytes\"},\"server.nat.port\":{\"id\":\"string\"},\"server.port\":{\"id\":\"string\"},\"source.bytes\":{\"id\":\"bytes\"},\"source.nat.port\":{\"id\":\"string\"},\"source.port\":{\"id\":\"string\"},\"system.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.memory.actual.free\":{\"id\":\"bytes\"},\"system.memory.total\":{\"id\":\"bytes\"},\"system.process.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.process.memory.rss.bytes\":{\"id\":\"bytes\"},\"system.process.memory.size\":{\"id\":\"bytes\"},\"url.port\":{\"id\":\"string\"},\"view spans\":{\"id\":\"url\",\"params\":{\"labelTemplate\":\"View Spans\"}}}", + "fields": "[{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"@timestamp\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.availability_zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.machine.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.region\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.tag\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.runtime\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.data\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.ttl\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.header_flags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.op_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.resolved_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.response_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"ecs.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.stack_trace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.dataset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.end\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.kind\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.outcome\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score_norm\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.sequence\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.timezone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.accessed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.ctime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.device\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.gid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.group\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.inode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mtime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.owner\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.method\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.referrer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.status_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.logger\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.priority\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.application\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.community_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.direction\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.forwarded_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.iana_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.transport\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.checksum\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.install_scope\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.installed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.state\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.framework\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.fragment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.password\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.query\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.scheme\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.username\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.device.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"fields\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"timeseries.instance\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.image.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"docker.container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.containerized\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.build\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.codename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.namespace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.labels.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.annotations.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.replicaset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.deployment.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.statefulset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.image\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.event\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"timestamp.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.request.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.finished\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.response.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.environment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.sampled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.breakdown.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.subtype\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"parent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.listening\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version_major\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"experimental\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.culprit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.grouping_key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.handled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.logger_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.param_message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.total\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.actual.free\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.rss.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.cpu.ns\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.samples.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.bundle_filepath\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"view spans\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.start.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.sync\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.link\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.result\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks.*.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.span_count.dropped\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_id\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_index\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_score\",\"scripted\":false,\"searchable\":false,\"type\":\"number\"}]", "sourceFilters": "[{\"value\":\"sourcemap.sourcemap\"}]", "timeFieldName": "@timestamp" },