From 550d44ea2da437be7f1b3ab08f7125481b04737c Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 30 Jul 2020 09:26:32 -0700 Subject: [PATCH 01/33] [i18n] remove unused translation keys --- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 2 files changed, 2 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 69a8449c4dc70..ff8c3186d5cf8 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5008,7 +5008,6 @@ "xpack.apm.settings.anomaly_detection.legacy_jobs.body": "以前の統合のレガシー機械学習ジョブが見つかりました。これは、APMアプリでは使用されていません。", "xpack.apm.settings.anomaly_detection.legacy_jobs.button": "ジョブの確認", "xpack.apm.settings.anomaly_detection.legacy_jobs.title": "レガシーMLジョブはAPMアプリで使用されていません。", - "xpack.apm.settings.anomaly_detection.license.text": "異常検知を使用するには、Elastic Platinumライセンスのサブスクリプションが必要です。このライセンスがあれば、機械学習を活用して、サービスを監視できます。", "xpack.apm.settings.anomalyDetection": "異常検知", "xpack.apm.settings.anomalyDetection.addEnvironments.cancelButtonText": "キャンセル", "xpack.apm.settings.anomalyDetection.addEnvironments.createJobsButtonText": "ジョブの作成", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 898897548b7fc..bcf2f6707e7bf 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5009,7 +5009,6 @@ "xpack.apm.settings.anomaly_detection.legacy_jobs.body": "我们在以前的集成中发现 APM 应用中不再使用的旧版 Machine Learning 作业", "xpack.apm.settings.anomaly_detection.legacy_jobs.button": "复查作业", "xpack.apm.settings.anomaly_detection.legacy_jobs.title": "旧版 ML 作业不再用于 APM 应用", - "xpack.apm.settings.anomaly_detection.license.text": "要使用异常检测,必须订阅 Elastic 白金级许可。使用该许可,您将能够借助 Machine Learning 监测服务。", "xpack.apm.settings.anomalyDetection": "异常检测", "xpack.apm.settings.anomalyDetection.addEnvironments.cancelButtonText": "取消", "xpack.apm.settings.anomalyDetection.addEnvironments.createJobsButtonText": "创建作业", From c09216b6698eee0189e096dd67085bbe9ff12e4b Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 30 Jul 2020 10:30:57 -0600 Subject: [PATCH 02/33] [Maps] fix application state filters transfer from other kibana application to maps application (#73516) * [Maps] fix application state filters transfer from other kibana application to maps application * clean up comment Co-authored-by: Elastic Machine --- .../maps/public/routing/routes/maps_app/maps_app_view.js | 8 -------- .../plugins/maps/public/routing/state_syncing/app_sync.js | 4 ++++ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js index 7578d2fd3d3ea..d945aa9623b21 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js @@ -91,14 +91,6 @@ export class MapsAppView extends React.Component { this._globalSyncChangeMonitorSubscription.unsubscribe(); } - // Clean up app state filters - const { filterManager } = getData().query; - filterManager.filters.forEach((filter) => { - if (filter.$state.store === esFilters.FilterStateStore.APP_STATE) { - filterManager.removeFilter(filter); - } - }); - getCoreChrome().setBreadcrumbs([]); } diff --git a/x-pack/plugins/maps/public/routing/state_syncing/app_sync.js b/x-pack/plugins/maps/public/routing/state_syncing/app_sync.js index 8dd643607ba9c..60e8dc9cd574c 100644 --- a/x-pack/plugins/maps/public/routing/state_syncing/app_sync.js +++ b/x-pack/plugins/maps/public/routing/state_syncing/app_sync.js @@ -15,6 +15,10 @@ export function startAppStateSyncing(appStateManager) { // sync app filters with app state container from data.query to state container const { query } = getData(); + // Filter manager state persists across applications + // clear app state filters to prevent application filters from other applications being transfered to maps + query.filterManager.setAppFilters([]); + const stateContainer = { get: () => ({ query: appStateManager.getQuery(), From e8cc2ff72f967a048d64c1f5072df8b1a4f2dae9 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Thu, 30 Jul 2020 13:02:54 -0400 Subject: [PATCH 03/33] [SECURITY_SOLUTION][ENDPOINT] Fix Endpoint Hosts crashing periodically when switching between policy responses from linux and windows (#73809) * Fix use of hooks in policy response * Additional fixes to use of `useMemo` * Added error boundary to management pages --- .../components/management_page_view.tsx | 7 ++- .../view/details/policy_response.tsx | 12 ++---- .../view/policy_forms/events/checkbox.tsx | 3 +- .../view/policy_forms/protections/malware.tsx | 3 +- .../pages/policy/view/policy_list.tsx | 43 ++++++++----------- 5 files changed, 31 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx b/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx index aa562b9a20201..54d9131209d0d 100644 --- a/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx +++ b/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx @@ -5,10 +5,15 @@ */ import React, { memo } from 'react'; +import { EuiErrorBoundary } from '@elastic/eui'; import { PageView, PageViewProps } from '../../common/components/endpoint/page_view'; export const ManagementPageView = memo>((options) => { - return ; + return ( + + + + ); }); ManagementPageView.displayName = 'ManagementPageView'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response.tsx index 4cdfaad69eb72..3a1dd180405e0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response.tsx @@ -140,20 +140,16 @@ export const PolicyResponse = memo( responseActions: Immutable; responseAttentionCount: Map; }) => { + const generateId = useMemo(() => htmlIdGenerator(), []); + return ( <> {Object.entries(responseConfig).map(([key, val]) => { const attentionCount = responseAttentionCount.get(key); return ( htmlIdGenerator()(), []) - } - key={ - /* eslint-disable-next-line react-hooks/rules-of-hooks */ - useMemo(() => htmlIdGenerator()(), []) - } + id={generateId(`id_${key}`)} + key={generateId(`key_${key}`)} data-test-subj="hostDetailsPolicyResponseConfigAccordion" buttonContent={ diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/checkbox.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/checkbox.tsx index 9ceade5d0264c..76077831c670b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/checkbox.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/checkbox.tsx @@ -27,6 +27,7 @@ export const EventsCheckbox = React.memo(function ({ const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); const selected = getter(policyDetailsConfig); const dispatch = useDispatch<(action: PolicyDetailsAction) => void>(); + const checkboxId = useMemo(() => htmlIdGenerator()(), []); const handleCheckboxChange = useCallback( (event: React.ChangeEvent) => { @@ -42,7 +43,7 @@ export const EventsCheckbox = React.memo(function ({ return ( htmlIdGenerator()(), [])} + id={checkboxId} label={name} checked={selected} onChange={handleCheckboxChange} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index 84d4bf5355cd9..1698f5bc3fd0c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -41,6 +41,7 @@ const protection = 'malware'; const ProtectionRadio = React.memo(({ id, label }: { id: ProtectionModes; label: string }) => { const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); const dispatch = useDispatch(); + const radioButtonId = useMemo(() => htmlIdGenerator()(), []); // currently just taking windows.malware, but both windows.malware and mac.malware should be the same value const selected = policyDetailsConfig && policyDetailsConfig.windows.malware.mode; @@ -66,7 +67,7 @@ const ProtectionRadio = React.memo(({ id, label }: { id: ProtectionModes; label: htmlIdGenerator()(), [])} + id={radioButtonId} checked={selected === id} onChange={handleRadioChange} disabled={selected === ProtectionModes.off} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx index 246dbeb39886f..39b77d259add1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx @@ -377,6 +377,22 @@ export const PolicyList = React.memo(() => { [services.application, handleDeleteOnClick, formatUrl, search] ); + const bodyContent = useMemo(() => { + return policyItems && policyItems.length > 0 ? ( + + ) : ( + + ); + }, [policyItems, loading, columns, handleCreatePolicyClick, handleTableChange, paginationSetup]); + return ( <> {showDelete && ( @@ -449,32 +465,7 @@ export const PolicyList = React.memo(() => { )} - {useMemo(() => { - return ( - <> - {policyItems && policyItems.length > 0 ? ( - - ) : ( - - )} - - ); - }, [ - policyItems, - loading, - columns, - handleCreatePolicyClick, - handleTableChange, - paginationSetup, - ])} + {bodyContent} From 585d58c202e84945877f6138769bc0414c23ab09 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 30 Jul 2020 20:12:37 +0300 Subject: [PATCH 04/33] [KP] Expose new es client (#73651) * mark legacy ES client types as deprecated * expose es client to plugins and update mocks * ElasticSearchClientMock --> ElasticsearchClientMock * expose es client mocks * expose es client via RequestHandlerContext * convert test/plugin_functional/config into ts * convert top_nav test into ts * add an integration test for the es client * update comments to refer to the new es client * fix import paths. do not use extensions temp * update docs * fix other refs * add test for a custom client * fix context * add test for scoped client * update docs --- ...lugin-core-server.assistanceapiresponse.md | 3 + ...in-core-server.assistantapiclientparams.md | 3 + ...-core-server.deprecationapiclientparams.md | 3 + ...ugin-core-server.deprecationapiresponse.md | 3 + ...bana-plugin-core-server.deprecationinfo.md | 3 + ...n-core-server.elasticsearchclientconfig.md | 18 +++++ ...server.elasticsearchservicestart.client.md | 22 +++++++ ....elasticsearchservicestart.createclient.md | 23 +++++++ ...n-core-server.elasticsearchservicestart.md | 2 + ...ervicesetup.registerroutehandlercontext.md | 2 +- ...re-server.iclusterclient.asinternaluser.md | 13 ++++ ...gin-core-server.iclusterclient.asscoped.md | 13 ++++ ...ibana-plugin-core-server.iclusterclient.md | 21 ++++++ ...-core-server.icustomclusterclient.close.md | 13 ++++ ...plugin-core-server.icustomclusterclient.md | 20 ++++++ ...plugin-core-server.ilegacyclusterclient.md | 5 ++ ...-core-server.ilegacycustomclusterclient.md | 5 ++ ...-core-server.ilegacyscopedclusterclient.md | 5 ++ ...ore-server.indexsettingsdeprecationinfo.md | 3 + ...rver.iscopedclusterclient.ascurrentuser.md | 13 ++++ ...ver.iscopedclusterclient.asinternaluser.md | 13 ++++ ...plugin-core-server.iscopedclusterclient.md | 21 ++++++ ...bana-plugin-core-server.legacyapicaller.md | 3 + ...plugin-core-server.legacycallapioptions.md | 4 ++ ...-plugin-core-server.legacyclusterclient.md | 6 ++ ...-server.legacyelasticsearchclientconfig.md | 3 + ...in-core-server.legacyelasticsearcherror.md | 1 + ...n-core-server.legacyscopedclusterclient.md | 6 ++ .../core/server/kibana-plugin-core-server.md | 12 ++-- ...erver.migration_assistance_index_action.md | 3 + ...core-server.migration_deprecation_level.md | 3 + ...-core-server.requesthandlercontext.core.md | 1 + ...lugin-core-server.requesthandlercontext.md | 4 +- scripts/functional_tests.js | 2 +- src/core/server/elasticsearch/client/mocks.ts | 15 ++--- .../client/retry_call_cluster.test.ts | 8 +-- .../client/scoped_cluster_client.test.ts | 8 +-- .../client/scoped_cluster_client.ts | 2 +- .../elasticsearch_service.mock.ts | 30 ++++----- .../server/elasticsearch/legacy/api_types.ts | 46 ++++++++++--- .../elasticsearch/legacy/cluster_client.ts | 4 +- .../legacy/elasticsearch_client_config.ts | 1 + .../server/elasticsearch/legacy/errors.ts | 5 +- .../legacy/scoped_cluster_client.ts | 2 + src/core/server/elasticsearch/types.ts | 64 +++++++++--------- src/core/server/http/types.ts | 2 +- src/core/server/index.ts | 11 +++- src/core/server/mocks.ts | 1 + src/core/server/plugins/plugin_context.ts | 2 + .../migrations/core/elastic_index.test.ts | 4 +- .../migrations/core/index_migrator.test.ts | 6 +- .../core/migration_es_client.test.ts | 4 +- .../migrations/kibana/kibana_migrator.test.ts | 4 +- .../service/lib/repository.test.js | 2 +- .../service/lib/repository_es_client.test.ts | 4 +- src/core/server/server.api.md | 66 +++++++++++++------ src/core/server/server.ts | 1 + src/plugins/data/server/server.api.md | 1 + tasks/config/run.js | 2 +- test/api_integration/services/index.ts | 1 - test/plugin_functional/README.md | 6 +- .../{config.js => config.ts} | 6 +- .../elasticsearch_client_plugin/kibana.json | 7 ++ .../elasticsearch_client_plugin/package.json | 15 +++++ .../server/index.ts} | 10 +-- .../server/plugin.ts | 53 +++++++++++++++ .../elasticsearch_client_plugin/tsconfig.json | 12 ++++ test/plugin_functional/services/index.ts | 11 +--- .../core_plugins/elasticsearch_client.ts | 38 +++++++++++ .../test_suites/core_plugins/index.ts | 1 + .../core_plugins/{top_nav.js => top_nav.ts} | 4 +- x-pack/test/plugin_api_integration/config.ts | 1 + .../plugins/elasticsearch_client/kibana.json | 7 ++ .../plugins/elasticsearch_client/package.json | 14 ++++ .../elasticsearch_client/server/index.ts | 9 +++ .../elasticsearch_client/server/plugin.ts | 36 ++++++++++ .../platform/elasticsearch_client.ts | 26 ++++++++ .../test_suites/platform/index.ts | 14 ++++ 78 files changed, 695 insertions(+), 146 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.client.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.createclient.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.iclusterclient.asinternaluser.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.iclusterclient.asscoped.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.iclusterclient.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.close.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.ascurrentuser.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.asinternaluser.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md rename test/plugin_functional/{config.js => config.ts} (94%) create mode 100644 test/plugin_functional/plugins/elasticsearch_client_plugin/kibana.json create mode 100644 test/plugin_functional/plugins/elasticsearch_client_plugin/package.json rename test/plugin_functional/{services/supertest.ts => plugins/elasticsearch_client_plugin/server/index.ts} (66%) create mode 100644 test/plugin_functional/plugins/elasticsearch_client_plugin/server/plugin.ts create mode 100644 test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json create mode 100644 test/plugin_functional/test_suites/core_plugins/elasticsearch_client.ts rename test/plugin_functional/test_suites/core_plugins/{top_nav.js => top_nav.ts} (88%) create mode 100644 x-pack/test/plugin_api_integration/plugins/elasticsearch_client/kibana.json create mode 100644 x-pack/test/plugin_api_integration/plugins/elasticsearch_client/package.json create mode 100644 x-pack/test/plugin_api_integration/plugins/elasticsearch_client/server/index.ts create mode 100644 x-pack/test/plugin_api_integration/plugins/elasticsearch_client/server/plugin.ts create mode 100644 x-pack/test/plugin_api_integration/test_suites/platform/elasticsearch_client.ts create mode 100644 x-pack/test/plugin_api_integration/test_suites/platform/index.ts diff --git a/docs/development/core/server/kibana-plugin-core-server.assistanceapiresponse.md b/docs/development/core/server/kibana-plugin-core-server.assistanceapiresponse.md index 9bc24f4d1d366..4778c98493b5b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.assistanceapiresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.assistanceapiresponse.md @@ -4,6 +4,9 @@ ## AssistanceAPIResponse interface +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.md b/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.md index 8be7a9edde363..6d3f8df2fa518 100644 --- a/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.md +++ b/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.md @@ -4,6 +4,9 @@ ## AssistantAPIClientParams interface +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.md b/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.md index e545cf42d3c26..ed64d61e75fab 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.md @@ -4,6 +4,9 @@ ## DeprecationAPIClientParams interface +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.md b/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.md index 1f6e1f9988fc2..1d837d9b4705d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.md @@ -4,6 +4,9 @@ ## DeprecationAPIResponse interface +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.md b/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.md index bd343f5bc7476..8eeb5ef638a82 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.md @@ -4,6 +4,9 @@ ## DeprecationInfo interface +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md new file mode 100644 index 0000000000000..1ba359e81b9c6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchClientConfig](./kibana-plugin-core-server.elasticsearchclientconfig.md) + +## ElasticsearchClientConfig type + +Configuration options to be used to create a [cluster client](./kibana-plugin-core-server.iclusterclient.md) using the [createClient API](./kibana-plugin-core-server.elasticsearchservicestart.createclient.md) + +Signature: + +```typescript +export declare type ElasticsearchClientConfig = Pick & { + pingTimeout?: ElasticsearchConfig['pingTimeout'] | ClientOptions['pingTimeout']; + requestTimeout?: ElasticsearchConfig['requestTimeout'] | ClientOptions['requestTimeout']; + ssl?: Partial; + keepAlive?: boolean; +}; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.client.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.client.md new file mode 100644 index 0000000000000..591f126c423e3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.client.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) > [client](./kibana-plugin-core-server.elasticsearchservicestart.client.md) + +## ElasticsearchServiceStart.client property + +A pre-configured [Elasticsearch client](./kibana-plugin-core-server.iclusterclient.md) + +Signature: + +```typescript +readonly client: IClusterClient; +``` + +## Example + + +```js +const client = core.elasticsearch.client; + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.createclient.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.createclient.md new file mode 100644 index 0000000000000..d4a13812ab533 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.createclient.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) > [createClient](./kibana-plugin-core-server.elasticsearchservicestart.createclient.md) + +## ElasticsearchServiceStart.createClient property + +Create application specific Elasticsearch cluster API client with customized config. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). + +Signature: + +```typescript +readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient; +``` + +## Example + + +```js +const client = elasticsearch.createClient('my-app-name', config); +const data = await client.asInternalUser.search(); + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.md index e059acdbd52fa..860867d654435 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.md @@ -15,5 +15,7 @@ export interface ElasticsearchServiceStart | Property | Type | Description | | --- | --- | --- | +| [client](./kibana-plugin-core-server.elasticsearchservicestart.client.md) | IClusterClient | A pre-configured [Elasticsearch client](./kibana-plugin-core-server.iclusterclient.md) | +| [createClient](./kibana-plugin-core-server.elasticsearchservicestart.createclient.md) | (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ICustomClusterClient | Create application specific Elasticsearch cluster API client with customized config. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). | | [legacy](./kibana-plugin-core-server.elasticsearchservicestart.legacy.md) | {
readonly createClient: (type: string, clientConfig?: Partial<LegacyElasticsearchClientConfig>) => ILegacyCustomClusterClient;
readonly client: ILegacyClusterClient;
} | | diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registerroutehandlercontext.md b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registerroutehandlercontext.md index 8958b49d98b0c..b0dc4d44f7559 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registerroutehandlercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registerroutehandlercontext.md @@ -21,7 +21,7 @@ registerRouteHandlerContext: (contextName 'myApp', (context, req) => { async function search (id: string) { - return await context.elasticsearch.legacy.client.callAsInternalUser('endpoint', id); + return await context.elasticsearch.client.asCurrentUser.find(id); } return { search }; } diff --git a/docs/development/core/server/kibana-plugin-core-server.iclusterclient.asinternaluser.md b/docs/development/core/server/kibana-plugin-core-server.iclusterclient.asinternaluser.md new file mode 100644 index 0000000000000..c7adc345af5a3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iclusterclient.asinternaluser.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) > [asInternalUser](./kibana-plugin-core-server.iclusterclient.asinternaluser.md) + +## IClusterClient.asInternalUser property + +A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the ES cluster on behalf of the Kibana internal user + +Signature: + +```typescript +readonly asInternalUser: ElasticsearchClient; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iclusterclient.asscoped.md b/docs/development/core/server/kibana-plugin-core-server.iclusterclient.asscoped.md new file mode 100644 index 0000000000000..301fcbfee5858 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iclusterclient.asscoped.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) > [asScoped](./kibana-plugin-core-server.iclusterclient.asscoped.md) + +## IClusterClient.asScoped property + +Creates a [scoped cluster client](./kibana-plugin-core-server.iscopedclusterclient.md) bound to given [request](./kibana-plugin-core-server.scopeablerequest.md) + +Signature: + +```typescript +asScoped: (request: ScopeableRequest) => IScopedClusterClient; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.iclusterclient.md new file mode 100644 index 0000000000000..f6bacee322538 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iclusterclient.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) + +## IClusterClient interface + +Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`). + +Signature: + +```typescript +export interface IClusterClient +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [asInternalUser](./kibana-plugin-core-server.iclusterclient.asinternaluser.md) | ElasticsearchClient | A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the ES cluster on behalf of the Kibana internal user | +| [asScoped](./kibana-plugin-core-server.iclusterclient.asscoped.md) | (request: ScopeableRequest) => IScopedClusterClient | Creates a [scoped cluster client](./kibana-plugin-core-server.iscopedclusterclient.md) bound to given [request](./kibana-plugin-core-server.scopeablerequest.md) | + diff --git a/docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.close.md b/docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.close.md new file mode 100644 index 0000000000000..5fa2e93cca75b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.close.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ICustomClusterClient](./kibana-plugin-core-server.icustomclusterclient.md) > [close](./kibana-plugin-core-server.icustomclusterclient.close.md) + +## ICustomClusterClient.close property + +Closes the cluster client. After that client cannot be used and one should create a new client instance to be able to interact with Elasticsearch API. + +Signature: + +```typescript +close: () => Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.md new file mode 100644 index 0000000000000..189a50b5d6c20 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ICustomClusterClient](./kibana-plugin-core-server.icustomclusterclient.md) + +## ICustomClusterClient interface + +See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) + +Signature: + +```typescript +export interface ICustomClusterClient extends IClusterClient +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [close](./kibana-plugin-core-server.icustomclusterclient.close.md) | () => Promise<void> | Closes the cluster client. After that client cannot be used and one should create a new client instance to be able to interact with Elasticsearch API. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.ilegacyclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.ilegacyclusterclient.md index c70a5ac07c6ad..b5fbb3d54b972 100644 --- a/docs/development/core/server/kibana-plugin-core-server.ilegacyclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.ilegacyclusterclient.md @@ -4,6 +4,11 @@ ## ILegacyClusterClient type +> Warning: This API is now obsolete. +> +> Use [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). +> + Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`). See [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md). diff --git a/docs/development/core/server/kibana-plugin-core-server.ilegacycustomclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.ilegacycustomclusterclient.md index a3cb8f1315021..4da121984d084 100644 --- a/docs/development/core/server/kibana-plugin-core-server.ilegacycustomclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.ilegacycustomclusterclient.md @@ -4,6 +4,11 @@ ## ILegacyCustomClusterClient type +> Warning: This API is now obsolete. +> +> Use [ICustomClusterClient](./kibana-plugin-core-server.icustomclusterclient.md). +> + Represents an Elasticsearch cluster API client created by a plugin. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`). See [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md). diff --git a/docs/development/core/server/kibana-plugin-core-server.ilegacyscopedclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.ilegacyscopedclusterclient.md index 1263b85acb498..51d0b2e4882cb 100644 --- a/docs/development/core/server/kibana-plugin-core-server.ilegacyscopedclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.ilegacyscopedclusterclient.md @@ -4,6 +4,11 @@ ## ILegacyScopedClusterClient type +> Warning: This API is now obsolete. +> +> Use [IScopedClusterClient](./kibana-plugin-core-server.iscopedclusterclient.md). +> + Serves the same purpose as "normal" `ClusterClient` but exposes additional `callAsCurrentUser` method that doesn't use credentials of the Kibana internal user (as `callAsInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API. See [LegacyScopedClusterClient](./kibana-plugin-core-server.legacyscopedclusterclient.md). diff --git a/docs/development/core/server/kibana-plugin-core-server.indexsettingsdeprecationinfo.md b/docs/development/core/server/kibana-plugin-core-server.indexsettingsdeprecationinfo.md index 00f1659549078..706898c4ad9aa 100644 --- a/docs/development/core/server/kibana-plugin-core-server.indexsettingsdeprecationinfo.md +++ b/docs/development/core/server/kibana-plugin-core-server.indexsettingsdeprecationinfo.md @@ -4,6 +4,9 @@ ## IndexSettingsDeprecationInfo interface +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.ascurrentuser.md b/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.ascurrentuser.md new file mode 100644 index 0000000000000..ddc6357bb8835 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.ascurrentuser.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IScopedClusterClient](./kibana-plugin-core-server.iscopedclusterclient.md) > [asCurrentUser](./kibana-plugin-core-server.iscopedclusterclient.ascurrentuser.md) + +## IScopedClusterClient.asCurrentUser property + +A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the elasticsearch cluster on behalf of the user that initiated the request to the Kibana server. + +Signature: + +```typescript +readonly asCurrentUser: ElasticsearchClient; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.asinternaluser.md b/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.asinternaluser.md new file mode 100644 index 0000000000000..f7f308aa13161 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.asinternaluser.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IScopedClusterClient](./kibana-plugin-core-server.iscopedclusterclient.md) > [asInternalUser](./kibana-plugin-core-server.iscopedclusterclient.asinternaluser.md) + +## IScopedClusterClient.asInternalUser property + +A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the elasticsearch cluster on behalf of the internal Kibana user. + +Signature: + +```typescript +readonly asInternalUser: ElasticsearchClient; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md new file mode 100644 index 0000000000000..f39db268288a6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IScopedClusterClient](./kibana-plugin-core-server.iscopedclusterclient.md) + +## IScopedClusterClient interface + +Serves the same purpose as the normal [cluster client](./kibana-plugin-core-server.iclusterclient.md) but exposes an additional `asCurrentUser` method that doesn't use credentials of the Kibana internal user (as `asInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. + +Signature: + +```typescript +export interface IScopedClusterClient +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [asCurrentUser](./kibana-plugin-core-server.iscopedclusterclient.ascurrentuser.md) | ElasticsearchClient | A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the elasticsearch cluster on behalf of the user that initiated the request to the Kibana server. | +| [asInternalUser](./kibana-plugin-core-server.iscopedclusterclient.asinternaluser.md) | ElasticsearchClient | A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the elasticsearch cluster on behalf of the internal Kibana user. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyapicaller.md b/docs/development/core/server/kibana-plugin-core-server.legacyapicaller.md index e6c2878d2b355..168209659046e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyapicaller.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyapicaller.md @@ -4,6 +4,9 @@ ## LegacyAPICaller interface +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.md b/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.md index 9ebe2fc57a54b..40def157114ef 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.md @@ -4,6 +4,10 @@ ## LegacyCallAPIOptions interface +> Warning: This API is now obsolete. +> +> + The set of options that defines how API call should be made and result be processed. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md index c51f1858c97a5..668d0b2866a26 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md @@ -4,6 +4,12 @@ ## LegacyClusterClient class +> Warning: This API is now obsolete. +> +> Use [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). +> + +Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`). Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md index 62b0f216c863c..78f7bf582d355 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md @@ -4,6 +4,9 @@ ## LegacyElasticsearchClientConfig type +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherror.md b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherror.md index f760780504e55..40fc1a8e05a68 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherror.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherror.md @@ -4,6 +4,7 @@ ## LegacyElasticsearchError interface +@deprecated. The new elasticsearch client doesn't wrap errors anymore. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md index c4a94d8661c47..7f752d70921ba 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md @@ -4,6 +4,12 @@ ## LegacyScopedClusterClient class +> Warning: This API is now obsolete. +> +> Use [scoped cluster client](./kibana-plugin-core-server.iscopedclusterclient.md). +> + +Serves the same purpose as the normal [cluster client](./kibana-plugin-core-server.iclusterclient.md) but exposes an additional `asCurrentUser` method that doesn't use credentials of the Kibana internal user (as `asInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 95b7627398b45..5347f0b55e19b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -20,9 +20,9 @@ The plugin integrates with the core system via lifecycle events: `setup` | [CspConfig](./kibana-plugin-core-server.cspconfig.md) | CSP configuration for use in Kibana. | | [ElasticsearchConfig](./kibana-plugin-core-server.elasticsearchconfig.md) | Wrapper of config schema. | | [KibanaRequest](./kibana-plugin-core-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. | -| [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md) | | +| [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md) | Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)). | | [LegacyElasticsearchErrorHelpers](./kibana-plugin-core-server.legacyelasticsearcherrorhelpers.md) | Helpers for working with errors returned from the Elasticsearch service.Since the internal data of errors are subject to change, consumers of the Elasticsearch service should always use these helpers to classify errors instead of checking error internals such as body.error.header[WWW-Authenticate] | -| [LegacyScopedClusterClient](./kibana-plugin-core-server.legacyscopedclusterclient.md) | | +| [LegacyScopedClusterClient](./kibana-plugin-core-server.legacyscopedclusterclient.md) | Serves the same purpose as the normal [cluster client](./kibana-plugin-core-server.iclusterclient.md) but exposes an additional asCurrentUser method that doesn't use credentials of the Kibana internal user (as asInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. | | [RouteValidationError](./kibana-plugin-core-server.routevalidationerror.md) | Error to return when the validation is not successful. | | [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) | | | [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) | | @@ -98,20 +98,23 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HttpServerInfo](./kibana-plugin-core-server.httpserverinfo.md) | | | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | Kibana HTTP Service provides own abstraction for work with HTTP stack. Plugins don't have direct access to hapi server and its primitives anymore. Moreover, plugins shouldn't rely on the fact that HTTP Service uses one or another library under the hood. This gives the platform flexibility to upgrade or changing our internal HTTP stack without breaking plugins. If the HTTP Service lacks functionality you need, we are happy to discuss and support your needs. | | [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) | | +| [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) | Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)). | | [IContextContainer](./kibana-plugin-core-server.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. | | [ICspConfig](./kibana-plugin-core-server.icspconfig.md) | CSP configuration for use in Kibana. | +| [ICustomClusterClient](./kibana-plugin-core-server.icustomclusterclient.md) | See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) | | [IKibanaResponse](./kibana-plugin-core-server.ikibanaresponse.md) | A response data object, expected to returned as a result of [RequestHandler](./kibana-plugin-core-server.requesthandler.md) execution | | [IKibanaSocket](./kibana-plugin-core-server.ikibanasocket.md) | A tiny abstraction for TCP socket. | | [ImageValidation](./kibana-plugin-core-server.imagevalidation.md) | | | [IndexSettingsDeprecationInfo](./kibana-plugin-core-server.indexsettingsdeprecationinfo.md) | | | [IRenderOptions](./kibana-plugin-core-server.irenderoptions.md) | | | [IRouter](./kibana-plugin-core-server.irouter.md) | Registers route handlers for specified resource path and method. See [RouteConfig](./kibana-plugin-core-server.routeconfig.md) and [RequestHandler](./kibana-plugin-core-server.requesthandler.md) for more information about arguments to route registrations. | +| [IScopedClusterClient](./kibana-plugin-core-server.iscopedclusterclient.md) | Serves the same purpose as the normal [cluster client](./kibana-plugin-core-server.iclusterclient.md) but exposes an additional asCurrentUser method that doesn't use credentials of the Kibana internal user (as asInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. | | [IUiSettingsClient](./kibana-plugin-core-server.iuisettingsclient.md) | Server-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. | | [KibanaRequestEvents](./kibana-plugin-core-server.kibanarequestevents.md) | Request events. | | [KibanaRequestRoute](./kibana-plugin-core-server.kibanarequestroute.md) | Request specific route information exposed to a handler. | | [LegacyAPICaller](./kibana-plugin-core-server.legacyapicaller.md) | | | [LegacyCallAPIOptions](./kibana-plugin-core-server.legacycallapioptions.md) | The set of options that defines how API call should be made and result be processed. | -| [LegacyElasticsearchError](./kibana-plugin-core-server.legacyelasticsearcherror.md) | | +| [LegacyElasticsearchError](./kibana-plugin-core-server.legacyelasticsearcherror.md) | @deprecated. The new elasticsearch client doesn't wrap errors anymore. | | [LegacyRequest](./kibana-plugin-core-server.legacyrequest.md) | | | [LegacyServiceSetupDeps](./kibana-plugin-core-server.legacyservicesetupdeps.md) | | | [LegacyServiceStartDeps](./kibana-plugin-core-server.legacyservicestartdeps.md) | | @@ -137,7 +140,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginConfigDescriptor](./kibana-plugin-core-server.pluginconfigdescriptor.md) | Describes a plugin configuration properties. | | [PluginInitializerContext](./kibana-plugin-core-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | | [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. | -| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | +| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request - [uiSettings.auditor](./kibana-plugin-core-server.auditor.md) - AuditTrail client scoped to the incoming request | | [RouteConfig](./kibana-plugin-core-server.routeconfig.md) | Route specific configuration. | | [RouteConfigOptions](./kibana-plugin-core-server.routeconfigoptions.md) | Additional route options. | | [RouteConfigOptionsBody](./kibana-plugin-core-server.routeconfigoptionsbody.md) | Additional body options for a route | @@ -236,6 +239,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ConfigPath](./kibana-plugin-core-server.configpath.md) | | | [DestructiveRouteMethod](./kibana-plugin-core-server.destructiveroutemethod.md) | Set of HTTP methods changing the state of the server. | | [ElasticsearchClient](./kibana-plugin-core-server.elasticsearchclient.md) | Client used to query the elasticsearch cluster. | +| [ElasticsearchClientConfig](./kibana-plugin-core-server.elasticsearchclientconfig.md) | Configuration options to be used to create a [cluster client](./kibana-plugin-core-server.iclusterclient.md) using the [createClient API](./kibana-plugin-core-server.elasticsearchservicestart.createclient.md) | | [Freezable](./kibana-plugin-core-server.freezable.md) | | | [GetAuthHeaders](./kibana-plugin-core-server.getauthheaders.md) | Get headers to authenticate a user against Elasticsearch. | | [GetAuthState](./kibana-plugin-core-server.getauthstate.md) | Gets authentication state for a request. Returned by auth interceptor. | diff --git a/docs/development/core/server/kibana-plugin-core-server.migration_assistance_index_action.md b/docs/development/core/server/kibana-plugin-core-server.migration_assistance_index_action.md index 69fb573d36530..a924f0cea6b6b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.migration_assistance_index_action.md +++ b/docs/development/core/server/kibana-plugin-core-server.migration_assistance_index_action.md @@ -4,6 +4,9 @@ ## MIGRATION\_ASSISTANCE\_INDEX\_ACTION type +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.migration_deprecation_level.md b/docs/development/core/server/kibana-plugin-core-server.migration_deprecation_level.md index c3256eaa78331..0fcae8c847cb4 100644 --- a/docs/development/core/server/kibana-plugin-core-server.migration_deprecation_level.md +++ b/docs/development/core/server/kibana-plugin-core-server.migration_deprecation_level.md @@ -4,6 +4,9 @@ ## MIGRATION\_DEPRECATION\_LEVEL type +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md index 2d31c24a077cb..5b8492ec5ece1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md @@ -13,6 +13,7 @@ core: { typeRegistry: ISavedObjectTypeRegistry; }; elasticsearch: { + client: IScopedClusterClient; legacy: { client: ILegacyScopedClusterClient; }; diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md index 07e6dcbdae125..4e530973f9d50 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md @@ -6,7 +6,7 @@ Plugin specific context passed to a route handler. -Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request +Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request - [uiSettings.auditor](./kibana-plugin-core-server.auditor.md) - AuditTrail client scoped to the incoming request Signature: @@ -18,5 +18,5 @@ export interface RequestHandlerContext | Property | Type | Description | | --- | --- | --- | -| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
};
elasticsearch: {
legacy: {
client: ILegacyScopedClusterClient;
};
};
uiSettings: {
client: IUiSettingsClient;
};
auditor: Auditor;
} | | +| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
};
elasticsearch: {
client: IScopedClusterClient;
legacy: {
client: ILegacyScopedClusterClient;
};
};
uiSettings: {
client: IUiSettingsClient;
};
auditor: Auditor;
} | | diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index 3fdab481dc750..4facbe1ffbb07 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -20,7 +20,7 @@ // eslint-disable-next-line no-restricted-syntax const alwaysImportedTests = [ require.resolve('../test/functional/config.js'), - require.resolve('../test/plugin_functional/config.js'), + require.resolve('../test/plugin_functional/config.ts'), require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'), require.resolve('../test/new_visualize_flow/config.js'), ]; diff --git a/src/core/server/elasticsearch/client/mocks.ts b/src/core/server/elasticsearch/client/mocks.ts index c93294404b52f..2f2ca08fee6f2 100644 --- a/src/core/server/elasticsearch/client/mocks.ts +++ b/src/core/server/elasticsearch/client/mocks.ts @@ -70,15 +70,14 @@ const createInternalClientMock = (): DeeplyMockedKeys => { return (mock as unknown) as DeeplyMockedKeys; }; -// TODO fix naming ElasticsearchClientMock -export type ElasticSearchClientMock = DeeplyMockedKeys; +export type ElasticsearchClientMock = DeeplyMockedKeys; -const createClientMock = (): ElasticSearchClientMock => - (createInternalClientMock() as unknown) as ElasticSearchClientMock; +const createClientMock = (): ElasticsearchClientMock => + (createInternalClientMock() as unknown) as ElasticsearchClientMock; interface ScopedClusterClientMock { - asInternalUser: ElasticSearchClientMock; - asCurrentUser: ElasticSearchClientMock; + asInternalUser: ElasticsearchClientMock; + asCurrentUser: ElasticsearchClientMock; } const createScopedClusterClientMock = () => { @@ -91,7 +90,7 @@ const createScopedClusterClientMock = () => { }; export interface ClusterClientMock { - asInternalUser: ElasticSearchClientMock; + asInternalUser: ElasticsearchClientMock; asScoped: jest.MockedFunction<() => ScopedClusterClientMock>; } @@ -157,7 +156,7 @@ export const elasticsearchClientMock = { createClusterClient: createClusterClientMock, createCustomClusterClient: createCustomClusterClientMock, createScopedClusterClient: createScopedClusterClientMock, - createElasticSearchClient: createClientMock, + createElasticsearchClient: createClientMock, createInternalClient: createInternalClientMock, createSuccessTransportRequestPromise, createErrorTransportRequestPromise, diff --git a/src/core/server/elasticsearch/client/retry_call_cluster.test.ts b/src/core/server/elasticsearch/client/retry_call_cluster.test.ts index 3aa47e8b40e24..c9366c575ba74 100644 --- a/src/core/server/elasticsearch/client/retry_call_cluster.test.ts +++ b/src/core/server/elasticsearch/client/retry_call_cluster.test.ts @@ -27,10 +27,10 @@ const createErrorReturn = (err: any) => elasticsearchClientMock.createErrorTransportRequestPromise(err); describe('retryCallCluster', () => { - let client: ReturnType; + let client: ReturnType; beforeEach(() => { - client = elasticsearchClientMock.createElasticSearchClient(); + client = elasticsearchClientMock.createElasticsearchClient(); }); it('returns response from ES API call in case of success', async () => { @@ -91,11 +91,11 @@ describe('retryCallCluster', () => { }); describe('migrationRetryCallCluster', () => { - let client: ReturnType; + let client: ReturnType; let logger: ReturnType; beforeEach(() => { - client = elasticsearchClientMock.createElasticSearchClient(); + client = elasticsearchClientMock.createElasticsearchClient(); logger = loggingSystemMock.createLogger(); }); diff --git a/src/core/server/elasticsearch/client/scoped_cluster_client.test.ts b/src/core/server/elasticsearch/client/scoped_cluster_client.test.ts index 78ca8fcbd3c07..4288c6bf6421d 100644 --- a/src/core/server/elasticsearch/client/scoped_cluster_client.test.ts +++ b/src/core/server/elasticsearch/client/scoped_cluster_client.test.ts @@ -22,8 +22,8 @@ import { ScopedClusterClient } from './scoped_cluster_client'; describe('ScopedClusterClient', () => { it('uses the internal client passed in the constructor', () => { - const internalClient = elasticsearchClientMock.createElasticSearchClient(); - const scopedClient = elasticsearchClientMock.createElasticSearchClient(); + const internalClient = elasticsearchClientMock.createElasticsearchClient(); + const scopedClient = elasticsearchClientMock.createElasticsearchClient(); const scopedClusterClient = new ScopedClusterClient(internalClient, scopedClient); @@ -31,8 +31,8 @@ describe('ScopedClusterClient', () => { }); it('uses the scoped client passed in the constructor', () => { - const internalClient = elasticsearchClientMock.createElasticSearchClient(); - const scopedClient = elasticsearchClientMock.createElasticSearchClient(); + const internalClient = elasticsearchClientMock.createElasticsearchClient(); + const scopedClient = elasticsearchClientMock.createElasticsearchClient(); const scopedClusterClient = new ScopedClusterClient(internalClient, scopedClient); diff --git a/src/core/server/elasticsearch/client/scoped_cluster_client.ts b/src/core/server/elasticsearch/client/scoped_cluster_client.ts index 1af7948a65e16..05ab67073f9e1 100644 --- a/src/core/server/elasticsearch/client/scoped_cluster_client.ts +++ b/src/core/server/elasticsearch/client/scoped_cluster_client.ts @@ -20,7 +20,7 @@ import { ElasticsearchClient } from './types'; /** - * Serves the same purpose as the normal {@link ClusterClient | cluster client} but exposes + * Serves the same purpose as the normal {@link IClusterClient | cluster client} but exposes * an additional `asCurrentUser` method that doesn't use credentials of the Kibana internal * user (as `asInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers * extracted from the current user request to the API instead. diff --git a/src/core/server/elasticsearch/elasticsearch_service.mock.ts b/src/core/server/elasticsearch/elasticsearch_service.mock.ts index b97f6df6b0afc..501ab619316c2 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.mock.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.mock.ts @@ -24,6 +24,7 @@ import { ClusterClientMock, CustomClusterClientMock, } from './client/mocks'; +import { ElasticsearchClientConfig } from './client'; import { legacyClientMock } from './legacy/mocks'; import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; @@ -38,12 +39,12 @@ interface MockedElasticSearchServiceSetup { }; } -type MockedElasticSearchServiceStart = MockedElasticSearchServiceSetup; - -interface MockedInternalElasticSearchServiceStart extends MockedElasticSearchServiceStart { +type MockedElasticSearchServiceStart = MockedElasticSearchServiceSetup & { client: ClusterClientMock; - createClient: jest.MockedFunction<() => CustomClusterClientMock>; -} + createClient: jest.MockedFunction< + (name: string, config?: Partial) => CustomClusterClientMock + >; +}; const createSetupContractMock = () => { const setupContract: MockedElasticSearchServiceSetup = { @@ -61,6 +62,8 @@ const createSetupContractMock = () => { const createStartContractMock = () => { const startContract: MockedElasticSearchServiceStart = { + client: elasticsearchClientMock.createClusterClient(), + createClient: jest.fn(), legacy: { createClient: jest.fn(), client: legacyClientMock.createClusterClient(), @@ -70,20 +73,13 @@ const createStartContractMock = () => { startContract.legacy.client.asScoped.mockReturnValue( legacyClientMock.createScopedClusterClient() ); + startContract.createClient.mockImplementation(() => + elasticsearchClientMock.createCustomClusterClient() + ); return startContract; }; -const createInternalStartContractMock = () => { - const startContract: MockedInternalElasticSearchServiceStart = { - ...createStartContractMock(), - client: elasticsearchClientMock.createClusterClient(), - createClient: jest.fn(), - }; - - startContract.createClient.mockReturnValue(elasticsearchClientMock.createCustomClusterClient()); - - return startContract; -}; +const createInternalStartContractMock = createStartContractMock; type MockedInternalElasticSearchServiceSetup = jest.Mocked< InternalElasticsearchServiceSetup & { @@ -136,4 +132,6 @@ export const elasticsearchServiceMock = { createLegacyCustomClusterClient: legacyClientMock.createCustomClusterClient, createLegacyScopedClusterClient: legacyClientMock.createScopedClusterClient, createLegacyElasticsearchClient: legacyClientMock.createElasticsearchClient, + + ...elasticsearchClientMock, }; diff --git a/src/core/server/elasticsearch/legacy/api_types.ts b/src/core/server/elasticsearch/legacy/api_types.ts index b9699ab290e3f..896a58e085d49 100644 --- a/src/core/server/elasticsearch/legacy/api_types.ts +++ b/src/core/server/elasticsearch/legacy/api_types.ts @@ -150,6 +150,7 @@ import { * processed. * * @public + * @deprecated */ export interface LegacyCallAPIOptions { /** @@ -165,7 +166,10 @@ export interface LegacyCallAPIOptions { signal?: AbortSignal; } -/** @public */ +/** + * @deprecated + * @public + * */ export interface LegacyAPICaller { /* eslint-disable */ (endpoint: 'bulk', params: BulkIndexDocumentsParams, options?: LegacyCallAPIOptions): ReturnType; @@ -317,18 +321,30 @@ export interface LegacyAPICaller { /* eslint-enable */ } -/** @public */ +/** + * @deprecated + * @public + * */ export interface AssistantAPIClientParams extends GenericParams { path: '/_migration/assistance'; method: 'GET'; } -/** @public */ +/** + * @deprecated + * @public + * */ export type MIGRATION_ASSISTANCE_INDEX_ACTION = 'upgrade' | 'reindex'; -/** @public */ +/** + * @deprecated + * @public + * */ export type MIGRATION_DEPRECATION_LEVEL = 'none' | 'info' | 'warning' | 'critical'; -/** @public */ +/** + * @deprecated + * @public + * */ export interface AssistanceAPIResponse { indices: { [indexName: string]: { @@ -337,13 +353,19 @@ export interface AssistanceAPIResponse { }; } -/** @public */ +/** + * @deprecated + * @public + * */ export interface DeprecationAPIClientParams extends GenericParams { path: '/_migration/deprecations'; method: 'GET'; } -/** @public */ +/** + * @deprecated + * @public + * */ export interface DeprecationInfo { level: MIGRATION_DEPRECATION_LEVEL; message: string; @@ -351,12 +373,18 @@ export interface DeprecationInfo { details?: string; } -/** @public */ +/** + * @deprecated + * @public + * */ export interface IndexSettingsDeprecationInfo { [indexName: string]: DeprecationInfo[]; } -/** @public */ +/** + * @deprecated + * @public + * */ export interface DeprecationAPIResponse { cluster_settings: DeprecationInfo[]; ml_settings: DeprecationInfo[]; diff --git a/src/core/server/elasticsearch/legacy/cluster_client.ts b/src/core/server/elasticsearch/legacy/cluster_client.ts index 7a39113d25a14..f8b2d39a4251c 100644 --- a/src/core/server/elasticsearch/legacy/cluster_client.ts +++ b/src/core/server/elasticsearch/legacy/cluster_client.ts @@ -88,6 +88,7 @@ const callAPI = async ( * * See {@link LegacyClusterClient}. * + * @deprecated Use {@link IClusterClient}. * @public */ export type ILegacyClusterClient = Pick; @@ -98,7 +99,7 @@ export type ILegacyClusterClient = Pick & diff --git a/src/core/server/elasticsearch/legacy/errors.ts b/src/core/server/elasticsearch/legacy/errors.ts index 3b3b8da51a907..de4d2739977bb 100644 --- a/src/core/server/elasticsearch/legacy/errors.ts +++ b/src/core/server/elasticsearch/legacy/errors.ts @@ -26,7 +26,10 @@ enum ErrorCode { NOT_AUTHORIZED = 'Elasticsearch/notAuthorized', } -/** @public */ +/** + * @deprecated. The new elasticsearch client doesn't wrap errors anymore. + * @public + * */ export interface LegacyElasticsearchError extends Boom { [code]?: string; } diff --git a/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts b/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts index 9edb73645f0e2..aee7a1daa8166 100644 --- a/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts +++ b/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts @@ -30,6 +30,7 @@ import { LegacyAPICaller, LegacyCallAPIOptions } from './api_types'; * * See {@link LegacyScopedClusterClient}. * + * @deprecated Use {@link IScopedClusterClient}. * @public */ export type ILegacyScopedClusterClient = Pick< @@ -39,6 +40,7 @@ export type ILegacyScopedClusterClient = Pick< /** * {@inheritDoc IScopedClusterClient} + * @deprecated Use {@link IScopedClusterClient | scoped cluster client}. * @public */ export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { diff --git a/src/core/server/elasticsearch/types.ts b/src/core/server/elasticsearch/types.ts index 40399aecbc446..88094af8047e7 100644 --- a/src/core/server/elasticsearch/types.ts +++ b/src/core/server/elasticsearch/types.ts @@ -95,6 +95,37 @@ export interface InternalElasticsearchServiceSetup { * @public */ export interface ElasticsearchServiceStart { + /** + * A pre-configured {@link IClusterClient | Elasticsearch client} + * + * @example + * ```js + * const client = core.elasticsearch.client; + * ``` + */ + readonly client: IClusterClient; + /** + * Create application specific Elasticsearch cluster API client with customized config. See {@link IClusterClient}. + * + * @param type Unique identifier of the client + * @param clientConfig A config consists of Elasticsearch JS client options and + * valid sub-set of Elasticsearch service config. + * We fill all the missing properties in the `clientConfig` using the default + * Elasticsearch config so that we don't depend on default values set and + * controlled by underlying Elasticsearch JS client. + * We don't run validation against the passed config and expect it to be valid. + * + * @example + * ```js + * const client = elasticsearch.createClient('my-app-name', config); + * const data = await client.asInternalUser.search(); + * ``` + */ + readonly createClient: ( + type: string, + clientConfig?: Partial + ) => ICustomClusterClient; + /** * @deprecated * Provided for the backward compatibility. @@ -138,38 +169,7 @@ export interface ElasticsearchServiceStart { /** * @internal */ -export interface InternalElasticsearchServiceStart extends ElasticsearchServiceStart { - /** - * A pre-configured {@link IClusterClient | Elasticsearch client} - * - * @example - * ```js - * const client = core.elasticsearch.client; - * ``` - */ - readonly client: IClusterClient; - /** - * Create application specific Elasticsearch cluster API client with customized config. See {@link IClusterClient}. - * - * @param type Unique identifier of the client - * @param clientConfig A config consists of Elasticsearch JS client options and - * valid sub-set of Elasticsearch service config. - * We fill all the missing properties in the `clientConfig` using the default - * Elasticsearch config so that we don't depend on default values set and - * controlled by underlying Elasticsearch JS client. - * We don't run validation against the passed config and expect it to be valid. - * - * @example - * ```js - * const client = elasticsearch.createClient('my-app-name', config); - * const data = await client.asInternalUser().search(); - * ``` - */ - readonly createClient: ( - type: string, - clientConfig?: Partial - ) => ICustomClusterClient; -} +export type InternalElasticsearchServiceStart = ElasticsearchServiceStart; /** @public */ export interface ElasticsearchStatusMeta { diff --git a/src/core/server/http/types.ts b/src/core/server/http/types.ts index 3df098a1df00d..4345783e46e11 100644 --- a/src/core/server/http/types.ts +++ b/src/core/server/http/types.ts @@ -250,7 +250,7 @@ export interface HttpServiceSetup { * 'myApp', * (context, req) => { * async function search (id: string) { - * return await context.elasticsearch.legacy.client.callAsInternalUser('endpoint', id); + * return await context.elasticsearch.client.asCurrentUser.find(id); * } * return { search }; * } diff --git a/src/core/server/index.ts b/src/core/server/index.ts index f46b41d6b8793..382318ea86a34 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -44,6 +44,7 @@ import { ILegacyScopedClusterClient, configSchema as elasticsearchConfigSchema, ElasticsearchServiceStart, + IScopedClusterClient, } from './elasticsearch'; import { HttpServiceSetup, HttpServiceStart } from './http'; @@ -110,6 +111,10 @@ export { FakeRequest, ScopeableRequest, ElasticsearchClient, + IClusterClient, + ICustomClusterClient, + ElasticsearchClientConfig, + IScopedClusterClient, SearchResponse, CountResponse, ShardsInfo, @@ -367,10 +372,13 @@ export { * which uses the credentials of the incoming request * - {@link ISavedObjectTypeRegistry | savedObjects.typeRegistry} - Type registry containing * all the registered types. - * - {@link LegacyScopedClusterClient | elasticsearch.legacy.client} - Elasticsearch + * - {@link IScopedClusterClient | elasticsearch.client} - Elasticsearch + * data client which uses the credentials of the incoming request + * - {@link LegacyScopedClusterClient | elasticsearch.legacy.client} - The legacy Elasticsearch * data client which uses the credentials of the incoming request * - {@link IUiSettingsClient | uiSettings.client} - uiSettings client * which uses the credentials of the incoming request + * - {@link Auditor | uiSettings.auditor} - AuditTrail client scoped to the incoming request * * @public */ @@ -381,6 +389,7 @@ export interface RequestHandlerContext { typeRegistry: ISavedObjectTypeRegistry; }; elasticsearch: { + client: IScopedClusterClient; legacy: { client: ILegacyScopedClusterClient; }; diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 84e4b4741b717..bf9dcc4abe01c 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -193,6 +193,7 @@ function createCoreRequestHandlerContextMock() { typeRegistry: savedObjectsTypeRegistryMock.create(), }, elasticsearch: { + client: elasticsearchServiceMock.createScopedClusterClient(), legacy: { client: elasticsearchServiceMock.createLegacyScopedClusterClient(), }, diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index c17b8df8bb52c..5235f3ee6d580 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -212,6 +212,8 @@ export function createPluginStartContext( resolveCapabilities: deps.capabilities.resolveCapabilities, }, elasticsearch: { + client: deps.elasticsearch.client, + createClient: deps.elasticsearch.createClient, legacy: deps.elasticsearch.legacy, }, http: { diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.test.ts b/src/core/server/saved_objects/migrations/core/elastic_index.test.ts index fb8fb4ef95081..0b3ad1b6e3cc8 100644 --- a/src/core/server/saved_objects/migrations/core/elastic_index.test.ts +++ b/src/core/server/saved_objects/migrations/core/elastic_index.test.ts @@ -22,10 +22,10 @@ import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; import * as Index from './elastic_index'; describe('ElasticIndex', () => { - let client: ReturnType; + let client: ReturnType; beforeEach(() => { - client = elasticsearchClientMock.createElasticSearchClient(); + client = elasticsearchClientMock.createElasticsearchClient(); }); describe('fetchInfo', () => { test('it handles 404', async () => { diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts index 78601d033f8d8..b0669774207dd 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts @@ -27,13 +27,13 @@ import { loggingSystemMock } from '../../../logging/logging_system.mock'; describe('IndexMigrator', () => { let testOpts: jest.Mocked & { - client: ReturnType; + client: ReturnType; }; beforeEach(() => { testOpts = { batchSize: 10, - client: elasticsearchClientMock.createElasticSearchClient(), + client: elasticsearchClientMock.createElasticsearchClient(), index: '.kibana', log: loggingSystemMock.create().get(), mappingProperties: {}, @@ -366,7 +366,7 @@ describe('IndexMigrator', () => { }); function withIndex( - client: ReturnType, + client: ReturnType, opts: any = {} ) { const defaultIndex = { diff --git a/src/core/server/saved_objects/migrations/core/migration_es_client.test.ts b/src/core/server/saved_objects/migrations/core/migration_es_client.test.ts index 40c06677c4a5a..a6da62095060c 100644 --- a/src/core/server/saved_objects/migrations/core/migration_es_client.test.ts +++ b/src/core/server/saved_objects/migrations/core/migration_es_client.test.ts @@ -24,11 +24,11 @@ import { loggerMock } from '../../../logging/logger.mock'; import { SavedObjectsErrorHelpers } from '../../service/lib/errors'; describe('MigrationEsClient', () => { - let client: ReturnType; + let client: ReturnType; let migrationEsClient: MigrationEsClient; beforeEach(() => { - client = elasticsearchClientMock.createElasticSearchClient(); + client = elasticsearchClientMock.createElasticsearchClient(); migrationEsClient = createMigrationEsClient(client, loggerMock.create()); migrationRetryCallClusterMock.mockClear(); }); diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index c3ed97a89af80..cc443093e30a3 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -127,7 +127,7 @@ describe('KibanaMigrator', () => { }); type MockedOptions = KibanaMigratorOptions & { - client: ReturnType; + client: ReturnType; }; const mockOptions = () => { @@ -170,7 +170,7 @@ const mockOptions = () => { scrollDuration: '10m', skip: false, }, - client: elasticsearchClientMock.createElasticSearchClient(), + client: elasticsearchClientMock.createElasticsearchClient(), }; return options; }; diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index b902179b012ff..4a9fceb9bf357 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -201,7 +201,7 @@ describe('SavedObjectsRepository', () => { }; beforeEach(() => { - client = elasticsearchClientMock.createElasticSearchClient(); + client = elasticsearchClientMock.createElasticsearchClient(); migrator = { migrateDocument: jest.fn().mockImplementation(documentMigrator.migrate), runMigrations: async () => ({ status: 'skipped' }), diff --git a/src/core/server/saved_objects/service/lib/repository_es_client.test.ts b/src/core/server/saved_objects/service/lib/repository_es_client.test.ts index 86a984fb67124..61df94fb6bfe2 100644 --- a/src/core/server/saved_objects/service/lib/repository_es_client.test.ts +++ b/src/core/server/saved_objects/service/lib/repository_es_client.test.ts @@ -23,11 +23,11 @@ import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; import { SavedObjectsErrorHelpers } from './errors'; describe('RepositoryEsClient', () => { - let client: ReturnType; + let client: ReturnType; let repositoryClient: RepositoryEsClient; beforeEach(() => { - client = elasticsearchClientMock.createElasticSearchClient(); + client = elasticsearchClientMock.createElasticsearchClient(); repositoryClient = createRepositoryEsClient(client); retryCallClusterMock.mockClear(); }); diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index c94151f8cee17..c1054c27d084e 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -158,7 +158,7 @@ export type AppenderConfigType = TypeOf; // @public export function assertNever(x: never): never; -// @public (undocumented) +// @public @deprecated (undocumented) export interface AssistanceAPIResponse { // (undocumented) indices: { @@ -168,7 +168,7 @@ export interface AssistanceAPIResponse { }; } -// @public (undocumented) +// @public @deprecated (undocumented) export interface AssistantAPIClientParams extends GenericParams { // (undocumented) method: 'GET'; @@ -622,7 +622,7 @@ export interface DeleteDocumentResponse { _version: number; } -// @public (undocumented) +// @public @deprecated (undocumented) export interface DeprecationAPIClientParams extends GenericParams { // (undocumented) method: 'GET'; @@ -630,7 +630,7 @@ export interface DeprecationAPIClientParams extends GenericParams { path: '/_migration/deprecations'; } -// @public (undocumented) +// @public @deprecated (undocumented) export interface DeprecationAPIResponse { // (undocumented) cluster_settings: DeprecationInfo[]; @@ -642,7 +642,7 @@ export interface DeprecationAPIResponse { node_settings: DeprecationInfo[]; } -// @public (undocumented) +// @public @deprecated (undocumented) export interface DeprecationInfo { // (undocumented) details?: string; @@ -679,6 +679,14 @@ export type ElasticsearchClient = Omit & { + pingTimeout?: ElasticsearchConfig['pingTimeout'] | ClientOptions['pingTimeout']; + requestTimeout?: ElasticsearchConfig['requestTimeout'] | ClientOptions['requestTimeout']; + ssl?: Partial; + keepAlive?: boolean; +}; + // @public export class ElasticsearchConfig { constructor(rawConfig: ElasticsearchConfigType); @@ -715,6 +723,8 @@ export interface ElasticsearchServiceSetup { // @public (undocumented) export interface ElasticsearchServiceStart { + readonly client: IClusterClient; + readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient; // @deprecated (undocumented) legacy: { readonly createClient: (type: string, clientConfig?: Partial) => ILegacyCustomClusterClient; @@ -895,6 +905,12 @@ export interface HttpServiceStart { // @public export type IBasePath = Pick; +// @public +export interface IClusterClient { + readonly asInternalUser: ElasticsearchClient; + asScoped: (request: ScopeableRequest) => IScopedClusterClient; +} + // @public export interface IContextContainer> { createHandler(pluginOpaqueId: PluginOpaqueId, handler: THandler): (...rest: HandlerParameters) => ShallowPromise>; @@ -914,6 +930,11 @@ export interface ICspConfig { readonly warnLegacyBrowsers: boolean; } +// @public +export interface ICustomClusterClient extends IClusterClient { + close: () => Promise; +} + // @public export interface IKibanaResponse { // (undocumented) @@ -935,13 +956,13 @@ export interface IKibanaSocket { getPeerCertificate(detailed?: boolean): PeerCertificate | DetailedPeerCertificate | null; } -// @public +// @public @deprecated export type ILegacyClusterClient = Pick; -// @public +// @public @deprecated export type ILegacyCustomClusterClient = Pick; -// @public +// @public @deprecated export type ILegacyScopedClusterClient = Pick; // @public (undocumented) @@ -956,7 +977,7 @@ export interface ImageValidation { // @public export function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, savedObjectsClient, supportedTypes, namespace, }: SavedObjectsImportOptions): Promise; -// @public (undocumented) +// @public @deprecated (undocumented) export interface IndexSettingsDeprecationInfo { // (undocumented) [indexName: string]: DeprecationInfo[]; @@ -997,6 +1018,12 @@ export type ISavedObjectsRepository = Pick; +// @public +export interface IScopedClusterClient { + readonly asCurrentUser: ElasticsearchClient; + readonly asInternalUser: ElasticsearchClient; +} + // @public export function isRelativeUrl(candidatePath: string): boolean; @@ -1086,7 +1113,7 @@ export const kibanaResponseFactory: { // @public export type KnownHeaders = KnownKeys; -// @public (undocumented) +// @public @deprecated (undocumented) export interface LegacyAPICaller { // (undocumented) (endpoint: 'bulk', params: BulkIndexDocumentsParams, options?: LegacyCallAPIOptions): ReturnType; @@ -1330,15 +1357,13 @@ export interface LegacyAPICaller { (endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions): Promise; } -// @public +// @public @deprecated export interface LegacyCallAPIOptions { signal?: AbortSignal; wrap401Errors?: boolean; } -// Warning: (ae-unresolved-inheritdoc-reference) The @inheritDoc reference could not be resolved: The package "kibana" does not have an export "IClusterClient" -// -// @public (undocumented) +// @public @deprecated export class LegacyClusterClient implements ILegacyClusterClient { constructor(config: LegacyElasticsearchClientConfig, log: Logger, getAuditorFactory: () => AuditorFactory, getAuthHeaders?: GetAuthHeaders); asScoped(request?: ScopeableRequest): ILegacyScopedClusterClient; @@ -1360,7 +1385,7 @@ export interface LegacyConfig { set(config: LegacyVars): void; } -// @public (undocumented) +// @public @deprecated (undocumented) export type LegacyElasticsearchClientConfig = Pick & Pick & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ConfigOptions['pingTimeout']; requestTimeout?: ElasticsearchConfig['requestTimeout'] | ConfigOptions['requestTimeout']; @@ -1368,7 +1393,7 @@ export type LegacyElasticsearchClientConfig = Pick; }; -// @public (undocumented) +// @public export interface LegacyElasticsearchError extends Boom { // (undocumented) [code]?: string; @@ -1401,9 +1426,7 @@ export class LegacyInternals implements ILegacyInternals { export interface LegacyRequest extends Request { } -// Warning: (ae-unresolved-inheritdoc-reference) The @inheritDoc reference could not be resolved: The package "kibana" does not have an export "IScopedClusterClient" -// -// @public (undocumented) +// @public @deprecated export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller, headers?: Headers | undefined, auditor?: Auditor | undefined); callAsCurrentUser(endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions): Promise; @@ -1559,10 +1582,10 @@ export interface LogRecord { export interface MetricsServiceSetup { } -// @public (undocumented) +// @public @deprecated (undocumented) export type MIGRATION_ASSISTANCE_INDEX_ACTION = 'upgrade' | 'reindex'; -// @public (undocumented) +// @public @deprecated (undocumented) export type MIGRATION_DEPRECATION_LEVEL = 'none' | 'info' | 'warning' | 'critical'; // @public @@ -1812,6 +1835,7 @@ export interface RequestHandlerContext { typeRegistry: ISavedObjectTypeRegistry; }; elasticsearch: { + client: IScopedClusterClient; legacy: { client: ILegacyScopedClusterClient; }; diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 2dae7f8f38f23..aff749ca97534 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -275,6 +275,7 @@ export class Server { typeRegistry: coreStart.savedObjects.getTypeRegistry(), }, elasticsearch: { + client: coreStart.elasticsearch.client.asScoped(req), legacy: { client: coreStart.elasticsearch.legacy.client.asScoped(req), }, diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 7ad2f9edd3325..d35a6a5bbb9a9 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -22,6 +22,7 @@ import { CatTasksParams } from 'elasticsearch'; import { CatThreadPoolParams } from 'elasticsearch'; import { ClearScrollParams } from 'elasticsearch'; import { Client } from 'elasticsearch'; +import { ClientOptions } from '@elastic/elasticsearch'; import { ClusterAllocationExplainParams } from 'elasticsearch'; import { ClusterGetSettingsParams } from 'elasticsearch'; import { ClusterHealthParams } from 'elasticsearch'; diff --git a/tasks/config/run.js b/tasks/config/run.js index 9ac8f72d56d4a..132b51765b3ed 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -210,7 +210,7 @@ module.exports = function () { args: [ 'scripts/functional_tests', '--config', - 'test/plugin_functional/config.js', + 'test/plugin_functional/config.ts', '--bail', '--debug', ], diff --git a/test/api_integration/services/index.ts b/test/api_integration/services/index.ts index 782ea271869ba..d024943bef792 100644 --- a/test/api_integration/services/index.ts +++ b/test/api_integration/services/index.ts @@ -19,7 +19,6 @@ import { services as commonServices } from '../../common/services'; -// @ts-ignore not TS yet import { KibanaSupertestProvider, ElasticsearchSupertestProvider } from './supertest'; export const services = { diff --git a/test/plugin_functional/README.md b/test/plugin_functional/README.md index 476c08408658c..075d321917c39 100644 --- a/test/plugin_functional/README.md +++ b/test/plugin_functional/README.md @@ -17,9 +17,9 @@ To run these tests during development you can use the following commands: ``` # Start the test server (can continue running) -node scripts/functional_tests_server.js --config test/plugin_functional/config.js +node scripts/functional_tests_server.js --config test/plugin_functional/config.ts # Start a test run -node scripts/functional_test_runner.js --config test/plugin_functional/config.js +node scripts/functional_test_runner.js --config test/plugin_functional/config.ts ``` ## Run Kibana with a test plugin @@ -42,7 +42,7 @@ If you wish to load up specific es archived data for your test, you can do so vi Another option, which will automatically use any specific settings the test environment may rely on, is to boot up the functional test server pointing to the plugin configuration file. ``` -node scripts/functional_tests_server --config test/plugin_functional/config.js +node scripts/functional_tests_server --config test/plugin_functional/config.ts ``` *Note:* you may still need to use the es_archiver script to boot up any required data. diff --git a/test/plugin_functional/config.js b/test/plugin_functional/config.ts similarity index 94% rename from test/plugin_functional/config.js rename to test/plugin_functional/config.ts index f51fb5e1bade4..c611300eade10 100644 --- a/test/plugin_functional/config.js +++ b/test/plugin_functional/config.ts @@ -16,12 +16,11 @@ * specific language governing permissions and limitations * under the License. */ - +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import path from 'path'; import fs from 'fs'; -import { services } from './services'; -export default async function ({ readConfigFile }) { +export default async function ({ readConfigFile }: FtrConfigProviderContext) { const functionalConfig = await readConfigFile(require.resolve('../functional/config')); // Find all folders in ./plugins since we treat all them as plugin folder @@ -42,7 +41,6 @@ export default async function ({ readConfigFile }) { ], services: { ...functionalConfig.get('services'), - ...services, }, pageObjects: functionalConfig.get('pageObjects'), servers: functionalConfig.get('servers'), diff --git a/test/plugin_functional/plugins/elasticsearch_client_plugin/kibana.json b/test/plugin_functional/plugins/elasticsearch_client_plugin/kibana.json new file mode 100644 index 0000000000000..a7674881e8ba0 --- /dev/null +++ b/test/plugin_functional/plugins/elasticsearch_client_plugin/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "elasticsearch_client_plugin", + "version": "0.0.1", + "kibanaVersion": "kibana", + "server": true, + "ui": false +} diff --git a/test/plugin_functional/plugins/elasticsearch_client_plugin/package.json b/test/plugin_functional/plugins/elasticsearch_client_plugin/package.json new file mode 100644 index 0000000000000..02c2955f26c86 --- /dev/null +++ b/test/plugin_functional/plugins/elasticsearch_client_plugin/package.json @@ -0,0 +1,15 @@ +{ + "name": "elasticsearch_client_plugin", + "version": "1.0.0", + "kibana": { + "version": "kibana" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.9.5" + } +} diff --git a/test/plugin_functional/services/supertest.ts b/test/plugin_functional/plugins/elasticsearch_client_plugin/server/index.ts similarity index 66% rename from test/plugin_functional/services/supertest.ts rename to test/plugin_functional/plugins/elasticsearch_client_plugin/server/index.ts index 6b7dc26248c06..3801e33a2cf3e 100644 --- a/test/plugin_functional/services/supertest.ts +++ b/test/plugin_functional/plugins/elasticsearch_client_plugin/server/index.ts @@ -16,13 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { format as formatUrl } from 'url'; -import { FtrProviderContext } from 'test/functional/ftr_provider_context'; -import supertestAsPromised from 'supertest-as-promised'; +import { ElasticsearchClientPlugin } from './plugin'; -export function KibanaSupertestProvider({ getService }: FtrProviderContext) { - const config = getService('config'); - const kibanaServerUrl = formatUrl(config.get('servers.kibana')); - return supertestAsPromised(kibanaServerUrl); -} +export const plugin = () => new ElasticsearchClientPlugin(); diff --git a/test/plugin_functional/plugins/elasticsearch_client_plugin/server/plugin.ts b/test/plugin_functional/plugins/elasticsearch_client_plugin/server/plugin.ts new file mode 100644 index 0000000000000..5e018ca7818d3 --- /dev/null +++ b/test/plugin_functional/plugins/elasticsearch_client_plugin/server/plugin.ts @@ -0,0 +1,53 @@ +/* + * 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 { Plugin, CoreSetup, CoreStart, ICustomClusterClient } from 'src/core/server'; + +export class ElasticsearchClientPlugin implements Plugin { + private client?: ICustomClusterClient; + public setup(core: CoreSetup) { + const router = core.http.createRouter(); + router.get( + { path: '/api/elasticsearch_client_plugin/context/ping', validate: false }, + async (context, req, res) => { + const { body } = await context.core.elasticsearch.client.asInternalUser.ping(); + return res.ok({ body }); + } + ); + router.get( + { path: '/api/elasticsearch_client_plugin/contract/ping', validate: false }, + async (context, req, res) => { + const [coreStart] = await core.getStartServices(); + const { body } = await coreStart.elasticsearch.client.asInternalUser.ping(); + return res.ok({ body }); + } + ); + router.get( + { path: '/api/elasticsearch_client_plugin/custom_client/ping', validate: false }, + async (context, req, res) => { + const { body } = await this.client!.asInternalUser.ping(); + return res.ok({ body }); + } + ); + } + + public start(core: CoreStart) { + this.client = core.elasticsearch.createClient('my-custom-client-test'); + } + public stop() {} +} diff --git a/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json b/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json new file mode 100644 index 0000000000000..d0751f31ecc5e --- /dev/null +++ b/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "server/**/*.ts", + "../../../../typings/**/*" + ], + "exclude": [] +} diff --git a/test/plugin_functional/services/index.ts b/test/plugin_functional/services/index.ts index dd2b25e14fe17..453cfc5a8636f 100644 --- a/test/plugin_functional/services/index.ts +++ b/test/plugin_functional/services/index.ts @@ -16,14 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; -import { FtrProviderContext } from 'test/functional/ftr_provider_context'; -import { KibanaSupertestProvider } from './supertest'; +import { FtrProviderContext } from '../../functional/ftr_provider_context'; -export const services = { - supertest: KibanaSupertestProvider, -}; - -export type PluginFunctionalProviderContext = FtrProviderContext & - GenericFtrProviderContext; +export type PluginFunctionalProviderContext = FtrProviderContext; diff --git a/test/plugin_functional/test_suites/core_plugins/elasticsearch_client.ts b/test/plugin_functional/test_suites/core_plugins/elasticsearch_client.ts new file mode 100644 index 0000000000000..9b9efc261126f --- /dev/null +++ b/test/plugin_functional/test_suites/core_plugins/elasticsearch_client.ts @@ -0,0 +1,38 @@ +/* + * 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 { PluginFunctionalProviderContext } from '../../services'; +import '../../plugins/core_provider_plugin/types'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) { + const supertest = getService('supertest'); + describe('elasticsearch client', () => { + it('server plugins have access to elasticsearch client via request context', async () => { + await supertest.get('/api/elasticsearch_client_plugin/context/ping').expect(200, 'true'); + }); + it('server plugins have access to elasticsearch client via core contract', async () => { + await supertest.get('/api/elasticsearch_client_plugin/contract/ping').expect(200, 'true'); + }); + it('server plugins can create a custom elasticsearch client', async () => { + await supertest + .get('/api/elasticsearch_client_plugin/custom_client/ping') + .expect(200, 'true'); + }); + }); +} diff --git a/test/plugin_functional/test_suites/core_plugins/index.ts b/test/plugin_functional/test_suites/core_plugins/index.ts index 8f54ec6c0f4cd..99ac6dc9b8474 100644 --- a/test/plugin_functional/test_suites/core_plugins/index.ts +++ b/test/plugin_functional/test_suites/core_plugins/index.ts @@ -22,6 +22,7 @@ import { PluginFunctionalProviderContext } from '../../services'; export default function ({ loadTestFile }: PluginFunctionalProviderContext) { describe('core plugins', () => { loadTestFile(require.resolve('./applications')); + loadTestFile(require.resolve('./elasticsearch_client')); loadTestFile(require.resolve('./legacy_plugins')); loadTestFile(require.resolve('./server_plugins')); loadTestFile(require.resolve('./ui_plugins')); diff --git a/test/plugin_functional/test_suites/core_plugins/top_nav.js b/test/plugin_functional/test_suites/core_plugins/top_nav.ts similarity index 88% rename from test/plugin_functional/test_suites/core_plugins/top_nav.js rename to test/plugin_functional/test_suites/core_plugins/top_nav.ts index 5c46e3d7f76db..6d2c6b7f85d28 100644 --- a/test/plugin_functional/test_suites/core_plugins/top_nav.js +++ b/test/plugin_functional/test_suites/core_plugins/top_nav.ts @@ -17,8 +17,10 @@ * under the License. */ import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; -export default function ({ getService, getPageObjects }) { +// eslint-disable-next-line import/no-default-export +export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) { const PageObjects = getPageObjects(['common']); const browser = getService('browser'); diff --git a/x-pack/test/plugin_api_integration/config.ts b/x-pack/test/plugin_api_integration/config.ts index 6a86bcb7176f7..b89ed6ad550a3 100644 --- a/x-pack/test/plugin_api_integration/config.ts +++ b/x-pack/test/plugin_api_integration/config.ts @@ -20,6 +20,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { testFiles: [ + require.resolve('./test_suites/platform'), require.resolve('./test_suites/task_manager'), require.resolve('./test_suites/event_log'), require.resolve('./test_suites/licensed_feature_usage'), diff --git a/x-pack/test/plugin_api_integration/plugins/elasticsearch_client/kibana.json b/x-pack/test/plugin_api_integration/plugins/elasticsearch_client/kibana.json new file mode 100644 index 0000000000000..37ec33c168e76 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/elasticsearch_client/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "elasticsearch_client_xpack", + "version": "1.0.0", + "kibanaVersion": "kibana", + "server": true, + "ui": false +} diff --git a/x-pack/test/plugin_api_integration/plugins/elasticsearch_client/package.json b/x-pack/test/plugin_api_integration/plugins/elasticsearch_client/package.json new file mode 100644 index 0000000000000..ed31989472134 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/elasticsearch_client/package.json @@ -0,0 +1,14 @@ +{ + "name": "elasticsearch_client_xpack", + "version": "1.0.0", + "kibana": { + "version": "kibana" + }, + "scripts": { + "kbn": "node ../../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.9.5" + } +} diff --git a/x-pack/test/plugin_api_integration/plugins/elasticsearch_client/server/index.ts b/x-pack/test/plugin_api_integration/plugins/elasticsearch_client/server/index.ts new file mode 100644 index 0000000000000..19b54a9b0326d --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/elasticsearch_client/server/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { ElasticsearchClientXPack } from './plugin'; + +export const plugin = () => new ElasticsearchClientXPack(); diff --git a/x-pack/test/plugin_api_integration/plugins/elasticsearch_client/server/plugin.ts b/x-pack/test/plugin_api_integration/plugins/elasticsearch_client/server/plugin.ts new file mode 100644 index 0000000000000..921c8f9c50860 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/elasticsearch_client/server/plugin.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 { Plugin, CoreSetup } from 'kibana/server'; + +export class ElasticsearchClientXPack implements Plugin { + constructor() {} + + public setup(core: CoreSetup) { + const router = core.http.createRouter(); + router.get( + { path: '/api/elasticsearch_client_xpack/context/user', validate: false }, + async (context, req, res) => { + const { body } = await context.core.elasticsearch.client.asCurrentUser.security.getUser(); + return res.ok({ body }); + } + ); + + router.get( + { path: '/api/elasticsearch_client_xpack/contract/user', validate: false }, + async (context, req, res) => { + const [coreStart] = await core.getStartServices(); + const { body } = await coreStart.elasticsearch.client + .asScoped(req) + .asCurrentUser.security.getUser(); + return res.ok({ body }); + } + ); + } + + public start() {} + public stop() {} +} diff --git a/x-pack/test/plugin_api_integration/test_suites/platform/elasticsearch_client.ts b/x-pack/test/plugin_api_integration/test_suites/platform/elasticsearch_client.ts new file mode 100644 index 0000000000000..c2f6e93de83c1 --- /dev/null +++ b/x-pack/test/plugin_api_integration/test_suites/platform/elasticsearch_client.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + describe('elasticsearch client', () => { + it('scopes the elasticsearch client provided via request context to user credentials', async () => { + const { body } = await supertest + .get('/api/elasticsearch_client_xpack/context/user') + .expect(200); + expect(body).not.to.be.empty(); + }); + it('scopes the elasticsearch client provided via request context to user credentials', async () => { + const { body } = await supertest + .get('/api/elasticsearch_client_xpack/contract/user') + .expect(200); + expect(body).not.to.be.empty(); + }); + }); +} diff --git a/x-pack/test/plugin_api_integration/test_suites/platform/index.ts b/x-pack/test/plugin_api_integration/test_suites/platform/index.ts new file mode 100644 index 0000000000000..3375e9ca839a5 --- /dev/null +++ b/x-pack/test/plugin_api_integration/test_suites/platform/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. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('platform', function taskManagerSuite() { + this.tags('ciGroup2'); + loadTestFile(require.resolve('./elasticsearch_client')); + }); +} From 15f4ead00504f0fe368e423421524325e75246e6 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 30 Jul 2020 11:20:12 -0600 Subject: [PATCH 05/33] [SIEM] Fixes broken maps link to Kibana index management (#73757) ## Summary Embarrassing simple fix If you don't have an index setup like below and click configure index patterns it takes you to `/app/kibana` when it should just be `/app` and there shouldn't be anymore hashes of `#`. Screen Shot 2020-07-29 at 5 09 10 PM --- .../index_patterns_missing_prompt.test.tsx.snap | 4 ++-- .../embeddables/index_patterns_missing_prompt.tsx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap index 30f7df940fa99..b72b9e5c73977 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap @@ -6,7 +6,7 @@ exports[`IndexPatternsMissingPrompt renders correctly against snapshot 1`] = ` Configure index patterns @@ -28,7 +28,7 @@ exports[`IndexPatternsMissingPrompt renders correctly against snapshot 1`] = ` beats , "defaultIndex": diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/index_patterns_missing_prompt.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/index_patterns_missing_prompt.tsx index 62ffad515f424..b5595daa9cf47 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/index_patterns_missing_prompt.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/index_patterns_missing_prompt.tsx @@ -13,7 +13,7 @@ import * as i18n from './translations'; export const IndexPatternsMissingPromptComponent = () => { const { docLinks } = useKibana().services; - const kibanaBasePath = `${useBasePath()}/app/kibana`; + const kibanaBasePath = `${useBasePath()}/app`; return ( { values={{ defaultIndex: ( @@ -61,7 +61,7 @@ export const IndexPatternsMissingPromptComponent = () => { } actions={ Date: Thu, 30 Jul 2020 13:21:12 -0400 Subject: [PATCH 06/33] [SECURITY_SOLUTION][ENDPOINT] Fix Endpoint Host page sometimes incorrectly showing onboarding UI (#73222) * Fix incorrectly showing the onboarding message on hosts * Allow loading of the Details even if list load failed * Some test fixes * Fix missing import * refactor UT for the policy list view * Refactor more UT * remove commented out code * Fix some failing tests following cherry-picks * start work on mock host list apis * Remove extra code that came from cherry-pick * more tests fixed * All test pass * Refactoring ++ cleanup * Remove unused import --- .../common/endpoint/generate_data.ts | 105 +++++++++- .../pages/endpoint_hosts/store/action.ts | 6 + .../pages/endpoint_hosts/store/index.test.ts | 1 + .../pages/endpoint_hosts/store/middleware.ts | 42 +++- .../store/mock_host_result_list.ts | 148 +++++++++++++- .../pages/endpoint_hosts/store/reducer.ts | 7 + .../pages/endpoint_hosts/store/selectors.ts | 6 + .../management/pages/endpoint_hosts/types.ts | 2 + .../pages/endpoint_hosts/view/index.test.tsx | 192 +++++++++++------- .../pages/endpoint_hosts/view/index.tsx | 17 +- .../policy/store/policy_list/index.test.ts | 1 + .../policy/store/policy_list/middleware.ts | 1 + .../policy_list/mock_policy_result_list.ts | 40 ---- .../store/policy_list/services/ingest.test.ts | 6 +- .../store/policy_list/services/ingest.ts | 2 +- .../store/policy_list/test_mock_utils.ts | 162 ++++++--------- .../pages/policy/view/policy_details.test.tsx | 8 +- .../pages/policy/view/policy_list.test.tsx | 46 ++--- 18 files changed, 526 insertions(+), 266 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/mock_policy_result_list.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index 9a92270fc9c14..aa3f0bf287fca 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -8,16 +8,26 @@ import seedrandom from 'seedrandom'; import { AlertEvent, EndpointEvent, + EndpointStatus, Host, HostMetadata, - OSFields, HostPolicyResponse, HostPolicyResponseActionStatus, + OSFields, PolicyData, - EndpointStatus, } from './types'; import { factory as policyFactory } from './models/policy_config'; import { parentEntityId } from './models/event'; +import { + GetAgentConfigsResponseItem, + GetPackagesResponse, +} from '../../../ingest_manager/common/types/rest_spec'; +import { + AgentConfigStatus, + EsAssetReference, + InstallationStatus, + KibanaAssetReference, +} from '../../../ingest_manager/common/types/models'; export type Event = AlertEvent | EndpointEvent; /** @@ -1062,6 +1072,97 @@ export class EndpointDocGenerator { }; } + /** + * Generate an Agent Configuration (ingest) + */ + public generateAgentConfig(): GetAgentConfigsResponseItem { + return { + id: this.seededUUIDv4(), + name: 'Agent Config', + status: AgentConfigStatus.Active, + description: 'Some description', + namespace: 'default', + monitoring_enabled: ['logs', 'metrics'], + revision: 2, + updated_at: '2020-07-22T16:36:49.196Z', + updated_by: 'elastic', + package_configs: ['852491f0-cc39-11ea-bac2-cdbf95b4b41a'], + agents: 0, + }; + } + + /** + * Generate an EPM Package for Endpoint + */ + public generateEpmPackage(): GetPackagesResponse['response'][0] { + return { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '0.5.0', + description: 'This is the Elastic Endpoint package.', + type: 'solution', + download: '/epr/endpoint/endpoint-0.5.0.tar.gz', + path: '/package/endpoint/0.5.0', + icons: [ + { + src: '/package/endpoint/0.5.0/img/logo-endpoint-64-color.svg', + size: '16x16', + type: 'image/svg+xml', + }, + ], + status: 'installed' as InstallationStatus, + savedObject: { + type: 'epm-packages', + id: 'endpoint', + attributes: { + installed_kibana: [ + { id: '826759f0-7074-11ea-9bc8-6b38f4d29a16', type: 'dashboard' }, + { id: '1cfceda0-728b-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '1e525190-7074-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '55387750-729c-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '92b1edc0-706a-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: 'a3a3bd10-706b-11ea-9bc8-6b38f4d29a16', type: 'map' }, + ] as KibanaAssetReference[], + installed_es: [ + { id: 'logs-endpoint.alerts', type: 'index_template' }, + { id: 'events-endpoint', type: 'index_template' }, + { id: 'logs-endpoint.events.file', type: 'index_template' }, + { id: 'logs-endpoint.events.library', type: 'index_template' }, + { id: 'metrics-endpoint.metadata', type: 'index_template' }, + { id: 'metrics-endpoint.metadata_mirror', type: 'index_template' }, + { id: 'logs-endpoint.events.network', type: 'index_template' }, + { id: 'metrics-endpoint.policy', type: 'index_template' }, + { id: 'logs-endpoint.events.process', type: 'index_template' }, + { id: 'logs-endpoint.events.registry', type: 'index_template' }, + { id: 'logs-endpoint.events.security', type: 'index_template' }, + { id: 'metrics-endpoint.telemetry', type: 'index_template' }, + ] as EsAssetReference[], + es_index_patterns: { + alerts: 'logs-endpoint.alerts-*', + events: 'events-endpoint-*', + file: 'logs-endpoint.events.file-*', + library: 'logs-endpoint.events.library-*', + metadata: 'metrics-endpoint.metadata-*', + metadata_mirror: 'metrics-endpoint.metadata_mirror-*', + network: 'logs-endpoint.events.network-*', + policy: 'metrics-endpoint.policy-*', + process: 'logs-endpoint.events.process-*', + registry: 'logs-endpoint.events.registry-*', + security: 'logs-endpoint.events.security-*', + telemetry: 'metrics-endpoint.telemetry-*', + }, + name: 'endpoint', + version: '0.5.0', + internal: false, + removable: false, + }, + references: [], + updated_at: '2020-06-24T14:41:23.098Z', + version: 'Wzc0LDFd', + }, + }; + } + /** * Generates a Host Policy response message */ diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts index 621fab2e4ee11..4a4326d5b2919 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts @@ -81,6 +81,11 @@ interface ServerReturnedHostNonExistingPolicies { payload: HostState['nonExistingPolicies']; } +interface ServerReturnedHostExistValue { + type: 'serverReturnedHostExistValue'; + payload: boolean; +} + export type HostAction = | ServerReturnedHostList | ServerFailedToReturnHostList @@ -92,6 +97,7 @@ export type HostAction = | ServerFailedToReturnPoliciesForOnboarding | UserSelectedEndpointPolicy | ServerCancelledHostListLoading + | ServerReturnedHostExistValue | ServerCancelledPolicyItemsLoading | ServerReturnedEndpointPackageInfo | ServerReturnedHostNonExistingPolicies; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts index b6e18506b6111..8ff4ad5a043b5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts @@ -51,6 +51,7 @@ describe('HostList store concerns', () => { policyItemsLoading: false, endpointPackageInfo: undefined, nonExistingPolicies: {}, + hostsExist: true, }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index edeca5659ee38..74bebf211258a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HttpSetup } from 'kibana/public'; +import { HttpStart } from 'kibana/public'; import { HostInfo, HostResultList } from '../../../../../common/endpoint/types'; import { GetPolicyListResponse } from '../../policy/types'; import { ImmutableMiddlewareFactory } from '../../../../common/store'; @@ -28,6 +28,8 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor return ({ getState, dispatch }) => (next) => async (action) => { next(action); const state = getState(); + + // Host list if ( action.type === 'userChangedUrl' && isOnHostPage(state) && @@ -89,6 +91,19 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor // No hosts, so we should check to see if there are policies for onboarding if (hostResponse && hostResponse.hosts.length === 0) { const http = coreStart.http; + + // The original query to the list could have had an invalid param (ex. invalid page_size), + // so we check first if hosts actually do exist before pulling in data for the onboarding + // messages. + if (await doHostsExist(http)) { + return; + } + + dispatch({ + type: 'serverReturnedHostExistValue', + payload: false, + }); + try { const policyDataResponse: GetPolicyListResponse = await sendGetEndpointSpecificPackageConfigs( http, @@ -119,6 +134,8 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor }); } } + + // Host Details if (action.type === 'userChangedUrl' && hasSelectedHost(state) === true) { dispatch({ type: 'serverCancelledPolicyItemsLoading', @@ -160,7 +177,6 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor type: 'serverFailedToReturnHostList', payload: error, }); - return; } } else { dispatch({ @@ -217,7 +233,7 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor }; const getNonExistingPoliciesForHostsList = async ( - http: HttpSetup, + http: HttpStart, hosts: HostResultList['hosts'], currentNonExistingPolicies: HostState['nonExistingPolicies'] ): Promise => { @@ -274,3 +290,23 @@ const getNonExistingPoliciesForHostsList = async ( return nonExisting; }; + +const doHostsExist = async (http: HttpStart): Promise => { + try { + return ( + ( + await http.post('/api/endpoint/metadata', { + body: JSON.stringify({ + paging_properties: [{ page_index: 0 }, { page_size: 1 }], + }), + }) + ).hosts.length !== 0 + ); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`error while trying to check if hosts exist`); + // eslint-disable-next-line no-console + console.error(error); + } + return false; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts index 05af1ee062de6..355c2bb5c19fc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts @@ -4,8 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostInfo, HostResultList, HostStatus } from '../../../../../common/endpoint/types'; +import { HttpStart } from 'kibana/public'; +import { + GetHostPolicyResponse, + HostInfo, + HostPolicyResponse, + HostResultList, + HostStatus, +} from '../../../../../common/endpoint/types'; import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; +import { + INGEST_API_AGENT_CONFIGS, + INGEST_API_EPM_PACKAGES, + INGEST_API_PACKAGE_CONFIGS, +} from '../../policy/store/policy_list/services/ingest'; +import { + GetAgentConfigsResponse, + GetPackagesResponse, +} from '../../../../../../ingest_manager/common/types/rest_spec'; +import { GetPolicyListResponse } from '../../policy/types'; + +const generator = new EndpointDocGenerator('seed'); export const mockHostResultList: (options?: { total?: number; @@ -26,7 +45,6 @@ export const mockHostResultList: (options?: { const hosts = []; for (let index = 0; index < actualCountToReturn; index++) { - const generator = new EndpointDocGenerator('seed'); hosts.push({ metadata: generator.generateHostMetadata(), host_status: HostStatus.ERROR, @@ -45,9 +63,133 @@ export const mockHostResultList: (options?: { * returns a mocked API response for retrieving a single host metadata */ export const mockHostDetailsApiResult = (): HostInfo => { - const generator = new EndpointDocGenerator('seed'); return { metadata: generator.generateHostMetadata(), host_status: HostStatus.ERROR, }; }; + +/** + * Mock API handlers used by the Endpoint Host list. It also sets up a list of + * API handlers for Host details based on a list of Host results. + */ +const hostListApiPathHandlerMocks = ({ + hostsResults = mockHostResultList({ total: 3 }).hosts, + epmPackages = [generator.generateEpmPackage()], + endpointPackageConfigs = [], + policyResponse = generator.generatePolicyResponse(), +}: { + /** route handlers will be setup for each individual host in this array */ + hostsResults?: HostResultList['hosts']; + epmPackages?: GetPackagesResponse['response']; + endpointPackageConfigs?: GetPolicyListResponse['items']; + policyResponse?: HostPolicyResponse; +} = {}) => { + const apiHandlers = { + // endpoint package info + [INGEST_API_EPM_PACKAGES]: (): GetPackagesResponse => { + return { + response: epmPackages, + success: true, + }; + }, + + // host list + '/api/endpoint/metadata': (): HostResultList => { + return { + hosts: hostsResults, + request_page_size: 10, + request_page_index: 0, + total: hostsResults?.length || 0, + }; + }, + + // Do policies referenced in host list exist + // just returns 1 single agent config that includes all of the packageConfig IDs provided + [INGEST_API_AGENT_CONFIGS]: (): GetAgentConfigsResponse => { + const agentConfig = generator.generateAgentConfig(); + (agentConfig.package_configs as string[]).push( + ...endpointPackageConfigs.map((packageConfig) => packageConfig.id) + ); + return { + items: [agentConfig], + total: 10, + success: true, + perPage: 10, + page: 1, + }; + }, + + // Policy Response + '/api/endpoint/policy_response': (): GetHostPolicyResponse => { + return { policy_response: policyResponse }; + }, + + // List of Policies (package configs) for onboarding + [INGEST_API_PACKAGE_CONFIGS]: (): GetPolicyListResponse => { + return { + items: endpointPackageConfigs, + page: 1, + perPage: 10, + total: endpointPackageConfigs?.length, + success: true, + }; + }, + }; + + // Build a GET route handler for each host details based on the list of Hosts passed on input + if (hostsResults) { + hostsResults.forEach((host) => { + // @ts-ignore + apiHandlers[`/api/endpoint/metadata/${host.metadata.host.id}`] = () => host; + }); + } + + return apiHandlers; +}; + +/** + * Sets up mock impelementations in support of the Hosts list view + * + * @param mockedHttpService + * @param hostsResults + * @param pathHandlersOptions + */ +export const setHostListApiMockImplementation: ( + mockedHttpService: jest.Mocked, + apiResponses?: Parameters[0] +) => void = ( + mockedHttpService, + { hostsResults = mockHostResultList({ total: 3 }).hosts, ...pathHandlersOptions } = {} +) => { + const apiHandlers = hostListApiPathHandlerMocks({ ...pathHandlersOptions, hostsResults }); + + mockedHttpService.post + .mockImplementation(async (...args) => { + throw new Error(`un-expected call to http.post: ${args}`); + }) + // First time called, return list of hosts + .mockImplementationOnce(async () => { + return apiHandlers['/api/endpoint/metadata'](); + }); + + // If the hosts list results is zero, then mock the second call to `/metadata` to return + // empty list - indicating there are no hosts currently present on the system + if (!hostsResults.length) { + mockedHttpService.post.mockImplementationOnce(async () => { + return apiHandlers['/api/endpoint/metadata'](); + }); + } + + // Setup handling of GET requests + mockedHttpService.get.mockImplementation(async (...args) => { + const [path] = args; + if (typeof path === 'string') { + if (apiHandlers[path]) { + return apiHandlers[path](); + } + } + + throw new Error(`MOCK: api request does not have a mocked handler: ${path}`); + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index 7f68baa4b85bd..e54f7df4d4f75 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -29,6 +29,7 @@ export const initialHostListState: Immutable = { policyItemsLoading: false, endpointPackageInfo: undefined, nonExistingPolicies: {}, + hostsExist: true, }; /* eslint-disable-next-line complexity */ @@ -125,6 +126,11 @@ export const hostListReducer: ImmutableReducer = ( ...state, endpointPackageInfo: action.payload, }; + } else if (action.type === 'serverReturnedHostExistValue') { + return { + ...state, + hostsExist: action.payload, + }; } else if (action.type === 'userChangedUrl') { const newState: Immutable = { ...state, @@ -181,6 +187,7 @@ export const hostListReducer: ImmutableReducer = ( error: undefined, detailsError: undefined, policyResponseError: undefined, + hostsExist: true, }; } return state; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts index 6e0823a920413..ca006f21c29ac 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts @@ -203,3 +203,9 @@ export const policyResponseStatus: (state: Immutable) => string = cre export const nonExistingPolicies: ( state: Immutable ) => Immutable = (state) => state.nonExistingPolicies; + +/** + * Return boolean that indicates whether hosts exist + * @param state + */ +export const hostsExist: (state: Immutable) => boolean = (state) => state.hostsExist; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts index 582a59cfd7605..6c949e9700b9a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts @@ -52,6 +52,8 @@ export interface HostState { endpointPackageInfo?: GetPackagesResponse['response'][0]; /** tracks the list of policies IDs used in Host metadata that may no longer exist */ nonExistingPolicies: Record; + /** Tracks whether hosts exist and helps control if onboarding should be visible */ + hostsExist: boolean; } /** diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 9d49c8705affe..3e00a5cc33db1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -8,18 +8,22 @@ import React from 'react'; import * as reactTestingLibrary from '@testing-library/react'; import { HostList } from './index'; -import { mockHostDetailsApiResult, mockHostResultList } from '../store/mock_host_result_list'; -import { mockPolicyResultList } from '../../policy/store/policy_list/mock_policy_result_list'; +import { + mockHostDetailsApiResult, + mockHostResultList, + setHostListApiMockImplementation, +} from '../store/mock_host_result_list'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; import { HostInfo, + HostPolicyResponse, HostPolicyResponseActionStatus, HostPolicyResponseAppliedAction, HostStatus, } from '../../../../../common/endpoint/types'; import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; -import { AppAction } from '../../../../common/store/actions'; import { POLICY_STATUS_TO_HEALTH_COLOR, POLICY_STATUS_TO_TEXT } from './host_constants'; +import { mockPolicyResultList } from '../../policy/store/policy_list/test_mock_utils'; jest.mock('../../../../common/components/link_to'); @@ -35,6 +39,9 @@ describe('when on the hosts page', () => { const mockedContext = createAppRootMockRenderer(); ({ history, store, coreStart, middlewareSpy } = mockedContext); render = () => mockedContext.render(); + reactTestingLibrary.act(() => { + history.push('/hosts'); + }); }); it('should NOT display timeline', async () => { @@ -43,35 +50,29 @@ describe('when on the hosts page', () => { expect(timelineFlyout).toBeNull(); }); - it('should show the empty state when there are no hosts or polices', async () => { - const renderResult = render(); - // Initially, there are no hosts or policies, so we prompt to add policies first. - const table = await renderResult.findByTestId('emptyPolicyTable'); - expect(table).not.toBeNull(); - }); - - describe('when there are policies, but no hosts', () => { + describe('when there are no hosts or polices', () => { beforeEach(() => { - reactTestingLibrary.act(() => { - const hostListData = mockHostResultList({ total: 0 }); - coreStart.http.get.mockReturnValue(Promise.resolve(hostListData)); - const hostAction: AppAction = { - type: 'serverReturnedHostList', - payload: hostListData, - }; - store.dispatch(hostAction); + setHostListApiMockImplementation(coreStart.http, { + hostsResults: [], + }); + }); - jest.clearAllMocks(); + it('should show the empty state when there are no hosts or polices', async () => { + const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding'); + }); + // Initially, there are no hosts or policies, so we prompt to add policies first. + const table = await renderResult.findByTestId('emptyPolicyTable'); + expect(table).not.toBeNull(); + }); + }); - const policyListData = mockPolicyResultList({ total: 3 }); - coreStart.http.get.mockReturnValue(Promise.resolve(policyListData)); - const policyAction: AppAction = { - type: 'serverReturnedPoliciesForOnboarding', - payload: { - policyItems: policyListData.items, - }, - }; - store.dispatch(policyAction); + describe('when there are policies, but no hosts', () => { + beforeEach(async () => { + setHostListApiMockImplementation(coreStart.http, { + hostsResults: [], + endpointPackageConfigs: mockPolicyResultList({ total: 3 }).items, }); }); afterEach(() => { @@ -80,18 +81,27 @@ describe('when on the hosts page', () => { it('should show the no hosts empty state', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding'); + }); const emptyHostsTable = await renderResult.findByTestId('emptyHostsTable'); expect(emptyHostsTable).not.toBeNull(); }); it('should display the onboarding steps', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding'); + }); const onboardingSteps = await renderResult.findByTestId('onboardingSteps'); expect(onboardingSteps).not.toBeNull(); }); it('should show policy selection', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding'); + }); const onboardingPolicySelect = await renderResult.findByTestId('onboardingPolicySelect'); expect(onboardingPolicySelect).not.toBeNull(); }); @@ -112,39 +122,54 @@ describe('when on the hosts page', () => { let firstPolicyID: string; beforeEach(() => { reactTestingLibrary.act(() => { - const hostListData = mockHostResultList({ total: 4 }); - firstPolicyID = hostListData.hosts[0].metadata.Endpoint.policy.applied.id; + const hostListData = mockHostResultList({ total: 4 }).hosts; + + firstPolicyID = hostListData[0].metadata.Endpoint.policy.applied.id; + [HostStatus.ERROR, HostStatus.ONLINE, HostStatus.OFFLINE, HostStatus.UNENROLLING].forEach( (status, index) => { - hostListData.hosts[index] = { - metadata: hostListData.hosts[index].metadata, + hostListData[index] = { + metadata: hostListData[index].metadata, host_status: status, }; } ); - hostListData.hosts.forEach((item, index) => { + hostListData.forEach((item, index) => { generatedPolicyStatuses[index] = item.metadata.Endpoint.policy.applied.status; }); - const action: AppAction = { - type: 'serverReturnedHostList', - payload: hostListData, - }; - store.dispatch(action); + + // Make sure that the first policy id in the host result is not set as non-existent + const ingestPackageConfigs = mockPolicyResultList({ total: 1 }).items; + ingestPackageConfigs[0].id = firstPolicyID; + + setHostListApiMockImplementation(coreStart.http, { + hostsResults: hostListData, + endpointPackageConfigs: ingestPackageConfigs, + }); }); }); it('should display rows in the table', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const rows = await renderResult.findAllByRole('row'); expect(rows).toHaveLength(5); }); it('should show total', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const total = await renderResult.findByTestId('hostListTableTotal'); expect(total.textContent).toEqual('4 Hosts'); }); it('should display correct status', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const hostStatuses = await renderResult.findAllByTestId('rowHostStatus'); expect(hostStatuses[0].textContent).toEqual('Error'); @@ -168,6 +193,9 @@ describe('when on the hosts page', () => { it('should display correct policy status', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const policyStatuses = await renderResult.findAllByTestId('rowPolicyStatus'); policyStatuses.forEach((status, index) => { @@ -184,6 +212,9 @@ describe('when on the hosts page', () => { it('should display policy name as a link', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const firstPolicyName = (await renderResult.findAllByTestId('policyNameCellLink'))[0]; expect(firstPolicyName).not.toBeNull(); expect(firstPolicyName.getAttribute('href')).toContain(`policy/${firstPolicyID}`); @@ -192,17 +223,10 @@ describe('when on the hosts page', () => { describe('when the user clicks the first hostname in the table', () => { let renderResult: reactTestingLibrary.RenderResult; beforeEach(async () => { - const hostDetailsApiResponse = mockHostDetailsApiResult(); - - coreStart.http.get.mockReturnValue(Promise.resolve(hostDetailsApiResponse)); - reactTestingLibrary.act(() => { - store.dispatch({ - type: 'serverReturnedHostDetails', - payload: hostDetailsApiResponse, - }); - }); - renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const hostNameLinks = await renderResult.findAllByTestId('hostnameCellLink'); if (hostNameLinks.length) { reactTestingLibrary.fireEvent.click(hostNameLinks[0]); @@ -221,9 +245,11 @@ describe('when on the hosts page', () => { describe('when there is a selected host in the url', () => { let hostDetails: HostInfo; let agentId: string; - const dispatchServerReturnedHostPolicyResponse = ( + let renderAndWaitForData: () => Promise>; + + const createPolicyResponse = ( overallStatus: HostPolicyResponseActionStatus = HostPolicyResponseActionStatus.success - ) => { + ): HostPolicyResponse => { const policyResponse = docGenerator.generatePolicyResponse(); const malwareResponseConfigurations = policyResponse.Endpoint.policy.applied.response.configurations.malware; @@ -269,21 +295,28 @@ describe('when on the hosts page', () => { policyResponse.Endpoint.policy.applied.actions.push(unknownAction); malwareResponseConfigurations.concerned_actions.push(unknownAction.name); + return policyResponse; + }; + + const dispatchServerReturnedHostPolicyResponse = ( + overallStatus: HostPolicyResponseActionStatus = HostPolicyResponseActionStatus.success + ) => { reactTestingLibrary.act(() => { store.dispatch({ type: 'serverReturnedHostPolicyResponse', payload: { - policy_response: policyResponse, + policy_response: createPolicyResponse(overallStatus), }, }); }); }; - beforeEach(() => { + beforeEach(async () => { const { host_status, metadata: { host, ...details }, } = mockHostDetailsApiResult(); + hostDetails = { host_status, metadata: { @@ -297,34 +330,37 @@ describe('when on the hosts page', () => { agentId = hostDetails.metadata.elastic.agent.id; - coreStart.http.get.mockReturnValue(Promise.resolve(hostDetails)); + const policy = docGenerator.generatePolicyPackageConfig(); + policy.id = hostDetails.metadata.Endpoint.policy.applied.id; - reactTestingLibrary.act(() => { - history.push({ - ...history.location, - search: '?selected_host=1', - }); + setHostListApiMockImplementation(coreStart.http, { + hostsResults: [hostDetails], + endpointPackageConfigs: [policy], }); + reactTestingLibrary.act(() => { - store.dispatch({ - type: 'serverReturnedHostDetails', - payload: hostDetails, - }); + history.push('/hosts?selected_host=1'); }); + + renderAndWaitForData = async () => { + const renderResult = render(); + await middlewareSpy.waitForAction('serverReturnedHostDetails'); + return renderResult; + }; }); afterEach(() => { jest.clearAllMocks(); }); - it('should show the flyout', () => { - const renderResult = render(); + it('should show the flyout', async () => { + const renderResult = await renderAndWaitForData(); return renderResult.findByTestId('hostDetailsFlyout').then((flyout) => { expect(flyout).not.toBeNull(); }); }); it('should display policy name value as a link', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const policyDetailsLink = await renderResult.findByTestId('policyDetailsValue'); expect(policyDetailsLink).not.toBeNull(); expect(policyDetailsLink.getAttribute('href')).toEqual( @@ -333,10 +369,7 @@ describe('when on the hosts page', () => { }); it('should update the URL when policy name link is clicked', async () => { - const policyItem = mockPolicyResultList({ total: 1 }).items[0]; - coreStart.http.get.mockReturnValue(Promise.resolve({ item: policyItem })); - - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const policyDetailsLink = await renderResult.findByTestId('policyDetailsValue'); const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); reactTestingLibrary.act(() => { @@ -349,7 +382,7 @@ describe('when on the hosts page', () => { }); it('should display policy status value as a link', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); expect(policyStatusLink).not.toBeNull(); expect(policyStatusLink.getAttribute('href')).toEqual( @@ -358,7 +391,7 @@ describe('when on the hosts page', () => { }); it('should update the URL when policy status link is clicked', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); reactTestingLibrary.act(() => { @@ -371,7 +404,7 @@ describe('when on the hosts page', () => { }); it('should display Success overall policy status', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); reactTestingLibrary.act(() => { dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.success); }); @@ -385,7 +418,7 @@ describe('when on the hosts page', () => { }); it('should display Warning overall policy status', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); reactTestingLibrary.act(() => { dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.warning); }); @@ -399,7 +432,7 @@ describe('when on the hosts page', () => { }); it('should display Failed overall policy status', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); reactTestingLibrary.act(() => { dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.failure); }); @@ -413,7 +446,7 @@ describe('when on the hosts page', () => { }); it('should display Unknown overall policy status', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); reactTestingLibrary.act(() => { dispatchServerReturnedHostPolicyResponse('' as HostPolicyResponseActionStatus); }); @@ -428,7 +461,7 @@ describe('when on the hosts page', () => { it('should include the link to reassignment in Ingest', async () => { coreStart.application.getUrlForApp.mockReturnValue('/app/ingestManager'); - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const linkToReassign = await renderResult.findByTestId('hostDetailsLinkToIngest'); expect(linkToReassign).not.toBeNull(); expect(linkToReassign.textContent).toEqual('Reassign Configuration'); @@ -440,7 +473,7 @@ describe('when on the hosts page', () => { describe('when link to reassignment in Ingest is clicked', () => { beforeEach(async () => { coreStart.application.getUrlForApp.mockReturnValue('/app/ingestManager'); - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const linkToReassign = await renderResult.findByTestId('hostDetailsLinkToIngest'); reactTestingLibrary.act(() => { reactTestingLibrary.fireEvent.click(linkToReassign); @@ -461,13 +494,14 @@ describe('when on the hosts page', () => { } throw new Error(`POST to '${requestOptions.path}' does not have a mock response!`); }); - renderResult = render(); + renderResult = await renderAndWaitForData(); const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); reactTestingLibrary.act(() => { reactTestingLibrary.fireEvent.click(policyStatusLink); }); await userChangedUrlChecker; + await middlewareSpy.waitForAction('serverReturnedHostPolicyResponse'); reactTestingLibrary.act(() => { dispatchServerReturnedHostPolicyResponse(); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 2692f7791b7c0..58442ab417b60 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -89,6 +89,7 @@ export const HostList = () => { selectedPolicyId, policyItemsLoading, endpointPackageVersion, + hostsExist, } = useHostSelector(selector); const { formatUrl, search } = useFormatUrl(SecurityPageName.administration); @@ -329,7 +330,7 @@ export const HostList = () => { }, [formatUrl, queryParams, search]); const renderTableOrEmptyState = useMemo(() => { - if (!loading && listData && listData.length > 0) { + if (hostsExist) { return ( { error={listError?.message} pagination={paginationSetup} onChange={onTableChange} + loading={loading} /> ); } else if (!policyItemsLoading && policyItems && policyItems.length > 0) { @@ -356,19 +358,20 @@ export const HostList = () => { ); } }, [ - listData, + loading, + hostsExist, + policyItemsLoading, policyItems, + listData, columns, - loading, + listError?.message, paginationSetup, onTableChange, - listError?.message, - handleCreatePolicyClick, handleDeployEndpointsClick, - handleSelectableOnChange, selectedPolicyId, + handleSelectableOnChange, selectionOptions, - policyItemsLoading, + handleCreatePolicyClick, ]); return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts index 8203aae244f24..6ee6a4232f7cf 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts @@ -143,6 +143,7 @@ describe('policy list store concerns', () => { isLoading: false, isDeleting: false, deleteStatus: undefined, + endpointPackageInfo: undefined, pageIndex: 0, pageSize: 10, total: 0, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts index b4e1da4e43da3..6fe555113617d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts @@ -29,6 +29,7 @@ export const policyListMiddlewareFactory: ImmutableMiddlewareFactory GetPolicyListResponse = (options = {}) => { - const { - total = 1, - request_page_size: requestPageSize = 10, - request_page_index: requestPageIndex = 0, - } = options; - - // Skip any that are before the page we're on - const numberToSkip = requestPageSize * requestPageIndex; - - // total - numberToSkip is the count of non-skipped ones, but return no more than a pageSize, and no less than 0 - const actualCountToReturn = Math.max(Math.min(total - numberToSkip, requestPageSize), 0); - - const policies = []; - for (let index = 0; index < actualCountToReturn; index++) { - const generator = new EndpointDocGenerator('seed'); - policies.push(generator.generatePolicyPackageConfig()); - } - const mock: GetPolicyListResponse = { - items: policies, - total, - page: requestPageIndex, - perPage: requestPageSize, - success: true, - }; - return mock; -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts index 7daa500ef1884..83cd8558306a0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts @@ -12,7 +12,7 @@ import { } from './ingest'; import { httpServiceMock } from '../../../../../../../../../../src/core/public/mocks'; import { PACKAGE_CONFIG_SAVED_OBJECT_TYPE } from '../../../../../../../../ingest_manager/common'; -import { apiPathMockResponseProviders } from '../test_mock_utils'; +import { policyListApiPathHandlers } from '../test_mock_utils'; describe('ingest service', () => { let http: ReturnType; @@ -61,7 +61,9 @@ describe('ingest service', () => { describe('sendGetEndpointSecurityPackage()', () => { it('should query EPM with category=security', async () => { - http.get.mockReturnValue(apiPathMockResponseProviders[INGEST_API_EPM_PACKAGES]()); + http.get.mockReturnValue( + Promise.resolve(policyListApiPathHandlers()[INGEST_API_EPM_PACKAGES]()) + ); await sendGetEndpointSecurityPackage(http); expect(http.get).toHaveBeenCalledWith('/api/ingest_manager/epm/packages', { query: { category: 'security' }, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts index c6e6146f4d5e4..266faf9eae32c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts @@ -20,7 +20,7 @@ import { NewPolicyData } from '../../../../../../../common/endpoint/types'; const INGEST_API_ROOT = `/api/ingest_manager`; export const INGEST_API_PACKAGE_CONFIGS = `${INGEST_API_ROOT}/package_configs`; -const INGEST_API_AGENT_CONFIGS = `${INGEST_API_ROOT}/agent_configs`; +export const INGEST_API_AGENT_CONFIGS = `${INGEST_API_ROOT}/agent_configs`; const INGEST_API_FLEET = `${INGEST_API_ROOT}/fleet`; const INGEST_API_FLEET_AGENT_STATUS = `${INGEST_API_FLEET}/agent-status`; export const INGEST_API_EPM_PACKAGES = `${INGEST_API_ROOT}/epm/packages`; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts index b5c67cc2c2014..3c9d5fde9b826 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts @@ -5,122 +5,84 @@ */ import { HttpStart } from 'kibana/public'; -import { INGEST_API_PACKAGE_CONFIGS, INGEST_API_EPM_PACKAGES } from './services/ingest'; +import { INGEST_API_EPM_PACKAGES, INGEST_API_PACKAGE_CONFIGS } from './services/ingest'; import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data'; import { GetPolicyListResponse } from '../../types'; -import { - KibanaAssetReference, - EsAssetReference, - GetPackagesResponse, - InstallationStatus, -} from '../../../../../../../ingest_manager/common'; +import { GetPackagesResponse } from '../../../../../../../ingest_manager/common'; const generator = new EndpointDocGenerator('policy-list'); -/** - * a list of API paths response mock providers - */ -export const apiPathMockResponseProviders = { - [INGEST_API_EPM_PACKAGES]: () => - Promise.resolve({ - response: [ - { - name: 'endpoint', - title: 'Elastic Endpoint', - version: '0.5.0', - description: 'This is the Elastic Endpoint package.', - type: 'solution', - download: '/epr/endpoint/endpoint-0.5.0.tar.gz', - path: '/package/endpoint/0.5.0', - icons: [ - { - src: '/package/endpoint/0.5.0/img/logo-endpoint-64-color.svg', - size: '16x16', - type: 'image/svg+xml', - }, - ], - status: 'installed' as InstallationStatus, - savedObject: { - type: 'epm-packages', - id: 'endpoint', - attributes: { - installed_kibana: [ - { id: '826759f0-7074-11ea-9bc8-6b38f4d29a16', type: 'dashboard' }, - { id: '1cfceda0-728b-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, - { id: '1e525190-7074-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, - { id: '55387750-729c-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, - { id: '92b1edc0-706a-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, - { id: 'a3a3bd10-706b-11ea-9bc8-6b38f4d29a16', type: 'map' }, - ] as KibanaAssetReference[], - installed_es: [ - { id: 'logs-endpoint.alerts', type: 'index_template' }, - { id: 'events-endpoint', type: 'index_template' }, - { id: 'logs-endpoint.events.file', type: 'index_template' }, - { id: 'logs-endpoint.events.library', type: 'index_template' }, - { id: 'metrics-endpoint.metadata', type: 'index_template' }, - { id: 'metrics-endpoint.metadata_mirror', type: 'index_template' }, - { id: 'logs-endpoint.events.network', type: 'index_template' }, - { id: 'metrics-endpoint.policy', type: 'index_template' }, - { id: 'logs-endpoint.events.process', type: 'index_template' }, - { id: 'logs-endpoint.events.registry', type: 'index_template' }, - { id: 'logs-endpoint.events.security', type: 'index_template' }, - { id: 'metrics-endpoint.telemetry', type: 'index_template' }, - ] as EsAssetReference[], - es_index_patterns: { - alerts: 'logs-endpoint.alerts-*', - events: 'events-endpoint-*', - file: 'logs-endpoint.events.file-*', - library: 'logs-endpoint.events.library-*', - metadata: 'metrics-endpoint.metadata-*', - metadata_mirror: 'metrics-endpoint.metadata_mirror-*', - network: 'logs-endpoint.events.network-*', - policy: 'metrics-endpoint.policy-*', - process: 'logs-endpoint.events.process-*', - registry: 'logs-endpoint.events.registry-*', - security: 'logs-endpoint.events.security-*', - telemetry: 'metrics-endpoint.telemetry-*', - }, - name: 'endpoint', - version: '0.5.0', - internal: false, - removable: false, - }, - references: [], - updated_at: '2020-06-24T14:41:23.098Z', - version: 'Wzc0LDFd', - }, - }, - ], - success: true, - }), -}; - /** * It sets the mock implementation on the necessary http methods to support the policy list view * @param mockedHttpService - * @param responseItems + * @param totalPolicies */ export const setPolicyListApiMockImplementation = ( mockedHttpService: jest.Mocked, - responseItems: GetPolicyListResponse['items'] = [generator.generatePolicyPackageConfig()] + totalPolicies: number = 1 ): void => { - mockedHttpService.get.mockImplementation((...args) => { + const policyApiHandlers = policyListApiPathHandlers(totalPolicies); + + mockedHttpService.get.mockImplementation(async (...args) => { const [path] = args; if (typeof path === 'string') { - if (path === INGEST_API_PACKAGE_CONFIGS) { - return Promise.resolve({ - items: responseItems, - total: 10, - page: 1, - perPage: 10, - success: true, - }); - } - - if (apiPathMockResponseProviders[path]) { - return apiPathMockResponseProviders[path](); + if (policyApiHandlers[path]) { + return policyApiHandlers[path](); } } return Promise.reject(new Error(`MOCK: unknown policy list api: ${path}`)); }); }; + +/** + * Returns the response body for a call to get the list of Policies + * @param options + */ +export const mockPolicyResultList: (options?: { + total?: number; + request_page_size?: number; + request_page_index?: number; +}) => GetPolicyListResponse = (options = {}) => { + const { + total = 1, + request_page_size: requestPageSize = 10, + request_page_index: requestPageIndex = 0, + } = options; + + // Skip any that are before the page we're on + const numberToSkip = requestPageSize * requestPageIndex; + + // total - numberToSkip is the count of non-skipped ones, but return no more than a pageSize, and no less than 0 + const actualCountToReturn = Math.max(Math.min(total - numberToSkip, requestPageSize), 0); + + const policies = []; + for (let index = 0; index < actualCountToReturn; index++) { + policies.push(generator.generatePolicyPackageConfig()); + } + const mock: GetPolicyListResponse = { + items: policies, + total, + page: requestPageIndex, + perPage: requestPageSize, + success: true, + }; + return mock; +}; + +/** + * Returns an object comprised of the API path as the key along with a function that + * returns that API's result value + */ +export const policyListApiPathHandlers = (totalPolicies: number = 1) => { + return { + [INGEST_API_PACKAGE_CONFIGS]: () => { + return mockPolicyResultList({ total: totalPolicies }); + }, + [INGEST_API_EPM_PACKAGES]: (): GetPackagesResponse => { + return { + response: [generator.generateEpmPackage()], + success: true, + }; + }, + }; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx index 03ab32dcb2b66..c81ffb0060c88 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx @@ -11,7 +11,7 @@ import { PolicyDetails } from './policy_details'; import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; import { getPolicyDetailPath, getHostListPath } from '../../../common/routing'; -import { apiPathMockResponseProviders } from '../store/policy_list/test_mock_utils'; +import { policyListApiPathHandlers } from '../store/policy_list/test_mock_utils'; jest.mock('../../../../common/components/link_to'); @@ -80,6 +80,8 @@ describe('Policy Details', () => { policyPackageConfig = generator.generatePolicyPackageConfig(); policyPackageConfig.id = '1'; + const policyListApiHandlers = policyListApiPathHandlers(); + http.get.mockImplementation((...args) => { const [path] = args; if (typeof path === 'string') { @@ -103,9 +105,9 @@ describe('Policy Details', () => { // Get package data // Used in tests that route back to the list - if (apiPathMockResponseProviders[path]) { + if (policyListApiHandlers[path]) { asyncActions = asyncActions.then(async () => sleep()); - return apiPathMockResponseProviders[path](); + return Promise.resolve(policyListApiHandlers[path]()); } } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx index e35c97698f5cb..97eaceff91e9c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx @@ -5,12 +5,9 @@ */ import React from 'react'; -import * as reactTestingLibrary from '@testing-library/react'; - import { PolicyList } from './index'; -import { mockPolicyResultList } from '../store/policy_list/mock_policy_result_list'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; -import { AppAction } from '../../../../common/store/actions'; +import { setPolicyListApiMockImplementation } from '../store/policy_list/test_mock_utils'; jest.mock('../../../../common/components/link_to'); @@ -18,11 +15,12 @@ jest.mock('../../../../common/components/link_to'); describe.skip('when on the policies page', () => { let render: () => ReturnType; let history: AppContextTestRender['history']; - let store: AppContextTestRender['store']; + let coreStart: AppContextTestRender['coreStart']; + let middlewareSpy: AppContextTestRender['middlewareSpy']; beforeEach(() => { const mockedContext = createAppRootMockRenderer(); - ({ history, store } = mockedContext); + ({ history, coreStart, middlewareSpy } = mockedContext); render = () => mockedContext.render(); }); @@ -46,34 +44,30 @@ describe.skip('when on the policies page', () => { describe('when list data loads', () => { let firstPolicyID: string; - beforeEach(() => { - reactTestingLibrary.act(() => { - history.push('/policy'); - reactTestingLibrary.act(() => { - const policyListData = mockPolicyResultList({ total: 3 }); - firstPolicyID = policyListData.items[0].id; - const action: AppAction = { - type: 'serverReturnedPolicyListData', - payload: { - policyItems: policyListData.items, - total: policyListData.total, - pageSize: policyListData.perPage, - pageIndex: policyListData.page, - }, - }; - store.dispatch(action); - }); - }); + const renderList = async () => { + const renderResult = render(); + history.push('/policy'); + await Promise.all([ + middlewareSpy + .waitForAction('serverReturnedPolicyListData') + .then((action) => (firstPolicyID = action.payload.policyItems[0].id)), + // middlewareSpy.waitForAction('serverReturnedAgentConfigListData'), + ]); + return renderResult; + }; + + beforeEach(async () => { + setPolicyListApiMockImplementation(coreStart.http, 3); }); it('should display rows in the table', async () => { - const renderResult = render(); + const renderResult = await renderList(); const rows = await renderResult.findAllByRole('row'); expect(rows).toHaveLength(4); }); it('should display policy name value as a link', async () => { - const renderResult = render(); + const renderResult = await renderList(); const policyNameLink = (await renderResult.findAllByTestId('policyNameLink'))[0]; expect(policyNameLink).not.toBeNull(); expect(policyNameLink.getAttribute('href')).toContain(`policy/${firstPolicyID}`); From 111e8758907383baea03ccefc647fc7a2c8c6e7f Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 30 Jul 2020 10:53:44 -0700 Subject: [PATCH 07/33] [kbn/optimizer] restore x-pack bundle banner (#73767) Co-authored-by: spalger --- .../mock_repo/x-pack/baz/kibana.json | 4 +++ .../mock_repo/x-pack/baz/public/index.ts | 21 ++++++++++++ .../kbn-optimizer/src/common/bundle.test.ts | 2 ++ packages/kbn-optimizer/src/common/bundle.ts | 14 ++++++++ .../basic_optimization.test.ts.snap | 32 +++++++++++++++++ .../basic_optimization.test.ts | 34 ++++++++++++++----- .../src/optimizer/get_plugin_bundles.test.ts | 23 +++++++++++++ .../src/optimizer/get_plugin_bundles.ts | 6 ++++ .../src/worker/webpack.config.ts | 1 + src/dev/precommit_hook/casing_check_config.js | 1 + 10 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/public/index.ts diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json new file mode 100644 index 0000000000000..10602d2e7981a --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json @@ -0,0 +1,4 @@ +{ + "id": "baz", + "ui": true +} diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/public/index.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/public/index.ts new file mode 100644 index 0000000000000..7313de07be04c --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/public/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// eslint-disable-next-line no-console +console.log('plugin in an x-pack dir'); diff --git a/packages/kbn-optimizer/src/common/bundle.test.ts b/packages/kbn-optimizer/src/common/bundle.test.ts index 6197a08485854..b8f9b94379f20 100644 --- a/packages/kbn-optimizer/src/common/bundle.test.ts +++ b/packages/kbn-optimizer/src/common/bundle.test.ts @@ -48,6 +48,7 @@ it('creates cache keys', () => { "/foo/bar/c": 789, }, "spec": Object { + "banner": undefined, "contextDir": "/foo/bar", "id": "bar", "manifestPath": undefined, @@ -80,6 +81,7 @@ it('parses bundles from JSON specs', () => { expect(bundles).toMatchInlineSnapshot(` Array [ Bundle { + "banner": undefined, "cache": BundleCache { "path": "/foo/bar/target/.kbn-optimizer-cache", "state": undefined, diff --git a/packages/kbn-optimizer/src/common/bundle.ts b/packages/kbn-optimizer/src/common/bundle.ts index a354da7a21521..25b37ace09a8f 100644 --- a/packages/kbn-optimizer/src/common/bundle.ts +++ b/packages/kbn-optimizer/src/common/bundle.ts @@ -43,6 +43,8 @@ export interface BundleSpec { readonly sourceRoot: string; /** Absolute path to the directory where output should be written */ readonly outputDir: string; + /** Banner that should be written to all bundle JS files */ + readonly banner?: string; /** Absolute path to a kibana.json manifest file, if omitted we assume there are not dependenices */ readonly manifestPath?: string; } @@ -64,6 +66,8 @@ export class Bundle { public readonly sourceRoot: BundleSpec['sourceRoot']; /** Absolute path to the output directory for this bundle */ public readonly outputDir: BundleSpec['outputDir']; + /** Banner that should be written to all bundle JS files */ + public readonly banner: BundleSpec['banner']; /** * Absolute path to a manifest file with "requiredBundles" which will be * used to allow bundleRefs from this bundle to the exports of another bundle. @@ -81,6 +85,7 @@ export class Bundle { this.sourceRoot = spec.sourceRoot; this.outputDir = spec.outputDir; this.manifestPath = spec.manifestPath; + this.banner = spec.banner; this.cache = new BundleCache(Path.resolve(this.outputDir, '.kbn-optimizer-cache')); } @@ -112,6 +117,7 @@ export class Bundle { sourceRoot: this.sourceRoot, outputDir: this.outputDir, manifestPath: this.manifestPath, + banner: this.banner, }; } @@ -220,6 +226,13 @@ export function parseBundles(json: string) { } } + const { banner } = spec; + if (banner !== undefined) { + if (!(typeof banner === 'string')) { + throw new Error('`bundles[]` must have a string `banner` property'); + } + } + return new Bundle({ type, id, @@ -227,6 +240,7 @@ export function parseBundles(json: string) { contextDir, sourceRoot, outputDir, + banner, manifestPath, }); } diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 109188e163d06..5f44d8068e694 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -4,6 +4,7 @@ exports[`builds expected bundles, saves bundle counts to metadata: OptimizerConf OptimizerConfig { "bundles": Array [ Bundle { + "banner": undefined, "cache": BundleCache { "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/target/public/.kbn-optimizer-cache, "state": undefined, @@ -19,6 +20,7 @@ OptimizerConfig { "type": "plugin", }, Bundle { + "banner": undefined, "cache": BundleCache { "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/target/public/.kbn-optimizer-cache, "state": undefined, @@ -33,6 +35,24 @@ OptimizerConfig { "sourceRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, "type": "plugin", }, + Bundle { + "banner": "/*! 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. */ +", + "cache": BundleCache { + "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/target/public/.kbn-optimizer-cache, + "state": undefined, + }, + "contextDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz, + "id": "baz", + "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/kibana.json, + "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/target/public, + "publicDirNames": Array [ + "public", + ], + "sourceRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, + "type": "plugin", + }, ], "cache": true, "dist": false, @@ -60,6 +80,13 @@ OptimizerConfig { "isUiPlugin": false, "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/nested/baz/kibana.json, }, + Object { + "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz, + "extraPublicDirs": Array [], + "id": "baz", + "isUiPlugin": true, + "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/kibana.json, + }, ], "profileWebpack": false, "repoRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, @@ -73,6 +100,11 @@ OptimizerConfig { exports[`prepares assets for distribution: bar bundle 1`] = `"(function(modules){var installedModules={};function __webpack_require__(moduleId){if(installedModules[moduleId]){return installedModules[moduleId].exports}var module=installedModules[moduleId]={i:moduleId,l:false,exports:{}};modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);module.l=true;return module.exports}__webpack_require__.m=modules;__webpack_require__.c=installedModules;__webpack_require__.d=function(exports,name,getter){if(!__webpack_require__.o(exports,name)){Object.defineProperty(exports,name,{enumerable:true,get:getter})}};__webpack_require__.r=function(exports){if(typeof Symbol!==\\"undefined\\"&&Symbol.toStringTag){Object.defineProperty(exports,Symbol.toStringTag,{value:\\"Module\\"})}Object.defineProperty(exports,\\"__esModule\\",{value:true})};__webpack_require__.t=function(value,mode){if(mode&1)value=__webpack_require__(value);if(mode&8)return value;if(mode&4&&typeof value===\\"object\\"&&value&&value.__esModule)return value;var ns=Object.create(null);__webpack_require__.r(ns);Object.defineProperty(ns,\\"default\\",{enumerable:true,value:value});if(mode&2&&typeof value!=\\"string\\")for(var key in value)__webpack_require__.d(ns,key,function(key){return value[key]}.bind(null,key));return ns};__webpack_require__.n=function(module){var getter=module&&module.__esModule?function getDefault(){return module[\\"default\\"]}:function getModuleExports(){return module};__webpack_require__.d(getter,\\"a\\",getter);return getter};__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)};__webpack_require__.p=\\"\\";return __webpack_require__(__webpack_require__.s=5)})([function(module,exports,__webpack_require__){\\"use strict\\";var isOldIE=function isOldIE(){var memo;return function memorize(){if(typeof memo===\\"undefined\\"){memo=Boolean(window&&document&&document.all&&!window.atob)}return memo}}();var getTarget=function getTarget(){var memo={};return function memorize(target){if(typeof memo[target]===\\"undefined\\"){var styleTarget=document.querySelector(target);if(window.HTMLIFrameElement&&styleTarget instanceof window.HTMLIFrameElement){try{styleTarget=styleTarget.contentDocument.head}catch(e){styleTarget=null}}memo[target]=styleTarget}return memo[target]}}();var stylesInDom=[];function getIndexByIdentifier(identifier){var result=-1;for(var i=0;i { it('builds expected bundles, saves bundle counts to metadata', async () => { const config = OptimizerConfig.create({ repoRoot: MOCK_REPO_DIR, - pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], + pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins'), Path.resolve(MOCK_REPO_DIR, 'x-pack')], maxWorkerCount: 1, dist: false, }); @@ -100,7 +100,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { (msg.event?.type === 'bundle cached' || msg.event?.type === 'bundle not cached') && msg.state.phase === 'initializing' ); - assert('produce two bundle cache events while initializing', bundleCacheStates.length === 2); + assert('produce three bundle cache events while initializing', bundleCacheStates.length === 3); const initializedStates = msgs.filter((msg) => msg.state.phase === 'initialized'); assert('produce at least one initialized event', initializedStates.length >= 1); @@ -110,17 +110,17 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { const runningStates = msgs.filter((msg) => msg.state.phase === 'running'); assert( - 'produce two or three "running" states', - runningStates.length === 2 || runningStates.length === 3 + 'produce three to five "running" states', + runningStates.length >= 3 && runningStates.length <= 5 ); const bundleNotCachedEvents = msgs.filter((msg) => msg.event?.type === 'bundle not cached'); - assert('produce two "bundle not cached" events', bundleNotCachedEvents.length === 2); + assert('produce three "bundle not cached" events', bundleNotCachedEvents.length === 3); const successStates = msgs.filter((msg) => msg.state.phase === 'success'); assert( - 'produce one or two "compiler success" states', - successStates.length === 1 || successStates.length === 2 + 'produce one to three "compiler success" states', + successStates.length >= 1 && successStates.length <= 3 ); const otherStates = msgs.filter( @@ -175,12 +175,26 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { /packages/kbn-ui-shared-deps/public_path_module_creator.js, ] `); + + const baz = config.bundles.find((b) => b.id === 'baz')!; + expect(baz).toBeTruthy(); + baz.cache.refresh(); + expect(baz.cache.getModuleCount()).toBe(3); + + expect(baz.cache.getReferencedFiles()).toMatchInlineSnapshot(` + Array [ + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/kibana.json, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/public/index.ts, + /packages/kbn-optimizer/target/worker/entry_point_creator.js, + /packages/kbn-ui-shared-deps/public_path_module_creator.js, + ] + `); }); it('uses cache on second run and exist cleanly', async () => { const config = OptimizerConfig.create({ repoRoot: MOCK_REPO_DIR, - pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], + pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins'), Path.resolve(MOCK_REPO_DIR, 'x-pack')], maxWorkerCount: 1, dist: false, }); @@ -202,6 +216,7 @@ it('uses cache on second run and exist cleanly', async () => { "initializing", "initializing", "initializing", + "initializing", "initialized", "success", ] @@ -211,7 +226,7 @@ it('uses cache on second run and exist cleanly', async () => { it('prepares assets for distribution', async () => { const config = OptimizerConfig.create({ repoRoot: MOCK_REPO_DIR, - pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], + pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins'), Path.resolve(MOCK_REPO_DIR, 'x-pack')], maxWorkerCount: 1, dist: true, }); @@ -224,6 +239,7 @@ it('prepares assets for distribution', async () => { 'foo async bundle' ); expectFileMatchesSnapshotWithCompression('plugins/bar/target/public/bar.plugin.js', 'bar bundle'); + expectFileMatchesSnapshotWithCompression('x-pack/baz/target/public/baz.plugin.js', 'baz bundle'); }); /** diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts index a70cfc759dd55..a823f66cf767b 100644 --- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts @@ -48,12 +48,20 @@ it('returns a bundle for core and each plugin', () => { extraPublicDirs: [], manifestPath: '/outside/of/repo/plugins/baz/kibana.json', }, + { + directory: '/repo/x-pack/plugins/box', + id: 'box', + isUiPlugin: true, + extraPublicDirs: [], + manifestPath: '/repo/x-pack/plugins/box/kibana.json', + }, ], '/repo' ).map((b) => b.toSpec()) ).toMatchInlineSnapshot(` Array [ Object { + "banner": undefined, "contextDir": /plugins/foo, "id": "foo", "manifestPath": /plugins/foo/kibana.json, @@ -65,6 +73,7 @@ it('returns a bundle for core and each plugin', () => { "type": "plugin", }, Object { + "banner": undefined, "contextDir": "/outside/of/repo/plugins/baz", "id": "baz", "manifestPath": "/outside/of/repo/plugins/baz/kibana.json", @@ -75,6 +84,20 @@ it('returns a bundle for core and each plugin', () => { "sourceRoot": , "type": "plugin", }, + Object { + "banner": "/*! 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. */ + ", + "contextDir": /x-pack/plugins/box, + "id": "box", + "manifestPath": /x-pack/plugins/box/kibana.json, + "outputDir": /x-pack/plugins/box/target/public, + "publicDirNames": Array [ + "public", + ], + "sourceRoot": , + "type": "plugin", + }, ] `); }); diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts index 04ab992addeec..9350b9464242a 100644 --- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts @@ -24,6 +24,8 @@ import { Bundle } from '../common'; import { KibanaPlatformPlugin } from './kibana_platform_plugins'; export function getPluginBundles(plugins: KibanaPlatformPlugin[], repoRoot: string) { + const xpackDirSlash = Path.resolve(repoRoot, 'x-pack') + Path.sep; + return plugins .filter((p) => p.isUiPlugin) .map( @@ -36,6 +38,10 @@ export function getPluginBundles(plugins: KibanaPlatformPlugin[], repoRoot: stri contextDir: p.directory, outputDir: Path.resolve(p.directory, 'target/public'), manifestPath: p.manifestPath, + banner: p.directory.startsWith(xpackDirSlash) + ? `/*! Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one or more contributor license agreements.\n` + + ` * Licensed under the Elastic License; you may not use this file except in compliance with the Elastic License. */\n` + : undefined, }) ); } diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 271ad49aee351..3d62ed1636869 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -72,6 +72,7 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: new CleanWebpackPlugin(), new DisallowedSyntaxPlugin(), new BundleRefsPlugin(bundle, bundleRefs), + ...(bundle.banner ? [new webpack.BannerPlugin({ banner: bundle.banner, raw: true })] : []), ], module: { diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 864bf7515053c..404ad67174681 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -105,6 +105,7 @@ export const IGNORE_DIRECTORY_GLOBS = [ 'test/functional/fixtures/es_archiver/visualize_source-filters', 'packages/kbn-pm/src/utils/__fixtures__/*', 'x-pack/dev-tools', + 'packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack', ]; /** From 1e067b1608adc89cf647f47a24bcecde2d3e99fe Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Thu, 30 Jul 2020 14:01:29 -0400 Subject: [PATCH 08/33] [Security Solution][Resolver] Adding resolver backend docs (#73726) * Adding resolver backend docs * Adding more clarity around ancestry array limit Co-authored-by: Elastic Machine --- .../common/endpoint/types.ts | 2 + .../endpoint/routes/resolver/docs/README.md | 216 ++++++++++++++++++ .../resolver/docs/resolver_tree_ancestry.png | Bin 0 -> 19926 bytes .../docs/resolver_tree_children_loop.png | Bin 0 -> 40224 bytes .../resolver_tree_children_pagination.png | Bin 0 -> 32477 bytes ...er_tree_children_pagination_with_after.png | Bin 0 -> 32398 bytes .../docs/resolver_tree_children_simple.png | Bin 0 -> 22889 bytes .../resolver/utils/ancestry_query_handler.ts | 24 +- .../endpoint/routes/resolver/utils/tree.ts | 33 +-- 9 files changed, 242 insertions(+), 33 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/README.md create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_ancestry.png create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_loop.png create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_pagination.png create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_pagination_with_after.png create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_simple.png diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts index a982f9ffe8f21..1c24e1abe5a57 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types.ts @@ -104,6 +104,8 @@ export interface ResolverChildNode extends ResolverLifecycleNode { * * string: Indicates this is a leaf node and it can be used to continue querying for additional descendants * using this node's entity_id + * + * For more information see the resolver docs on pagination [here](../../server/endpoint/routes/resolver/docs/README.md#L129) */ nextChild?: string | null; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/README.md b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/README.md new file mode 100644 index 0000000000000..1c0692db344c4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/README.md @@ -0,0 +1,216 @@ +# Resolver Backend + +This readme will describe the backend implementation for resolver. + +## Ancestry Array + +The ancestry array is an array of entity_ids. This array is included with each event sent by the elastic endpoint +and defines the ancestors of a particular process. The array is formatted such that [0] of the array contains the direct +parent of the process. [1] of the array contains the grandparent of the process. For example if Process A spawned process +B which spawned process C. Process C's array would be [B,A]. + +The presence of the ancestry array makes querying ancestors and children for a process more efficient. + +## Ancestry Array Limit + +The ancestry array is currently limited to 20 values. The exact limit should not be relied on though. + +## Ancestors + +To query for ancestors of a process leveraging the ancestry array, we first retrieve the lifecycle events for the event_id +passed in. Once we have the origin node we can check to see if the document has the `process.Ext.ancestry` array. If +it does we can perform a search for the values in the array. This will retrieve all the ancestors for the process of interest +up to the limit of the ancestry array. Since the array is capped at 20, if the request is asking for more than 20 +ancestors we will have to examine the most distant ancestor that has been retrieved and use its ancestry array to retrieve +the next set of results to fulfill the request. + +### Pagination + +After the backend gathers the results for an ancestry query, it will set a pagination cursor depending on the results from ES. + +If the number of ancestors we have gathered is equal to the size in the request we don't know if ES has more results or not. So we will set `nextAncestor` to the entity_id of the most distant ancestor retrieved. + +If the request asked for 10 and we only found 8 from ES, we know for sure that there aren't anymore results. In this case we will set `nextAncestor` to `null`. + +### Code + +The code for handling the ancestor logic is in [here](../utils/ancestry_query_handler.ts) + +### Ancestors Multiple Queries Example + +![alt text](./resolver_tree_ancestry.png 'Retrieve ancestors') + +For this example let's assume that the _ancestry array limit_ is 2. The process of interest is A (the entity_id of a node is the character in the circle). Process A has an ancestry array of `[3,2]`, its parent has an ancestry array of `[2,1]` etc. Here is the execution of a request for 3 ancestors for entity_id A. + +**Request:** `GET /resolver/A/ancestry?ancestors=3` + +1. Retrieve lifecycle events for entity_id `A` +2. Retrieve `A`'s start event's ancestry array + 1. In the event that the node of interest does not have an ancestry array, the entity id of it's parent will be used, essentially an ancestry array of length 1, [3] in the example here +3. `A`'s ancestry array is `[3,2]`, query for the lifecycle events for processes with `entity_id` 3 or 2 +4. Check to see if we have retrieved enough ancestors to fulfill the request (we have not, we only received 2 nodes of the 3 that were requested) +5. We haven't so use the most distant ancestor in our result set (process 2) +6. Use process 2's ancestry array to query for the next set of results to fulfill the request +7. Process 2's ancestry array is `[1]` so repeat the process in steps 3-4 and retrieve process with entity_id 1. This fulfills the request so we can return the results for the lifecycle events of A, 3, 2, and 1. + +If process 2 had an ancestry array of `[1,0]` we know that we only need 1 more process to fulfill the request so we can truncate the array to `[1]` instead of searching for all the entries in the array. + +More generically: In the event where our request stops at the x (non-final) position in an ancestry array, we won't search all items in the array, just those up to the x position. The next-cursor will be set to the last ancestor received since there might be more data. + +The `nextAncestor` cursor will be set to `1` in this scenario because we retrieved all 3 ancestors from ES but we don't know if ES has anymore. + +## Descendants + +We can also leverage the ancestry array to query for the descendants of a process. The basic query for the descendants of a process is: _find all processes where their ancestry array contains a particular entity_id_. The results of this query will be sorted in ascending order by the timestamp field. I will try to outline a couple different scenarios for retrieving descendants using the ancestry array below. + +### Start events vs all lifecycle events + +There are two parts to querying for descendant process nodes. When a request comes in for 7 process nodes we need to communicate to ES that we want all of the lifecycle nodes for 7 processes. We could use a query that retrieves all lifecycle events (start, end, etc) but the issue with this is that we need to indicate a `size` in our ES query. If we set the `size` to 7, we will only get 7 lifecycle events. These events could be start, end, or already_running events. It doesn't guarantee that we get all of the lifecycle events for 7 process nodes. + +Instead we can first query for 7 start events, which guarantees that we will have 7 unique process descendants and then we can gather all those entity_ids and do another query for all the lifecycle events for those 7 processes. The downside here is that you have to do two queries to retrieve all the lifecycle events. Optimizations can be made for the first query for the start events by reducing the `_source` that ES returns to only include the `entity_id` and `ancestry`. This will reduce the amount of data that ES has to send back and speed up the query. + +### Scenario Background + +In the scenarios below let's assume the _ancestry array limit_ is 2. The times next to the nodes are the time the node was spawned. The value in red indicates that the process terminated at the time in red. + +Let's also ignore the fact that retrieving the lifecycle events for a descendant actually takes two queries. Let's assume that it's taken care of, and when we say "query for lifecycle events" we get all the lifecycle events back for the descendants using the algorithm described in the [previous section](#start-events-vs-all-lifecycle-events) + +### Simple Scenario + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> E -> F -> G -> H. + +![alt text](./resolver_tree_children_simple.png 'Descendants Simple Scenario') + +**Request:** `GET /resolver/A/children?children=6` + +For this scenario we will retrieve all the lifecycle events for 6 descendants of the process with entity_id `A`. As shown in the diagram above ES has 6 descendants for A so the response to this request will be: `[B, C, E, F, G, H]` because the results are sorted in ascending ordering based on the `timestamp` field which is when the process was started. + +### Looping Scenario + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> E -> F -> G -> J -> K -> H. + +![alt text](./resolver_tree_children_loop.png 'Descendants Looping Scenario') + +**Request:** `GET /resolver/A/children?children=9` + +In this scenario the request is for more descendants than can be retrieved using a single querying with the entity_id `A`. This is because the ancestry array for the descendants in the red section do not have `A` in their ancestry array. So when we query for all process nodes that have `A` in their ancestry array we won't receive D, J, or K. + +Like in the previous scenario, for the first query we will receive `[B, C, E, F, G, H]`. What we want to do next is use a subset of that response that will get us 3 more descendants to fulfill the request for a total of 9. + +We _could_ use `B` and `G` to do this (mostly `B`) but the problem is that when we query for descendants that have `B` or `G` in their ancestry array we will get back some duplicates that we have already received before. For example if we use `B` and `G` we'd get `[C, D, E, F, J, K, H]` but this isn't efficient because we have already received `[E, F, G, H]` from the previous query. + +What we want to do is use the most distant descendants from `A` to make the next query to retrieve the last 3 process nodes to fulfill the request. Those would be `[C, E, F, H]`. So our next query will be: _find all process nodes where their ancestry array contains C or E or F or H_. This query can be limited to a size of 3 so that we will only receive `[D, J, K]`. + +We have now received all the nodes for the request and we can return the results as `[B, C, E, F, G, H, D, J, K]`. + +### Important Caveats + +#### Ordering + +In the previous example the final results are not sorted based on timestamp in ascending order. This is because we had to perform multiple queries to retrieve all the results. The backend will not return the results in sorted order. + +#### Tie breaks on timestamp + +In the previous example we saw that J and K had the same timestamp of `12:13 pm`. The reason they were returned in the order `[J, K]` is because the `event.id` field is used to break ties like this. The `event.id` field is unique for a particular event and an increasing value per ECS's guidelines. Therefore J comes before K because it has an `event.id` of 1 vs 2. + +#### Finding the most distant descendants + +In the previous scenario we saw that we needed to use the most distant descendants from a particular node. To determine if a node is a most distant descendant we can use the ancestry array. Nodes C, E, F, and H all have `A` as their last entry in the ancestry array. This indicates that they are a distant descendant that should be used in the next query. There's one problem with this approach. In a mostly impossible scenario where the node of interest (A) does not have any ancestors, its direct children will also have `A` as the last entry in their ancestry array. + +This edge case will likely never be encountered but we'll try to solve it anyway. To get around this as we iterate over the results from our first query (`[B, C, E, F, G, H]`) we can bucket the ones that have `A` as the last entry in their ancestry array. We bucket the results based on the length of their ancestry array (basically a `Map>`). So after bucketing our results will look like: + +```javascript +{ + 1: [B, G] + 2: [C, E, F, H] +} +``` + +While we are iterating we also keep track of the largest ancestry array that we have seen. In our scenario that will be a size of 2. Then to determine the distant descendants we simply get the nodes that had the largest ancestry array length. In this scenario that'd be `[C, E, F, H]`. + +### Handling Pagination + +#### Pagination Cursor Values + +There are 3 possible states for the pagination cursor for a child node and 2 possible states for the pagination cursor for the node of interest (the node that we are using in the API request). + +Potential cursors for the node of interest: +**a string cursor:** a cursor that can be used to skip the previous set of results. The cursor is a base64 version of a json object with `event.id` and `timestamp` of the last process's start event that was received. +**null:** indicates that no more results can be received using this process's entity_id + +Potential cursors for descendants of the node of interest (these apply to the results of a request): +**a string cursor:** a cursor that can be used to skip the previous set of results. The cursor is a base64 version of a json object with `event.id` and `timestamp` of the last process's start event that was received. This cursor should be used in conjunction with using this process's entity_id for a query. +**undefined:** the node may contain additional children, but we are not aware. To find out, perform additional queries on the node of interest that original returned these results or move down the tree to a descendant of this node to query for more descendants. +**null:** We have found all possible direct children for this node. There may be more descendants but not direct children for this node. + +#### Pagination Examples + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> G -> F -> H -> E -> J -> K. + +![alt text](./resolver_tree_children_pagination.png 'Descendants Pagination Scenario') + +Handling pagination for the children API in a little tricky. Let's consider this scenario: + +**Request:** `GET /resolver/A/children?children=3` + +Let's use the diagram above to show the relationship between processes and the data in ES. The response for the request for 3 children is `[B, C, G]`. More process nodes exist in ES so it would be helpful to indicate in the response that there is more data and a way to skip `[B, C, G]` to get the next set of data. A cursor can be set on the response for `A` to point to the last process node in the response which can be sent in another request to retrieve the next set of data. This cursor will contain information from `G` because it has the latest timestamp (in ascending order). + +If another request was made using the returned cursor like the following: + +**Request:** `GET /resolver/A/children?children=5&after=` + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> G -> F -> H -> E -> J -> K (the same as above). + +![alt text](./resolver_tree_children_pagination_with_after.png 'Descendants Pagination Scenario Part 2') + +For this request we will do a query for _find all process nodes where their ancestry array has entity_id `A` and use the cursor to skip old results_. The response for this request is `[F, H, E, K]`. The request actually asked for 5 nodes but there was only 4 in ES so only 4 were returned. + +The odd thing about this response is that it did not receive D and J. The problem is that the backend does not have any concept of C in this second request because it was received in the previous one. It will be skipped based on the pagination cursor returned previously. + +This example highlights a scenario where it is not easy for the backend to go back and continue to get the descendants for C because of the limitation of the ancestry array. + +#### Pagination cursor for descendant nodes + +Let's go back to the first request where we got `[B, C, G]`. How could we go about getting the rest of the children for `B`? We have two ways of solving this. First we could determine what the last descendant we had received of `B` and use that as the cursor when returning all the results for this request. That is actually kind of difficult. + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> G -> F -> H -> E -> J -> K (the same as above). + +![alt text](./resolver_tree_children_pagination.png 'Descendants Pagination Scenario') + +Let's imagine for a moment that the _ancestry array limit_ is 3 instead of 2. Taking our previous request we would instead get `[B, C, D]` because D started before G. In this case the last descendant for `B` is actually `D` and not `C`. This gets complicated because we'd have to keep track of which descendant was the last (time wise) one for each intermediate process node. In this example we'd need to find the last descendant for both `B` and `C`. We'd have to track the descendants for each process node and build a map to quickly be able to retrieve the last descendant. + +Instead of doing that we could also continue to get the immediate children of `B` by doing another request for `A` like was shown in previous example when using the after cursor. This would guarantee that all children (first level descendants of a node) had been retrieved. + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> G -> F -> H -> E -> J -> K (the same as above). + +![alt text](./resolver_tree_children_pagination_with_after.png 'Descendants Pagination Scenario Part 2') + +To get to the response shown in the diagram above (the blue nodes is the response) a request was made for 3 nodes which returned `[B, C, D]` and then another request was made using the returned cursor for `A` to get an additional 4 nodes `[F, H, E, K]`. Let's assume that the request looked like: + +**Request:** `GET /resolver/A/children?children=5&after=` + +So 5 nodes were actually asked for. After `[F, H, E]` are returned during the first query to ES, we will use the most distant children (also `[F, H, E]`) and make another request for any nodes that have F or H or E in their ancestry array. Only a single node satisfies that query which returns `K`. Therefore we can know with certainty that E, F, and H have no more children because ES only returned K instead of K and one more node (since we requested a total of 5). With this knowledge we can mark A, E, F, H's pagination cursors in a way to communicate that they have no more descendants. + +The way the backend communicates this is by marking the cursor as null. + +If the request was actually for only 4 children like: + +**Request:** `GET /resolver/A/children?children=4&after=` + +Then we wouldn't know for sure whether ES had more results than K. But what we can know is that `A` does not have any more descendants that we can retrieve in a single query using its ancestry array. We have received all nodes where `A` is in their ancestry array when we made the second query for nodes E, F, and H and received `K`. Therefore at the moment when we received `[F, H, E]` we can mark `A`'s cursor as null. + +When we make the next query for any nodes that have F or H or E in their ancestry array and get back K, this satisfies our size of only needing one more node (4 total). At this point we don't know for sure if E, F, or H have more descendants. Since we don't know we will mark E, F, and H's cursors to point to K. + +#### Undefined Pagination + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> E -> F -> G -> J -> K -> H. (Not the same as above). + +For this scenario let's assume ES has the data in the diagram below. Let's say the request looks like: + +**Request:** `GET /resolver/A/children?children=6` + +![alt text](./resolver_tree_children_loop.png 'Descendants Looping Scenario') + +The result for this request will be `[B, C, E, F, G, H]`. Since the request was looking for 6 nodes and we got that amount the cursor for `A` will be set to the last one: `H`. The cursors for the intermediate nodes `B` and `G` will be undefined. This is because we don't know if `B` or `G` have more children but `A` can be used to determine that. We also don't know if C, E, F, or H have more descendants so their cursor will also be marked as undefined. If we wanted to know if C had more descendants we can simply issue a new request like `GET /resolver/C/children` to get its descendants and we won't receive and duplicates because we never received D, J or K. + +If we want to know if `B` has more children we can issue another request using the cursor set for `A`. diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_ancestry.png b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_ancestry.png new file mode 100644 index 0000000000000000000000000000000000000000..a2636c7cd38cb515849ede721f4e4d24a851241a GIT binary patch literal 19926 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
99kI%2SH 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

1~>f(gVdq^NZINKai89#6pt6sG< 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 literal 0 HcmV?d00001 diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_simple.png b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_simple.png new file mode 100644 index 0000000000000000000000000000000000000000..f05963c0f0b677ab0882628de635bff7714a85c1 GIT binary patch literal 22889 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%0Dcg{&6(YS?=)aT0-r6RgHFfT*uf(W7+h$d9Q`N+ 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 { // bucket the start and end events together for a single node const ancestryNodes = this.toMapOfNodes(results); - // the order of this array is going to be weird, it will look like this - // [furthest grandparent...closer grandparent, next recursive call furthest grandparent...closer grandparent] + /** + * This array (this.ancestry.ancestors) is the accumulated ancestors of the node of interest. This array is different + * from the ancestry array of a specific document. The order of this array is going to be weird, it will look like this + * [most distant ancestor...closer ancestor, next recursive call most distant ancestor...closer ancestor] + * + * Here is an example of why this happens + * Consider the following tree: + * A -> B -> C -> D -> E -> Origin + * Where A was spawn before B, which was before C, etc + * + * Let's assume the ancestry array limit is 2 so Origin's array would be: [E, D] + * E's ancestry array would be: [D, C] etc + * + * If a request comes in to retrieve all the ancestors in this tree, the accumulate results will be: + * [D, E, B, C, A] + * + * The first iteration would retrieve D and E in that order because they are sorted in ascending order by timestamp. + * The next iteration would get the ancestors of D (since that's the most distant ancestor from Origin) which are + * [B, C] + * The next iteration would get the ancestors of B which is A + * Hence: [D, E, B, C, A] + */ this.ancestry.ancestors.push(...ancestryNodes.values()); this.ancestry.nextAncestor = parentEntityId(results[0]) || null; this.levels = this.levels - ancestryNodes.size; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts index 9e47f4eb94485..3f941851a4143 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts @@ -30,38 +30,9 @@ export interface Options { } /** - * This class aids in constructing a tree of process events. It works in the following way: + * This class aids in constructing a tree of process events. * - * 1. We construct a tree structure starting with the root node for the event we're requesting. - * 2. We leverage the ability to pass hashes and arrays by reference to construct a fast cache of - * process identifiers that updates the tree structure as we push values into the cache. - * - * When we query a single level of results for child process events we have a flattened, sorted result - * list that we need to add into a constructed tree. We also need to signal in an API response whether - * or not there are more child processes events that we have not yet retrieved, and, if so, for what parent - * process. So, at the end of our tree construction we have a relational layout of the events with no - * pagination information for the given parent nodes. In order to actually construct both the tree and - * insert the pagination information we basically do the following: - * - * 1. Using a terms aggregation query, we return an approximate roll-up of the number of child process - * "creation" events, this gives us an estimation of the number of associated children per parent - * 2. We feed these child process creation event "unique identifiers" (basically a process.entity_id) - * into a second query to get the current state of the process via its "lifecycle" events. - * 3. We construct the tree above with the "lifecycle" events. - * 4. Using the terms query results, we mark each non-leaf node with the number of expected children, if our - * tree has less children than expected, we create a pagination cursor to indicate "we have a truncated set - * of values". - * 5. We mark each leaf node (the last level of the tree we're constructing) with a "null" for the expected - * number of children to indicate "we have not yet attempted to get any children". - * - * Following this scheme, we use exactly 2 queries per level of children that we return--one for the pagination - * and one for the lifecycle events of the processes. The downside to this is that we need to dynamically expand - * the number of documents we can retrieve per level due to the exponential fanout of child processes, - * what this means is that noisy neighbors for a given level may hide other child process events that occur later - * temporally in the same level--so, while a heavily forking process might get shown, maybe the actually malicious - * event doesn't show up in the tree at the beginning. - * - * This Tree's root/origin could be in the middle of the tree. The origin corresponds to the id passed in when this + * This Tree's root/origin will likely be in the middle of the tree. The origin corresponds to the id passed in when this * Tree object is constructed. The tree can have ancestors and children coming from the origin. */ export class Tree { From 827e91c4474e8790374417d30b433296f909b110 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 30 Jul 2020 11:16:49 -0700 Subject: [PATCH 09/33] move and unify postcss config into `@kbn/optimizer` (#73633) Co-authored-by: spalger --- .eslintrc.js | 1 + package.json | 2 +- packages/kbn-optimizer/package.json | 1 + .../{src/worker => }/postcss.config.js | 0 .../basic_optimization.test.ts | 2 +- .../src/worker/webpack.config.ts | 2 +- .../kbn-storybook/lib/webpack.dll.config.js | 2 +- .../storybook_config/webpack.config.js | 2 +- packages/kbn-ui-framework/Gruntfile.js | 2 +- .../doc_site/postcss.config.js | 22 ------------------- packages/kbn-ui-framework/package.json | 4 ++-- packages/kbn-ui-shared-deps/package.json | 1 + src/optimize/base_optimizer.js | 2 +- x-pack/package.json | 7 +++++- .../shareable_runtime/postcss.config.js | 1 - .../shareable_runtime/webpack.config.js | 2 +- .../canvas/storybook/webpack.config.js | 8 +++++-- .../canvas/storybook/webpack.dll.config.js | 2 +- 18 files changed, 26 insertions(+), 37 deletions(-) rename packages/kbn-optimizer/{src/worker => }/postcss.config.js (100%) delete mode 100644 packages/kbn-ui-framework/doc_site/postcss.config.js diff --git a/.eslintrc.js b/.eslintrc.js index c9f9d96f9ddae..b3d29c9866411 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -529,6 +529,7 @@ module.exports = { 'x-pack/test_utils/**/*', 'x-pack/gulpfile.js', 'x-pack/plugins/apm/public/utils/testHelpers.js', + 'x-pack/plugins/canvas/shareable_runtime/postcss.config.js', ], rules: { 'import/no-extraneous-dependencies': [ diff --git a/package.json b/package.json index 51a41cbbab9ff..880534997cff0 100644 --- a/package.json +++ b/package.json @@ -480,7 +480,7 @@ "pixelmatch": "^5.1.0", "pkg-up": "^2.0.0", "pngjs": "^3.4.0", - "postcss": "^7.0.26", + "postcss": "^7.0.32", "postcss-url": "^8.0.0", "prettier": "^2.0.5", "proxyquire": "1.8.0", diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index c11bd1b646933..4fbbc920c4447 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -36,6 +36,7 @@ "loader-utils": "^1.2.3", "node-sass": "^4.13.0", "normalize-path": "^3.0.0", + "postcss": "^7.0.32", "postcss-loader": "^3.0.0", "raw-loader": "^3.1.0", "resolve-url-loader": "^3.1.1", diff --git a/packages/kbn-optimizer/src/worker/postcss.config.js b/packages/kbn-optimizer/postcss.config.js similarity index 100% rename from packages/kbn-optimizer/src/worker/postcss.config.js rename to packages/kbn-optimizer/postcss.config.js diff --git a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts index 2d0d60da1e4a0..bab47d4a1e412 100644 --- a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts +++ b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts @@ -161,6 +161,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { Array [ /node_modules/css-loader/package.json, /node_modules/style-loader/package.json, + /packages/kbn-optimizer/postcss.config.js, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/kibana.json, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.ts, @@ -171,7 +172,6 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/legacy/ui/public/styles/_globals_v7dark.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/legacy/ui/public/styles/_globals_v7light.scss, /packages/kbn-optimizer/target/worker/entry_point_creator.js, - /packages/kbn-optimizer/target/worker/postcss.config.js, /packages/kbn-ui-shared-deps/public_path_module_creator.js, ] `); diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 3d62ed1636869..ae5d2b5fb3292 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -152,7 +152,7 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: options: { sourceMap: !worker.dist, config: { - path: require.resolve('./postcss.config'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, diff --git a/packages/kbn-storybook/lib/webpack.dll.config.js b/packages/kbn-storybook/lib/webpack.dll.config.js index 740ee3819c36f..661312b9a0581 100644 --- a/packages/kbn-storybook/lib/webpack.dll.config.js +++ b/packages/kbn-storybook/lib/webpack.dll.config.js @@ -127,7 +127,7 @@ module.exports = { loader: 'postcss-loader', options: { config: { - path: path.resolve(REPO_ROOT, 'src/optimize/postcss.config.js'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, diff --git a/packages/kbn-storybook/storybook_config/webpack.config.js b/packages/kbn-storybook/storybook_config/webpack.config.js index b2df4f40d4fbe..0a9977463aee8 100644 --- a/packages/kbn-storybook/storybook_config/webpack.config.js +++ b/packages/kbn-storybook/storybook_config/webpack.config.js @@ -91,7 +91,7 @@ module.exports = async ({ config }) => { loader: 'postcss-loader', options: { config: { - path: resolve(REPO_ROOT, 'src/optimize/'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, diff --git a/packages/kbn-ui-framework/Gruntfile.js b/packages/kbn-ui-framework/Gruntfile.js index b7ba1e87b2f00..bb8e7b72cb7bd 100644 --- a/packages/kbn-ui-framework/Gruntfile.js +++ b/packages/kbn-ui-framework/Gruntfile.js @@ -19,7 +19,7 @@ const sass = require('node-sass'); const postcss = require('postcss'); -const postcssConfig = require('../../src/optimize/postcss.config'); +const postcssConfig = require('@kbn/optimizer/postcss.config.js'); const chokidar = require('chokidar'); const { debounce } = require('lodash'); diff --git a/packages/kbn-ui-framework/doc_site/postcss.config.js b/packages/kbn-ui-framework/doc_site/postcss.config.js deleted file mode 100644 index 571bae86dee37..0000000000000 --- a/packages/kbn-ui-framework/doc_site/postcss.config.js +++ /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. - */ - -module.exports = { - plugins: [require('autoprefixer')()], -}; diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index abf64906e0253..7933ce06d6847 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -33,7 +33,7 @@ "@babel/core": "^7.10.2", "@elastic/eui": "0.0.55", "@kbn/babel-preset": "1.0.0", - "autoprefixer": "^9.7.4", + "@kbn/optimizer": "1.0.0", "babel-loader": "^8.0.6", "brace": "0.11.1", "chalk": "^2.4.2", @@ -54,7 +54,7 @@ "keymirror": "0.1.1", "moment": "^2.24.0", "node-sass": "^4.13.1", - "postcss": "^7.0.26", + "postcss": "^7.0.32", "postcss-loader": "^3.0.0", "raw-loader": "^3.1.0", "react-dom": "^16.12.0", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 8398d1c081da6..3c03a52383f77 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -21,6 +21,7 @@ "custom-event-polyfill": "^0.3.0", "elasticsearch-browser": "^16.7.0", "jquery": "^3.5.0", + "mini-css-extract-plugin": "0.8.0", "moment": "^2.24.0", "moment-timezone": "^0.5.27", "react": "^16.12.0", diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index 41628a2264193..74973887ae9c1 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -34,7 +34,7 @@ import { IS_KIBANA_DISTRIBUTABLE } from '../legacy/utils'; import { fromRoot } from '../core/server/utils'; import { PUBLIC_PATH_PLACEHOLDER } from './public_path_placeholder'; -const POSTCSS_CONFIG_PATH = require.resolve('./postcss.config'); +const POSTCSS_CONFIG_PATH = require.resolve('./postcss.config.js'); const BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset'); const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset'); const EMPTY_MODULE_PATH = require.resolve('./intentionally_empty_module.js'); diff --git a/x-pack/package.json b/x-pack/package.json index 3a9b3ca606de6..2d7cb148c43b0 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -121,8 +121,10 @@ "@types/pretty-ms": "^5.0.0", "@welldone-software/why-did-you-render": "^4.0.0", "abab": "^1.0.4", + "autoprefixer": "^9.7.4", "axios": "^0.19.0", "babel-jest": "^25.5.1", + "babel-loader": "^8.0.6", "babel-plugin-require-context-hook": "npm:babel-plugin-require-context-hook-babel7@1.0.0", "base64-js": "^1.3.1", "base64url": "^3.0.1", @@ -159,6 +161,7 @@ "loader-utils": "^1.2.3", "madge": "3.4.4", "marge": "^1.0.1", + "mini-css-extract-plugin": "0.8.0", "mocha": "^7.1.1", "mocha-junit-reporter": "^1.23.1", "mochawesome": "^4.1.0", @@ -168,6 +171,9 @@ "node-fetch": "^2.6.0", "null-loader": "^3.0.0", "pixelmatch": "^5.1.0", + "postcss": "^7.0.32", + "postcss-loader": "^3.0.0", + "postcss-prefix-selector": "^1.7.2", "proxyquire": "1.8.0", "react-docgen-typescript-loader": "^3.1.1", "react-is": "^16.8.0", @@ -308,7 +314,6 @@ "pluralize": "3.1.0", "pngjs": "3.4.0", "polished": "^1.9.2", - "postcss-prefix-selector": "^1.7.2", "prop-types": "^15.6.0", "proper-lockfile": "^3.2.0", "puid": "1.0.7", diff --git a/x-pack/plugins/canvas/shareable_runtime/postcss.config.js b/x-pack/plugins/canvas/shareable_runtime/postcss.config.js index 10baaddfc9b05..e1db6e4a64f71 100644 --- a/x-pack/plugins/canvas/shareable_runtime/postcss.config.js +++ b/x-pack/plugins/canvas/shareable_runtime/postcss.config.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line const autoprefixer = require('autoprefixer'); const prefixer = require('postcss-prefix-selector'); diff --git a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js index 93dc3dbccd549..43e422a161569 100644 --- a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js +++ b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js @@ -111,7 +111,7 @@ module.exports = { loader: 'postcss-loader', options: { config: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, diff --git a/x-pack/plugins/canvas/storybook/webpack.config.js b/x-pack/plugins/canvas/storybook/webpack.config.js index 927f71b832ba0..982185a731b14 100644 --- a/x-pack/plugins/canvas/storybook/webpack.config.js +++ b/x-pack/plugins/canvas/storybook/webpack.config.js @@ -77,7 +77,9 @@ module.exports = async ({ config }) => { { loader: 'postcss-loader', options: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + config: { + path: require.resolve('@kbn/optimizer/postcss.config.js'), + }, }, }, { @@ -114,7 +116,9 @@ module.exports = async ({ config }) => { { loader: 'postcss-loader', options: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + config: { + path: require.resolve('@kbn/optimizer/postcss.config.js'), + }, }, }, { diff --git a/x-pack/plugins/canvas/storybook/webpack.dll.config.js b/x-pack/plugins/canvas/storybook/webpack.dll.config.js index 0e9371e4cb5e4..81d19c035075f 100644 --- a/x-pack/plugins/canvas/storybook/webpack.dll.config.js +++ b/x-pack/plugins/canvas/storybook/webpack.dll.config.js @@ -114,7 +114,7 @@ module.exports = { loader: 'postcss-loader', options: { config: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, From 70d4eac30c381802d87a5112825699ae6cd8089f Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Thu, 30 Jul 2020 14:43:33 -0400 Subject: [PATCH 10/33] [Security Solution] Adding tests for endpoint package pipelines (#73703) * Adding tests for endpoint package pipelines * Removing content type check on types that can change based on docker image version * Skipping ingest tests instead of remove expect * Switching ingest tests over to use application/json * Removing country names Co-authored-by: Elastic Machine --- .../apis/epm/file.ts | 6 +- .../ingest_manager_api_integration/config.ts | 4 +- .../apis/fixtures/package_registry_config.yml | 2 + .../apis/index.ts | 1 + .../apis/package.ts | 140 ++++++++++++++++++ .../apis/resolver/entity_id.ts | 20 +-- .../services/resolver.ts | 25 +++- 7 files changed, 168 insertions(+), 30 deletions(-) create mode 100644 x-pack/test/security_solution_endpoint_api_int/apis/package.ts diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/file.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/file.ts index 733b8d4fd9bd6..3f99f91394d2c 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/file.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/file.ts @@ -47,7 +47,7 @@ export default function ({ getService }: FtrProviderContext) { '/api/ingest_manager/epm/packages/filetest/0.1.0/kibana/visualization/sample_visualization.json' ) .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('Content-Type', 'application/json; charset=utf-8') .expect(200); } else { warnAndSkipTest(this, log); @@ -61,7 +61,7 @@ export default function ({ getService }: FtrProviderContext) { '/api/ingest_manager/epm/packages/filetest/0.1.0/kibana/dashboard/sample_dashboard.json' ) .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('Content-Type', 'application/json; charset=utf-8') .expect(200); } else { warnAndSkipTest(this, log); @@ -73,7 +73,7 @@ export default function ({ getService }: FtrProviderContext) { await supertest .get('/api/ingest_manager/epm/packages/filetest/0.1.0/kibana/search/sample_search.json') .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('Content-Type', 'application/json; charset=utf-8') .expect(200); } else { warnAndSkipTest(this, log); diff --git a/x-pack/test/ingest_manager_api_integration/config.ts b/x-pack/test/ingest_manager_api_integration/config.ts index ddb49a09a7afa..85d1c20c7f155 100644 --- a/x-pack/test/ingest_manager_api_integration/config.ts +++ b/x-pack/test/ingest_manager_api_integration/config.ts @@ -10,9 +10,9 @@ import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { defineDockerServersConfig } from '@kbn/test'; // Docker image to use for Ingest Manager API integration tests. -// This hash comes from the commit hash here: https://github.com/elastic/package-storage/commit/48f3935a72b0c5aacc6fec8ef36d559b089a238b +// This hash comes from the commit hash here: https://github.com/elastic/package-storage/commit export const dockerImage = - 'docker.elastic.co/package-registry/distribution:48f3935a72b0c5aacc6fec8ef36d559b089a238b'; + 'docker.elastic.co/package-registry/distribution:80e93ade87f65e18d487b1c407406825915daba8'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/fixtures/package_registry_config.yml b/x-pack/test/security_solution_endpoint_api_int/apis/fixtures/package_registry_config.yml index 4d93386b4d4e1..00e01fe9ea0fc 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/fixtures/package_registry_config.yml +++ b/x-pack/test/security_solution_endpoint_api_int/apis/fixtures/package_registry_config.yml @@ -1,2 +1,4 @@ package_paths: - /packages/production + - /packages/staging + - /packages/snapshot diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts index 56adc2382e234..b1317c2d9f1c1 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts @@ -31,5 +31,6 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider loadTestFile(require.resolve('./metadata')); loadTestFile(require.resolve('./policy')); loadTestFile(require.resolve('./artifacts')); + loadTestFile(require.resolve('./package')); }); } diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/package.ts b/x-pack/test/security_solution_endpoint_api_int/apis/package.ts new file mode 100644 index 0000000000000..3b5873d1fe0cd --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/apis/package.ts @@ -0,0 +1,140 @@ +/* + * 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 { SearchResponse } from 'elasticsearch'; +import { eventsIndexPattern } from '../../../plugins/security_solution/common/endpoint/constants'; +import { + EndpointDocGenerator, + Event, +} from '../../../plugins/security_solution/common/endpoint/generate_data'; +import { FtrProviderContext } from '../ftr_provider_context'; +import { InsertedEvents, processEventsIndex } from '../services/resolver'; + +interface EventIngested { + event: { + ingested: number; + }; +} + +interface NetworkEvent { + source: { + geo?: { + country_name: string; + }; + }; + destination: { + geo?: { + country_name: string; + }; + }; +} + +const networkIndex = 'logs-endpoint.events.network-default'; + +export default function ({ getService }: FtrProviderContext) { + const resolver = getService('resolverGenerator'); + const es = getService('es'); + const generator = new EndpointDocGenerator('data'); + + const searchForID = async (id: string) => { + return es.search>({ + index: eventsIndexPattern, + body: { + query: { + bool: { + filter: [ + { + ids: { + values: id, + }, + }, + ], + }, + }, + }, + }); + }; + + describe('Endpoint package', () => { + describe('ingested processor', () => { + let event: Event; + let genData: InsertedEvents; + + before(async () => { + event = generator.generateEvent(); + genData = await resolver.insertEvents([event]); + }); + + after(async () => { + await resolver.deleteData(genData); + }); + + it('sets the event.ingested field', async () => { + const resp = await searchForID(genData.eventsInfo[0]._id); + expect(resp.body.hits.hits[0]._source.event.ingested).to.not.be(undefined); + }); + }); + + describe('geoip processor', () => { + let processIndexData: InsertedEvents; + let networkIndexData: InsertedEvents; + + before(async () => { + // 46.239.193.5 should be in Iceland + // 8.8.8.8 should be in the US + const eventWithBothIPs = generator.generateEvent({ + extensions: { source: { ip: '8.8.8.8' }, destination: { ip: '46.239.193.5' } }, + }); + + const eventWithSourceOnly = generator.generateEvent({ + extensions: { source: { ip: '8.8.8.8' } }, + }); + networkIndexData = await resolver.insertEvents( + [eventWithBothIPs, eventWithSourceOnly], + networkIndex + ); + + processIndexData = await resolver.insertEvents([eventWithBothIPs], processEventsIndex); + }); + + after(async () => { + await resolver.deleteData(networkIndexData); + await resolver.deleteData(processIndexData); + }); + + it('sets the geoip fields', async () => { + const eventWithBothIPs = await searchForID( + networkIndexData.eventsInfo[0]._id + ); + // Should be 'United States' + expect(eventWithBothIPs.body.hits.hits[0]._source.source.geo?.country_name).to.not.be( + undefined + ); + // should be 'Iceland' + expect(eventWithBothIPs.body.hits.hits[0]._source.destination.geo?.country_name).to.not.be( + undefined + ); + + const eventWithSourceOnly = await searchForID( + networkIndexData.eventsInfo[1]._id + ); + // Should be 'United States' + expect(eventWithBothIPs.body.hits.hits[0]._source.source.geo?.country_name).to.not.be( + undefined + ); + expect(eventWithSourceOnly.body.hits.hits[0]._source.destination?.geo).to.be(undefined); + }); + + it('does not set geoip fields for events in indices other than the network index', async () => { + const eventWithBothIPs = await searchForID( + processIndexData.eventsInfo[0]._id + ); + expect(eventWithBothIPs.body.hits.hits[0]._source.source.geo).to.be(undefined); + expect(eventWithBothIPs.body.hits.hits[0]._source.destination.geo).to.be(undefined); + }); + }); + }); +} diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts index 4f2a801377204..231871fae3d39 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; -import { SearchResponse } from 'elasticsearch'; import { eventsIndexPattern } from '../../../../plugins/security_solution/common/endpoint/constants'; import { ResolverTree, @@ -20,7 +19,6 @@ import { InsertedEvents } from '../../services/resolver'; export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const resolver = getService('resolverGenerator'); - const es = getService('es'); const generator = new EndpointDocGenerator('resolver'); describe('Resolver handling of entity ids', () => { @@ -38,26 +36,10 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC }); it('excludes events that have an empty entity_id field', async () => { - // first lets get the _id of the document using the parent.process.entity_id - // then we'll use the API to search for that specific document - const res = await es.search>({ - index: genData.indices[0], - body: { - query: { - bool: { - filter: [ - { - term: { 'process.parent.entity_id': origin.process.parent!.entity_id }, - }, - ], - }, - }, - }, - }); const { body }: { body: ResolverEntityIndex } = await supertest.get( // using the same indices value here twice to force the query parameter to be an array // for some reason using supertest's query() function doesn't construct a parsable array - `/api/endpoint/resolver/entity?_id=${res.body.hits.hits[0]._id}&indices=${eventsIndexPattern}&indices=${eventsIndexPattern}` + `/api/endpoint/resolver/entity?_id=${genData.eventsInfo[0]._id}&indices=${eventsIndexPattern}&indices=${eventsIndexPattern}` ); expect(body).to.be.empty(); }); diff --git a/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts b/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts index 335689b804d5b..7e4d4177affac 100644 --- a/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts +++ b/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts @@ -11,7 +11,7 @@ import { } from '../../../plugins/security_solution/common/endpoint/generate_data'; import { FtrProviderContext } from '../ftr_provider_context'; -const processIndex = 'logs-endpoint.events.process-default'; +export const processEventsIndex = 'logs-endpoint.events.process-default'; /** * Options for build a resolver tree @@ -36,7 +36,7 @@ export interface GeneratedTrees { * Structure containing the events inserted into ES and the index they live in */ export interface InsertedEvents { - events: Event[]; + eventsInfo: Array<{ _id: string; event: Event }>; indices: string[]; } @@ -46,24 +46,37 @@ interface BulkCreateHeader { }; } +interface BulkResponse { + items: Array<{ + create: { + _id: string; + }; + }>; +} + export function ResolverGeneratorProvider({ getService }: FtrProviderContext) { const client = getService('es'); return { async insertEvents( events: Event[], - eventsIndex: string = processIndex + eventsIndex: string = processEventsIndex ): Promise { const body = events.reduce((array: Array, doc) => { array.push({ create: { _index: eventsIndex } }, doc); return array; }, []); - await client.bulk({ body, refresh: true }); - return { events, indices: [eventsIndex] }; + const bulkResp = await client.bulk({ body, refresh: true }); + + const eventsInfo = events.map((event: Event, i: number) => { + return { event, _id: bulkResp.body.items[i].create._id }; + }); + + return { eventsInfo, indices: [eventsIndex] }; }, async createTrees( options: Options, - eventsIndex: string = processIndex, + eventsIndex: string = processEventsIndex, alertsIndex: string = 'logs-endpoint.alerts-default' ): Promise { const seed = options.seed || 'resolver-seed'; From c21474b4ce029b820072b53f3908075404bccbde Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Thu, 30 Jul 2020 14:51:35 -0400 Subject: [PATCH 11/33] [Security Solution][Detections] Change from sha1 to sha256 (#73741) --- .../exceptions/add_exception_modal/index.tsx | 3 +- .../exceptions/edit_exception_modal/index.tsx | 3 +- .../exceptions/exceptionable_fields.json | 26 +++-------- .../components/exceptions/helpers.test.tsx | 45 +++++++++++++++++++ .../common/components/exceptions/helpers.tsx | 36 +++++++++++++-- .../alerts_table/default_config.tsx | 2 +- 6 files changed, 88 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index bb547f05090b7..e6eaa4947e404 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -40,6 +40,7 @@ import { AddExceptionComments } from '../add_exception_comments'; import { enrichNewExceptionItemsWithComments, enrichExceptionItemsWithOS, + lowercaseHashValues, defaultEndpointExceptionItems, entryHasListType, entryHasNonEcsType, @@ -256,7 +257,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ : exceptionItemsToAdd; if (exceptionListType === 'endpoint') { const osTypes = retrieveAlertOsTypes(); - enriched = enrichExceptionItemsWithOS(enriched, osTypes); + enriched = lowercaseHashValues(enrichExceptionItemsWithOS(enriched, osTypes)); } return enriched; }, [comment, exceptionItemsToAdd, exceptionListType, retrieveAlertOsTypes]); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 341d2f2bab37a..6109b85f2da5a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -40,6 +40,7 @@ import { getOperatingSystems, entryHasListType, entryHasNonEcsType, + lowercaseHashValues, } from '../helpers'; import { Loader } from '../../loader'; @@ -195,7 +196,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({ ]; if (exceptionListType === 'endpoint') { const osTypes = exceptionItem._tags ? getOperatingSystems(exceptionItem._tags) : []; - enriched = enrichExceptionItemsWithOS(enriched, osTypes); + enriched = lowercaseHashValues(enrichExceptionItemsWithOS(enriched, osTypes)); } return enriched; }, [exceptionItemsToAdd, exceptionItem, comment, exceptionListType]); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json index fdf0ea60ecf6a..037e340ee7fa2 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json @@ -6,32 +6,25 @@ "Target.process.Ext.code_signature.valid", "Target.process.Ext.services", "Target.process.Ext.user", - "Target.process.command_line", "Target.process.command_line.text", - "Target.process.executable", "Target.process.executable.text", "Target.process.hash.md5", "Target.process.hash.sha1", "Target.process.hash.sha256", "Target.process.hash.sha512", - "Target.process.name", "Target.process.name.text", "Target.process.parent.Ext.code_signature.status", "Target.process.parent.Ext.code_signature.subject_name", "Target.process.parent.Ext.code_signature.trusted", "Target.process.parent.Ext.code_signature.valid", - "Target.process.parent.command_line", "Target.process.parent.command_line.text", - "Target.process.parent.executable", "Target.process.parent.executable.text", "Target.process.parent.hash.md5", "Target.process.parent.hash.sha1", "Target.process.parent.hash.sha256", "Target.process.parent.hash.sha512", - "Target.process.parent.name", "Target.process.parent.name.text", "Target.process.parent.pgid", - "Target.process.parent.working_directory", "Target.process.parent.working_directory.text", "Target.process.pe.company", "Target.process.pe.description", @@ -39,7 +32,6 @@ "Target.process.pe.original_file_name", "Target.process.pe.product", "Target.process.pgid", - "Target.process.working_directory", "Target.process.working_directory.text", "agent.id", "agent.type", @@ -74,7 +66,6 @@ "file.mode", "file.name", "file.owner", - "file.path", "file.path.text", "file.pe.company", "file.pe.description", @@ -82,7 +73,6 @@ "file.pe.original_file_name", "file.pe.product", "file.size", - "file.target_path", "file.target_path.text", "file.type", "file.uid", @@ -94,10 +84,8 @@ "host.id", "host.os.Ext.variant", "host.os.family", - "host.os.full", "host.os.full.text", "host.os.kernel", - "host.os.name", "host.os.name.text", "host.os.platform", "host.os.version", @@ -108,32 +96,25 @@ "process.Ext.code_signature.valid", "process.Ext.services", "process.Ext.user", - "process.command_line", "process.command_line.text", - "process.executable", "process.executable.text", "process.hash.md5", "process.hash.sha1", "process.hash.sha256", "process.hash.sha512", - "process.name", "process.name.text", "process.parent.Ext.code_signature.status", "process.parent.Ext.code_signature.subject_name", "process.parent.Ext.code_signature.trusted", "process.parent.Ext.code_signature.valid", - "process.parent.command_line", "process.parent.command_line.text", - "process.parent.executable", "process.parent.executable.text", "process.parent.hash.md5", "process.parent.hash.sha1", "process.parent.hash.sha256", "process.parent.hash.sha512", - "process.parent.name", "process.parent.name.text", "process.parent.pgid", - "process.parent.working_directory", "process.parent.working_directory.text", "process.pe.company", "process.pe.description", @@ -141,7 +122,10 @@ "process.pe.original_file_name", "process.pe.product", "process.pgid", - "process.working_directory", "process.working_directory.text", - "rule.uuid" + "rule.uuid", + "user.domain", + "user.email", + "user.hash", + "user.id" ] \ No newline at end of file diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 5cb65ee6db8ff..18b509d16b352 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -24,6 +24,7 @@ import { entryHasListType, entryHasNonEcsType, prepareExceptionItemsForBulkClose, + lowercaseHashValues, } from './helpers'; import { EmptyEntry } from './types'; import { @@ -663,4 +664,48 @@ describe('Exception helpers', () => { expect(result).toEqual(expected); }); }); + + describe('#lowercaseHashValues', () => { + test('it should return an empty array with an empty array', () => { + const payload: ExceptionListItemSchema[] = []; + const result = lowercaseHashValues(payload); + expect(result).toEqual([]); + }); + + test('it should return all list items with entry hashes lowercased', () => { + const payload = [ + { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.hash', type: 'match', value: 'DDDFFF' }] as EntriesArray, + }, + { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.hash', type: 'match', value: 'aaabbb' }] as EntriesArray, + }, + { + ...getExceptionListItemSchemaMock(), + entries: [ + { field: 'user.hash', type: 'match_any', value: ['aaabbb', 'DDDFFF'] }, + ] as EntriesArray, + }, + ]; + const result = lowercaseHashValues(payload); + expect(result).toEqual([ + { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.hash', type: 'match', value: 'dddfff' }] as EntriesArray, + }, + { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.hash', type: 'match', value: 'aaabbb' }] as EntriesArray, + }, + { + ...getExceptionListItemSchemaMock(), + entries: [ + { field: 'user.hash', type: 'match_any', value: ['aaabbb', 'dddfff'] }, + ] as EntriesArray, + }, + ]); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 3abb788312ff4..2b526ede12acf 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -335,6 +335,36 @@ export const enrichExceptionItemsWithOS = ( }); }; +/** + * Returns given exceptionItems with all hash-related entries lowercased + */ +export const lowercaseHashValues = ( + exceptionItems: Array +): Array => { + return exceptionItems.map((item) => { + const newEntries = item.entries.map((itemEntry) => { + if (itemEntry.field.includes('.hash')) { + if (itemEntry.type === 'match') { + return { + ...itemEntry, + value: itemEntry.value.toLowerCase(), + }; + } else if (itemEntry.type === 'match_any') { + return { + ...itemEntry, + value: itemEntry.value.map((val) => val.toLowerCase()), + }; + } + } + return itemEntry; + }); + return { + ...item, + entries: newEntries, + }; + }); +}; + /** * Returns the value for the given fieldname within TimelineNonEcsData if it exists */ @@ -413,7 +443,7 @@ export const defaultEndpointExceptionItems = ( data: alertData, fieldName: 'file.Ext.code_signature.trusted', }); - const [sha1Hash] = getMappedNonEcsValue({ data: alertData, fieldName: 'file.hash.sha1' }); + const [sha256Hash] = getMappedNonEcsValue({ data: alertData, fieldName: 'file.hash.sha256' }); const [eventCode] = getMappedNonEcsValue({ data: alertData, fieldName: 'event.code' }); const namespaceType = 'agnostic'; @@ -446,10 +476,10 @@ export const defaultEndpointExceptionItems = ( value: filePath ?? '', }, { - field: 'file.hash.sha1', + field: 'file.hash.sha256', operator: 'included', type: 'match', - value: sha1Hash ?? '', + value: sha256Hash ?? '', }, { field: 'event.code', diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 010129d2d4593..f38a9107afca9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -202,7 +202,7 @@ export const requiredFieldsForActions = [ 'file.path', 'file.Ext.code_signature.subject_name', 'file.Ext.code_signature.trusted', - 'file.hash.sha1', + 'file.hash.sha256', 'host.os.family', 'event.code', ]; From c670afab5ac3557b1aaa4adc66e152a176960cd6 Mon Sep 17 00:00:00 2001 From: Madison Caldwell Date: Thu, 30 Jul 2020 16:00:24 -0400 Subject: [PATCH 12/33] Handle promise rejections when building artifacts (#73831) Co-authored-by: Elastic Machine --- .../manifest_manager/manifest_manager.mock.ts | 21 +++++++++++++++---- .../manifest_manager/manifest_manager.test.ts | 11 +++++++++- .../manifest_manager/manifest_manager.ts | 11 +++++----- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts index 592ffb0eae62a..34e18c5fe85fc 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts @@ -8,6 +8,7 @@ import { savedObjectsClientMock, loggingSystemMock } from 'src/core/server/mocks import { Logger } from 'src/core/server'; import { PackageConfigServiceInterface } from '../../../../../../ingest_manager/server'; import { createPackageConfigServiceMock } from '../../../../../../ingest_manager/server/mocks'; +import { ExceptionListClient } from '../../../../../../lists/server'; import { listMock } from '../../../../../../lists/server/mocks'; import LRU from 'lru-cache'; import { getArtifactClientMock } from '../artifact_client.mock'; @@ -23,22 +24,29 @@ import { export enum ManifestManagerMockType { InitialSystemState, + ListClientPromiseRejection, NormalFlow, } export const getManifestManagerMock = (opts?: { mockType?: ManifestManagerMockType; cache?: LRU; + exceptionListClient?: ExceptionListClient; packageConfigService?: jest.Mocked; savedObjectsClient?: ReturnType; }): ManifestManager => { let cache = new LRU({ max: 10, maxAge: 1000 * 60 * 60 }); - if (opts?.cache !== undefined) { + if (opts?.cache != null) { cache = opts.cache; } + let exceptionListClient = listMock.getExceptionListClient(); + if (opts?.exceptionListClient != null) { + exceptionListClient = opts.exceptionListClient; + } + let packageConfigService = createPackageConfigServiceMock(); - if (opts?.packageConfigService !== undefined) { + if (opts?.packageConfigService != null) { packageConfigService = opts.packageConfigService; } packageConfigService.list = jest.fn().mockResolvedValue({ @@ -51,7 +59,7 @@ export const getManifestManagerMock = (opts?: { }); let savedObjectsClient = savedObjectsClientMock.create(); - if (opts?.savedObjectsClient !== undefined) { + if (opts?.savedObjectsClient != null) { savedObjectsClient = opts.savedObjectsClient; } @@ -61,6 +69,11 @@ export const getManifestManagerMock = (opts?: { switch (mockType) { case ManifestManagerMockType.InitialSystemState: return getEmptyMockArtifacts(); + case ManifestManagerMockType.ListClientPromiseRejection: + exceptionListClient.findExceptionListItem = jest + .fn() + .mockRejectedValue(new Error('unexpected thing happened')); + return super.buildExceptionListArtifacts('v1'); case ManifestManagerMockType.NormalFlow: return getMockArtifactsWithDiff(); } @@ -85,7 +98,7 @@ export const getManifestManagerMock = (opts?: { artifactClient: getArtifactClientMock(savedObjectsClient), cache, packageConfigService, - exceptionListClient: listMock.getExceptionListClient(), + exceptionListClient, logger: loggingSystemMock.create().get() as jest.Mocked, savedObjectsClient, }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index d99d6a959d7aa..8e0d55104fb7c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -9,7 +9,7 @@ import { savedObjectsClientMock } from 'src/core/server/mocks'; import { createPackageConfigServiceMock } from '../../../../../../ingest_manager/server/mocks'; import { ArtifactConstants, ManifestConstants, isCompleteArtifact } from '../../../lib/artifacts'; -import { getManifestManagerMock } from './manifest_manager.mock'; +import { getManifestManagerMock, ManifestManagerMockType } from './manifest_manager.mock'; import LRU from 'lru-cache'; describe('manifest_manager', () => { @@ -204,5 +204,14 @@ describe('manifest_manager', () => { oldArtifactId ); }); + + test('ManifestManager handles promise rejections when building artifacts', async () => { + // This test won't fail on an unhandled promise rejection, but it will cause + // an UnhandledPromiseRejectionWarning to be printed. + const manifestManager = getManifestManagerMock({ + mockType: ManifestManagerMockType.ListClientPromiseRejection, + }); + await expect(manifestManager.buildNewManifest()).rejects.toThrow(); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index 217fd6de2ba68..7d700cdffc60d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -82,18 +82,17 @@ export class ManifestManager { protected async buildExceptionListArtifacts( artifactSchemaVersion?: string ): Promise { - return ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.reduce< - Promise - >(async (acc, os) => { + const artifacts: InternalArtifactCompleteSchema[] = []; + for (const os of ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS) { const exceptionList = await getFullEndpointExceptionList( this.exceptionListClient, os, artifactSchemaVersion ?? 'v1' ); - const artifacts = await acc; const artifact = await buildArtifact(exceptionList, os, artifactSchemaVersion ?? 'v1'); - return Promise.resolve([...artifacts, artifact]); - }, Promise.resolve([])); + artifacts.push(artifact); + } + return artifacts; } /** From 0d8846d374f353f5473025b1a66b683b74df3fbf Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Thu, 30 Jul 2020 13:12:30 -0700 Subject: [PATCH 13/33] [APM] fixes linking errors to ML and Discover (#73758) * Closes #73755 by removing the extra uri encoding from time ranges and using correct refreshValue rison types rather than just strings * removes commented out test assertion --- .../DiscoverLinks.integration.test.tsx | 16 ++++----- .../MachineLearningLinks/MLJobLink.test.tsx | 4 +-- .../MachineLearningLinks/MLLink.test.tsx | 2 +- .../useTimeSeriesExplorerHref.test.ts | 34 +++++++++++++++++++ .../useTimeSeriesExplorerHref.ts | 5 ++- .../shared/Links/rison_helpers.test.ts | 27 +++++++++++++++ .../components/shared/Links/rison_helpers.ts | 14 ++++---- .../__test__/sections.test.ts | 6 ++-- 8 files changed, 85 insertions(+), 23 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.test.ts create mode 100644 x-pack/plugins/apm/public/components/shared/Links/rison_helpers.test.ts diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx index 359468073f7f4..ca02abc395992 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx @@ -33,8 +33,8 @@ describe('DiscoverLinks', () => { } as Location ); - expect(href).toEqual( - `/basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'processor.event:"transaction" AND transaction.id:"8b60bd32ecc6e150" AND trace.id:"8b60bd32ecc6e1506735a8b6cfcf175c"'))` + expect(href).toMatchInlineSnapshot( + `"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'processor.event:\\"transaction\\" AND transaction.id:\\"8b60bd32ecc6e150\\" AND trace.id:\\"8b60bd32ecc6e1506735a8b6cfcf175c\\"'))"` ); }); @@ -50,8 +50,8 @@ describe('DiscoverLinks', () => { '?rangeFrom=now/w&rangeTo=now&refreshPaused=true&refreshInterval=0', } as Location); - expect(href).toEqual( - `/basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'span.id:"test-span-id"'))` + expect(href).toMatchInlineSnapshot( + `"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'span.id:\\"test-span-id\\"'))"` ); }); @@ -72,8 +72,8 @@ describe('DiscoverLinks', () => { } as Location ); - expect(href).toEqual( - `/basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'service.name:"service-name" AND error.grouping_key:"grouping-key"'),sort:('@timestamp':desc))` + expect(href).toMatchInlineSnapshot( + `"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'service.name:\\"service-name\\" AND error.grouping_key:\\"grouping-key\\"'),sort:('@timestamp':desc))"` ); }); @@ -95,8 +95,8 @@ describe('DiscoverLinks', () => { } as Location ); - expect(href).toEqual( - `/basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'service.name:"service-name" AND error.grouping_key:"grouping-key" AND some:kuery-string'),sort:('@timestamp':desc))` + expect(href).toMatchInlineSnapshot( + `"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'service.name:\\"service-name\\" AND error.grouping_key:\\"grouping-key\\" AND some:kuery-string'),sort:('@timestamp':desc))"` ); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx index 39082c2639a2c..9ba4aab0e23d9 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx @@ -22,7 +22,7 @@ describe('MLJobLink', () => { ); expect(href).toMatchInlineSnapshot( - `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))"` + `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now-4h))"` ); }); it('should produce the correct URL with jobId, serviceName, and transactionType', async () => { @@ -41,7 +41,7 @@ describe('MLJobLink', () => { ); expect(href).toMatchInlineSnapshot( - `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))&_a=(mlTimeSeriesExplorer:(entities:(service.name:opbeans-test,transaction.type:request)))"` + `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now-4h))&_a=(mlTimeSeriesExplorer:(entities:(service.name:opbeans-test,transaction.type:request),zoom:(from:now/w,to:now-4h)))"` ); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx index b4187b2f797ab..da345e35c10b1 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx @@ -21,6 +21,6 @@ test('MLLink produces the correct URL', async () => { ); expect(href).toMatchInlineSnapshot( - `"/basepath/app/ml#/some/path?_g=(ml:(jobIds:!(something)),refreshInterval:(pause:true,value:'0'),time:(from:now-5h,to:now-2h))"` + `"/basepath/app/ml#/some/path?_g=(ml:(jobIds:!(something)),refreshInterval:(pause:!t,value:0),time:(from:now-5h,to:now-2h))"` ); }); diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.test.ts b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.test.ts new file mode 100644 index 0000000000000..28daae7fd830e --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useTimeSeriesExplorerHref } from './useTimeSeriesExplorerHref'; + +jest.mock('../../../../hooks/useApmPluginContext', () => ({ + useApmPluginContext: () => ({ + core: { http: { basePath: { prepend: (url: string) => url } } }, + }), +})); + +jest.mock('../../../../hooks/useLocation', () => ({ + useLocation: () => ({ + search: + '?rangeFrom=2020-07-29T17:27:29.000Z&rangeTo=2020-07-29T18:45:00.000Z&refreshInterval=10000&refreshPaused=true', + }), +})); + +describe('useTimeSeriesExplorerHref', () => { + it('correctly encodes time range values', async () => { + const href = useTimeSeriesExplorerHref({ + jobId: 'apm-production-485b-high_mean_transaction_duration', + serviceName: 'opbeans-java', + transactionType: 'request', + }); + + expect(href).toMatchInlineSnapshot( + `"/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(apm-production-485b-high_mean_transaction_duration)),refreshInterval:(pause:!t,value:10000),time:(from:'2020-07-29T17:27:29.000Z',to:'2020-07-29T18:45:00.000Z'))&_a=(mlTimeSeriesExplorer:(entities:(service.name:opbeans-java,transaction.type:request),zoom:(from:'2020-07-29T17:27:29.000Z',to:'2020-07-29T18:45:00.000Z')))"` + ); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts index 625b9205b6ce0..459ee8f0282ff 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts @@ -22,12 +22,14 @@ export function useTimeSeriesExplorerHref({ }) { const { core } = useApmPluginContext(); const location = useLocation(); + const { time, refreshInterval } = getTimepickerRisonData(location.search); const search = querystring.stringify( { _g: rison.encode({ ml: { jobIds: [jobId] }, - ...getTimepickerRisonData(location.search), + time, + refreshInterval, }), ...(serviceName && transactionType ? { @@ -37,6 +39,7 @@ export function useTimeSeriesExplorerHref({ 'service.name': serviceName, 'transaction.type': transactionType, }, + zoom: time, }, }), } diff --git a/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.test.ts b/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.test.ts new file mode 100644 index 0000000000000..8dd662339b61a --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.test.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 { getTimepickerRisonData } from './rison_helpers'; + +describe('getTimepickerRisonData', () => { + it('returns object of timepicker range and refresh interval values', async () => { + const locationSearch = `?rangeFrom=2020-07-29T17:27:29.000Z&rangeTo=2020-07-29T18:45:00.000Z&refreshInterval=10000&refreshPaused=true`; + const timepickerValues = getTimepickerRisonData(locationSearch); + + expect(timepickerValues).toMatchInlineSnapshot(` + Object { + "refreshInterval": Object { + "pause": true, + "value": 10000, + }, + "time": Object { + "from": "2020-07-29T17:27:29.000Z", + "to": "2020-07-29T18:45:00.000Z", + }, + } + `); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts b/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts index 8b4d891dba83b..cab822b42be56 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts @@ -22,18 +22,16 @@ export function getTimepickerRisonData(currentSearch: Location['search']) { const currentQuery = toQuery(currentSearch); return { time: { - from: currentQuery.rangeFrom - ? encodeURIComponent(currentQuery.rangeFrom) - : '', - to: currentQuery.rangeTo ? encodeURIComponent(currentQuery.rangeTo) : '', + from: currentQuery.rangeFrom || '', + to: currentQuery.rangeTo || '', }, refreshInterval: { pause: currentQuery.refreshPaused - ? String(currentQuery.refreshPaused) - : '', + ? Boolean(currentQuery.refreshPaused) + : true, value: currentQuery.refreshInterval - ? String(currentQuery.refreshInterval) - : '', + ? parseInt(currentQuery.refreshInterval, 10) + : 0, }, }; } diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts index 186fc082ce5fe..e057e9c034615 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts @@ -68,7 +68,7 @@ describe('Transaction action menu', () => { key: 'sampleDocument', label: 'View sample document', href: - 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:\'0\'),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', + 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', condition: true, }, ], @@ -139,7 +139,7 @@ describe('Transaction action menu', () => { key: 'sampleDocument', label: 'View sample document', href: - 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:\'0\'),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', + 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', condition: true, }, ], @@ -209,7 +209,7 @@ describe('Transaction action menu', () => { key: 'sampleDocument', label: 'View sample document', href: - 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:\'0\'),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', + 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', condition: true, }, ], From 83fab293bc8c15660249b8d449e36e7b806056b6 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 30 Jul 2020 13:20:07 -0700 Subject: [PATCH 14/33] [DOCS] Fixes typo in Alerting actions (#73756) --- .../alerting-getting-started.asciidoc | 202 +++++++++++++++++ docs/user/alerting/index.asciidoc | 203 +----------------- 2 files changed, 203 insertions(+), 202 deletions(-) create mode 100644 docs/user/alerting/alerting-getting-started.asciidoc diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc new file mode 100644 index 0000000000000..6bc085b0f78b9 --- /dev/null +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -0,0 +1,202 @@ +[role="xpack"] +[[alerting-getting-started]] += Alerting and Actions + +beta[] + +-- + +Alerting allows you to detect complex conditions within different {kib} apps and trigger actions when those conditions are met. Alerting is integrated with <>, <>, <>, <>, can be centrally managed from the <> UI, and provides a set of built-in <> and <> for you to use. + +image::images/alerting-overview.png[Alerts and actions UI] + +[IMPORTANT] +============================================== +To make sure you can access alerting and actions, see the <> section. +============================================== + +[float] +== Concepts and terminology + +*Alerts* work by running checks on a schedule to detect conditions. When a condition is met, the alert tracks it as an *alert instance* and responds by triggering one or more *actions*. +Actions typically involve interaction with {kib} services or third party integrations. *Connectors* allow actions to talk to these services and integrations. +This section describes all of these elements and how they operate together. + +[float] +=== What is an alert? + +An alert specifies a background task that runs on the {kib} server to check for specific conditions. It consists of three main parts: + +* *Conditions*: what needs to be detected? +* *Schedule*: when/how often should detection checks run? +* *Actions*: what happens when a condition is detected? + +For example, when monitoring a set of servers, an alert might check for average CPU usage > 0.9 on each server for the two minutes (condition), checked every minute (schedule), sending a warning email message via SMTP with subject `CPU on {{server}} is high` (action). + +image::images/what-is-an-alert.svg[Three components of an alert] + +The following sections each part of the alert is described in more detail. + +[float] +[[alerting-concepts-conditions]] +==== Conditions + +Under the hood, {kib} alerts detect conditions by running javascript function on the {kib} server, which gives it flexibility to support a wide range of detections, anything from the results of a simple {es} query to heavy computations involving data from multiple sources or external systems. + +These detections are packaged and exposed as *alert types*. An alert type hides the underlying details of the detection, and exposes a set of parameters +to control the details of the conditions to detect. + +For example, an <> lets you specify the index to query, an aggregation field, and a time window, but the details of the underlying {es} query are hidden. + +See <> for the types of alerts provided by {kib} and how they express their conditions. + +[float] +[[alerting-concepts-scheduling]] +==== Schedule + +Alert schedules are defined as an interval between subsequent checks, and can range from a few seconds to months. + +[IMPORTANT] +============================================== +The intervals of alert checks in {kib} are approximate, their timing of their execution is affected by factors such as the frequency at which tasks are claimed and the task load on the system. See <> for more information. +============================================== + +[float] +[[alerting-concepts-actions]] +==== Actions + +Actions are invocations of {kib} services or integrations with third-party systems, that run as background tasks on the {kib} server when alert conditions are met. + +When defining actions in an alert, you specify: + +* the *action type*: the type of service or integration to use +* the connection for that type by referencing a <> +* a mapping of alert values to properties exposed for that type of action + +The result is a template: all the parameters needed to invoke a service are supplied except for specific values that are only known at the time the alert condition is detected. + +In the server monitoring example, the `email` action type is used, and `server` is mapped to the body of the email, using the template string `CPU on {{server}} is high`. + +When the alert detects the condition, it creates an <> containing the details of the condition, renders the template with these details such as server name, and executes the action on the {kib} server by invoking the `email` action type. + +image::images/what-is-an-action.svg[Actions are like templates that are rendered when an alert detects a condition] + +See <> for details on the types of actions provided by {kib}. + +[float] +[[alerting-concepts-alert-instances]] +=== Alert instances + +When checking for a condition, an alert might identify multiple occurrences of the condition. {kib} tracks each of these *alert instances* separately and takes action per instance. + +Using the server monitoring example, each server with average CPU > 0.9 is tracked as an alert instance. This means a separate email is sent for each server that exceeds the threshold. + +image::images/alert-instances.svg[{kib} tracks each detected condition as an alert instance and takes action on each instance] + +[float] +[[alerting-concepts-suppressing-duplicate-notifications]] +=== Suppressing duplicate notifications + +Since actions are taken per instance, alerts can end up generating a large number of actions. Take the following example where an alert is monitoring three servers every minute for CPU usage > 0.9: + +* Minute 1: server X123 > 0.9. *One email* is sent for server X123. +* Minute 2: X123 and Y456 > 0.9. *Two emails* are sent, on for X123 and one for Y456. +* Minute 3: X123, Y456, Z789 > 0.9. *Three emails* are sent, one for each of X123, Y456, Z789. + +In the above example, three emails are sent for server X123 in the span of 3 minutes for the same condition. Often it's desirable to suppress frequent re-notification. Operations like muting and re-notification throttling can be applied at the instance level. If we set the alert re-notify interval to 5 minutes, we reduce noise by only getting emails for new servers that exceed the threshold: + +* Minute 1: server X123 > 0.9. *One email* is sent for server X123. +* Minute 2: X123 and Y456 > 0.9. *One email* is sent for Y456 +* Minute 3: X123, Y456, Z789 > 0.9. *One email* is sent for Z789. + +[float] +[[alerting-concepts-connectors]] +=== Connectors + +Actions often involve connecting with services inside {kib} or integrations with third-party systems. +Rather than repeatedly entering connection information and credentials for each action, {kib} simplifies action setup using *connectors*. + +*Connectors* provide a central place to store connection information for services and integrations. For example if four alerts send email notifications via the same SMTP service, +they all reference the same SMTP connector. When the SMTP settings change they are updated once in the connector, instead of having to update four alerts. + +image::images/alert-concepts-connectors.svg[Connectors provide a central place to store service connection settings] + +[float] +=== Summary + +An _alert_ consists of conditions, _actions_, and a schedule. When conditions are met, _alert instances_ are created that render _actions_ and invoke them. To make action setup and update easier, actions refer to _connectors_ that centralize the information used to connect with {kib} services and third-party integrations. + +image::images/alert-concepts-summary.svg[Alerts, actions, alert instances and connectors work together to convert detection into action] + +* *Alert*: a specification of the conditions to be detected, the schedule for detection, and the response when detection occurs. +* *Action*: the response to a detected condition defined in the alert. Typically actions specify a service or third party integration along with alert details that will be sent to it. +* *Alert instance*: state tracked by {kib} for every occurrence of a detected condition. Actions as well as controls like muting and re-notification are controlled at the instance level. +* *Connector*: centralized configurations for services and third party integration that are referenced by actions. + +[float] +[[alerting-concepts-differences]] +== Differences from Watcher + +{kib} alerting and <> are both used to detect conditions and can trigger actions in response, but they are completely independent alerting systems. + +This section will clarify some of the important differences in the function and intent of the two systems. + +Functionally, {kib} alerting differs in that: + +* Scheduled checks are run on {kib} instead of {es} +* {kib} <> through *alert types*, whereas watches provide low-level control over inputs, conditions, and transformations. +* {kib} alerts tracks and persists the state of each detected condition through *alert instances*. This makes it possible to mute and throttle individual instances, and detect changes in state such as resolution. +* Actions are linked to *alert instances* in {kib} alerting. Actions are fired for each occurrence of a detected condition, rather than for the entire alert. + +At a higher level, {kib} alerts allow rich integrations across use cases like <>, <>, <>, and <>. +Pre-packaged *alert types* simplify setup, hide the details complex domain-specific detections, while providing a consistent interface across {kib}. + +[float] +[[alerting-setup-prerequisites]] +== Setup and prerequisites + +If you are using an *on-premises* Elastic Stack deployment: + +* In the kibana.yml configuration file, add the <> setting. + +If you are using an *on-premises* Elastic Stack deployment with <>: + +* You must enable Transport Layer Security (TLS) for communication <>. {kib} alerting uses <> to secure background alert checks and actions, and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. A proxy will not suffice. + +[float] +[[alerting-security]] +== Security + +To access alerting in a space, a user must have access to one of the following features: + +* <> +* <> +* <> +* <> + +See <> for more information on configuring roles that provide access to these features. + +[float] +[[alerting-spaces]] +=== Space isolation + +Alerts and connectors are isolated to the {kib} space in which they were created. An alert or connector created in one space will not be visible in another. + +[float] +[[alerting-authorization]] +=== Authorization + +Alerts, including all background detection and the actions they generate are authorized using an <> associated with the last user to edit the alert. Upon creating or modifying an alert, an API key is generated for that user, capturing a snapshot of their privileges at that moment in time. The API key is then used to run all background tasks associated with the alert including detection checks and executing actions. + +[IMPORTANT] +============================================== +If an alert requires certain privileges to run such as index privileges, keep in mind that if a user without those privileges updates the alert, the alert will no longer function. +============================================== + +[float] +[[alerting-restricting-actions]] +=== Restricting actions + +For security reasons you may wish to limit the extent to which {kib} can connect to external services. <> allows you to disable certain <> and whitelist the hostnames that {kib} can connect with. + +-- \ No newline at end of file diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc index 6f691f2715bc8..56404d9a33b80 100644 --- a/docs/user/alerting/index.asciidoc +++ b/docs/user/alerting/index.asciidoc @@ -1,205 +1,4 @@ -[role="xpack"] -[[alerting-getting-started]] -= Alerting and Actions - -beta[] - --- - -Alerting allows you to detect complex conditions within different {kib} apps and trigger actions when those conditions are met. Alerting is integrated with <>, <>, <>, <>, can be centrally managed from the <> UI, and provides a set of built-in <> and <> for you to use. - -image::images/alerting-overview.png[Alerts and actions UI] - -[IMPORTANT] -============================================== -To make sure you can access alerting and actions, see the <> section. -============================================== - -[float] -== Concepts and terminology - -*Alerts* work by running checks on a schedule to detect conditions. When a condition is met, the alert tracks it as an *alert instance* and responds by triggering one or more *actions*. -Actions typically involve interaction with {kib} services or third party integrations. *Connectors* allow actions to talk to these services and integrations. -This section describes all of these elements and how they operate together. - -[float] -=== What is an alert? - -An alert specifies a background task that runs on the {kib} server to check for specific conditions. It consists of three main parts: - -* *Conditions*: what needs to be detected? -* *Schedule*: when/how often should detection checks run? -* *Actions*: what happens when a condition is detected? - -For example, when monitoring a set of servers, an alert might check for average CPU usage > 0.9 on each server for the two minutes (condition), checked every minute (schedule), sending a warning email message via SMTP with subject `CPU on {{server}} is high` (action). - -image::images/what-is-an-alert.svg[Three components of an alert] - -The following sections each part of the alert is described in more detail. - -[float] -[[alerting-concepts-conditions]] -==== Conditions - -Under the hood, {kib} alerts detect conditions by running javascript function on the {kib} server, which gives it flexibility to support a wide range of detections, anything from the results of a simple {es} query to heavy computations involving data from multiple sources or external systems. - -These detections are packaged and exposed as *alert types*. An alert type hides the underlying details of the detection, and exposes a set of parameters -to control the details of the conditions to detect. - -For example, an <> lets you specify the index to query, an aggregation field, and a time window, but the details of the underlying {es} query are hidden. - -See <> for the types of alerts provided by {kib} and how they express their conditions. - -[float] -[[alerting-concepts-scheduling]] -==== Schedule - -Alert schedules are defined as an interval between subsequent checks, and can range from a few seconds to months. - -[IMPORTANT] -============================================== -The intervals of alert checks in {kib} are approximate, their timing of their execution is affected by factors such as the frequency at which tasks are claimed and the task load on the system. See <> for more information. -============================================== - -[float] -[[alerting-concepts-actions]] -==== Actions - -Actions are invocations of {kib} services or integrations with third-party systems, that run as background tasks on the {kib} server when alert conditions are met. - -When defining actions in an alert, you specify -* the *action type*: the type of service or integration to use> -* the connection for that type by referencing a <>. -* a mapping of alert values to properties exposed for that type of action. - -The result is a template: all the parameters needed to invoke a service are supplied except for specific values that are only known at the time the alert condition is detected. - -In the server monitoring example, the `email` action type is used, and `server` is mapped to the body of the email, using the template string `CPU on {{server}} is high`. - -When the alert detects the condition, it creates an <> containing the details of the condition, renders the template with these details such as server name, and executes the action on the {kib} server by invoking the `email` action type. - -image::images/what-is-an-action.svg[Actions are like templates that are rendered when an alert detects a condition] - -See <> for details on the types of actions provided by {kib}. - -[float] -[[alerting-concepts-alert-instances]] -=== Alert instances - -When checking for a condition, an alert might identify multiple occurrences of the condition. {kib} tracks each of these *alert instances* separately and takes action per instance. - -Using the server monitoring example, each server with average CPU > 0.9 is tracked as an alert instance. This means a separate email is sent for each server that exceeds the threshold. - -image::images/alert-instances.svg[{kib} tracks each detected condition as an alert instance and takes action on each instance] - -[float] -[[alerting-concepts-suppressing-duplicate-notifications]] -=== Suppressing duplicate notifications - -Since actions are taken per instance, alerts can end up generating a large number of actions. Take the following example where an alert is monitoring three servers every minute for CPU usage > 0.9: - -* Minute 1: server X123 > 0.9. *One email* is sent for server X123. -* Minute 2: X123 and Y456 > 0.9. *Two emails* are sent, on for X123 and one for Y456. -* Minute 3: X123, Y456, Z789 > 0.9. *Three emails* are sent, one for each of X123, Y456, Z789. - -In the above example, three emails are sent for server X123 in the span of 3 minutes for the same condition. Often it's desirable to suppress frequent re-notification. Operations like muting and re-notification throttling can be applied at the instance level. If we set the alert re-notify interval to 5 minutes, we reduce noise by only getting emails for new servers that exceed the threshold: - -* Minute 1: server X123 > 0.9. *One email* is sent for server X123. -* Minute 2: X123 and Y456 > 0.9. *One email* is sent for Y456 -* Minute 3: X123, Y456, Z789 > 0.9. *One email* is sent for Z789. - -[float] -[[alerting-concepts-connectors]] -=== Connectors - -Actions often involve connecting with services inside {kib} or integrations with third-party systems. -Rather than repeatedly entering connection information and credentials for each action, {kib} simplifies action setup using *connectors*. - -*Connectors* provide a central place to store connection information for services and integrations. For example if four alerts send email notifications via the same SMTP service, -they all reference the same SMTP connector. When the SMTP settings change they are updated once in the connector, instead of having to update four alerts. - -image::images/alert-concepts-connectors.svg[Connectors provide a central place to store service connection settings] - -[float] -=== Summary - -An _alert_ consists of conditions, _actions_, and a schedule. When conditions are met, _alert instances_ are created that render _actions_ and invoke them. To make action setup and update easier, actions refer to _connectors_ that centralize the information used to connect with {kib} services and third-party integrations. - -image::images/alert-concepts-summary.svg[Alerts, actions, alert instances and connectors work together to convert detection into action] - -* *Alert*: a specification of the conditions to be detected, the schedule for detection, and the response when detection occurs. -* *Action*: the response to a detected condition defined in the alert. Typically actions specify a service or third party integration along with alert details that will be sent to it. -* *Alert instance*: state tracked by {kib} for every occurrence of a detected condition. Actions as well as controls like muting and re-notification are controlled at the instance level. -* *Connector*: centralized configurations for services and third party integration that are referenced by actions. - -[float] -[[alerting-concepts-differences]] -== Differences from Watcher - -{kib} alerting and <> are both used to detect conditions and can trigger actions in response, but they are completely independent alerting systems. - -This section will clarify some of the important differences in the function and intent of the two systems. - -Functionally, {kib} alerting differs in that: - -* Scheduled checks are run on {kib} instead of {es} -* {kib} <> through *alert types*, whereas watches provide low-level control over inputs, conditions, and transformations. -* {kib} alerts tracks and persists the state of each detected condition through *alert instances*. This makes it possible to mute and throttle individual instances, and detect changes in state such as resolution. -* Actions are linked to *alert instances* in {kib} alerting. Actions are fired for each occurrence of a detected condition, rather than for the entire alert. - -At a higher level, {kib} alerts allow rich integrations across use cases like <>, <>, <>, and <>. -Pre-packaged *alert types* simplify setup, hide the details complex domain-specific detections, while providing a consistent interface across {kib}. - -[float] -[[alerting-setup-prerequisites]] -== Setup and prerequisites - -If you are using an *on-premises* Elastic Stack deployment: - -* In the kibana.yml configuration file, add the <> setting. - -If you are using an *on-premises* Elastic Stack deployment with <>: - -* You must enable Transport Layer Security (TLS) for communication <>. {kib} alerting uses <> to secure background alert checks and actions, and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. A proxy will not suffice. - -[float] -[[alerting-security]] -== Security - -To access alerting in a space, a user must have access to one of the following features: - -* <> -* <> -* <> -* <> - -See <> for more information on configuring roles that provide access to these features. - -[float] -[[alerting-spaces]] -=== Space isolation - -Alerts and connectors are isolated to the {kib} space in which they were created. An alert or connector created in one space will not be visible in another. - -[float] -[[alerting-authorization]] -=== Authorization - -Alerts, including all background detection and the actions they generate are authorized using an <> associated with the last user to edit the alert. Upon creating or modifying an alert, an API key is generated for that user, capturing a snapshot of their privileges at that moment in time. The API key is then used to run all background tasks associated with the alert including detection checks and executing actions. - -[IMPORTANT] -============================================== -If an alert requires certain privileges to run such as index privileges, keep in mind that if a user without those privileges updates the alert, the alert will no longer function. -============================================== - -[float] -[[alerting-restricting-actions]] -=== Restricting actions - -For security reasons you may wish to limit the extent to which {kib} can connect to external services. <> allows you to disable certain <> and whitelist the hostnames that {kib} can connect with. - --- - +include::alerting-getting-started.asciidoc[] include::defining-alerts.asciidoc[] include::action-types.asciidoc[] include::alert-types.asciidoc[] From a6888e369465ae65e8d1c5181c25dad1d50b0a43 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 30 Jul 2020 16:21:29 -0400 Subject: [PATCH 15/33] [Ingest Manager] Fix config selection in enrollment flyout from config list page (#73833) --- .../agent_enrollment_flyout/config_selection.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx index e98ebb7cadc7c..5343d86244f1e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx @@ -64,6 +64,14 @@ export const EnrollmentStepAgentConfig: React.FC = (props) => { useEffect( function useDefaultConfigEffect() { if (agentConfigs && agentConfigs.length && !selectedState.agentConfigId) { + if (agentConfigs.length === 1) { + setSelectedState({ + ...selectedState, + agentConfigId: agentConfigs[0].id, + }); + return; + } + const defaultConfig = agentConfigs.find((config) => config.is_default); if (defaultConfig) { setSelectedState({ From 5e86d2f848ee6b44fa42dd27c4aa00ddb3288e44 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Thu, 30 Jul 2020 13:44:37 -0700 Subject: [PATCH 16/33] Closes #72914 by hiding anomaly detection settings links when the ml plugin is disabled. (#73638) Co-authored-by: Elastic Machine --- .../apm/public/components/app/Home/index.tsx | 11 ++++--- .../anomaly_detection/add_environments.tsx | 2 +- .../app/Settings/anomaly_detection/index.tsx | 4 +-- .../public/components/app/Settings/index.tsx | 33 ++++++++++++------- 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/Home/index.tsx b/x-pack/plugins/apm/public/components/app/Home/index.tsx index b09c03f853aa9..c6c0861c26a34 100644 --- a/x-pack/plugins/apm/public/components/app/Home/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Home/index.tsx @@ -83,7 +83,8 @@ interface Props { } export function Home({ tab }: Props) { - const { config } = useApmPluginContext(); + const { config, core } = useApmPluginContext(); + const isMLEnabled = !!core.application.capabilities.ml; const homeTabs = getHomeTabs(config); const selectedTab = homeTabs.find( (homeTab) => homeTab.name === tab @@ -105,9 +106,11 @@ export function Home({ tab }: Props) { - - - + {isMLEnabled && ( + + + + )} diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx index 48fb19560e43f..a594edb32b083 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx @@ -64,7 +64,7 @@ export function AddEnvironments({ return ( {ML_ERRORS.MISSING_WRITE_PRIVILEGES}} /> diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx index f59949b22b3c8..9c04caf61022a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx @@ -29,7 +29,7 @@ const DEFAULT_VALUE: AnomalyDetectionApiResponse = { export function AnomalyDetection() { const plugin = useApmPluginContext(); - const canGetJobs = !!plugin.core.application.capabilities.ml.canGetJobs; + const canGetJobs = !!plugin.core.application.capabilities.ml?.canGetJobs; const license = useLicense(); const hasValidLicense = license?.isActive && license?.hasAtLeast('platinum'); @@ -57,7 +57,7 @@ export function AnomalyDetection() { return ( {ML_ERRORS.MISSING_READ_PRIVILEGES}} /> diff --git a/x-pack/plugins/apm/public/components/app/Settings/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/index.tsx index bd2ea706e492d..1471bc345d850 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/index.tsx @@ -16,8 +16,11 @@ import { import { HomeLink } from '../../shared/Links/apm/HomeLink'; import { useLocation } from '../../../hooks/useLocation'; import { getAPMHref } from '../../shared/Links/apm/APMLink'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; export function Settings(props: { children: ReactNode }) { + const plugin = useApmPluginContext(); + const isMLEnabled = !!plugin.core.application.capabilities.ml; const { search, pathname } = useLocation(); return ( <> @@ -48,17 +51,25 @@ export function Settings(props: { children: ReactNode }) { '/settings/agent-configuration' ), }, - { - name: i18n.translate( - 'xpack.apm.settings.anomalyDetection', - { - defaultMessage: 'Anomaly detection', - } - ), - id: '4', - href: getAPMHref('/settings/anomaly-detection', search), - isSelected: pathname === '/settings/anomaly-detection', - }, + ...(isMLEnabled + ? [ + { + name: i18n.translate( + 'xpack.apm.settings.anomalyDetection', + { + defaultMessage: 'Anomaly detection', + } + ), + id: '4', + href: getAPMHref( + '/settings/anomaly-detection', + search + ), + isSelected: + pathname === '/settings/anomaly-detection', + }, + ] + : []), { name: i18n.translate('xpack.apm.settings.customizeApp', { defaultMessage: 'Customize app', From c2d8869ccac54a80a9bc239ad141920c3a7df09d Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Thu, 30 Jul 2020 15:52:32 -0500 Subject: [PATCH 17/33] [Metrics UI] Fix previewing of No Data results (#73753) --- .../common/components/alert_preview.tsx | 2 +- .../inventory/components/expression.tsx | 1 + .../evaluate_condition.ts | 8 ++-- .../inventory_metric_threshold_executor.ts | 9 ++-- ...review_inventory_metric_threshold_alert.ts | 41 ++++++++++--------- .../metric_threshold/lib/evaluate_alert.ts | 4 +- .../metric_threshold_executor.ts | 2 +- .../preview_metric_threshold_alert.ts | 30 +++++++------- .../infra/server/routes/alerting/preview.ts | 23 +++++++---- 9 files changed, 67 insertions(+), 53 deletions(-) diff --git a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx index f3136ca155c78..d5b50fce38718 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx +++ b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx @@ -194,7 +194,7 @@ export const AlertPreview: React.FC = (props) => { plural: previewResult.resultTotals.noData !== 1 ? 's' : '', }} /> - ) : null} + ) : null}{' '} {previewResult.resultTotals.error ? ( = (props) => { validate={validateMetricThreshold} fetch={alertsContext.http.fetch} groupByDisplayName={alertParams.nodeType} + showNoDataResults={alertParams.alertOnNoData} /> diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts index 5c31c78b10fa9..3b795810b39f0 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts @@ -23,9 +23,9 @@ import { InfraSourceConfiguration } from '../../sources'; import { UNGROUPED_FACTORY_KEY } from '../common/utils'; type ConditionResult = InventoryMetricConditions & { - shouldFire: boolean | boolean[]; + shouldFire: boolean[]; currentValue: number; - isNoData: boolean; + isNoData: boolean[]; isError: boolean; }; @@ -71,8 +71,8 @@ export const evaluateCondition = async ( value !== null && (Array.isArray(value) ? value.map((v) => comparisonFunction(Number(v), threshold)) - : comparisonFunction(value as number, threshold)), - isNoData: value === null, + : [comparisonFunction(value as number, threshold)]), + isNoData: Array.isArray(value) ? value.map((v) => v === null) : [value === null], isError: value === undefined, currentValue: getCurrentValue(value), }; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 60eee49a5010d..7b816f2f225b5 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -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 { first, get } from 'lodash'; +import { first, get, last } from 'lodash'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { toMetricOpt } from '../../../../common/snapshot_metric_i18n'; @@ -56,11 +56,14 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = for (const item of inventoryItems) { const alertInstance = services.alertInstanceFactory(`${item}`); // AND logic; all criteria must be across the threshold - const shouldAlertFire = results.every((result) => result[item].shouldFire); + const shouldAlertFire = results.every((result) => + // Grab the result of the most recent bucket + last(result[item].shouldFire) + ); // AND logic; because we need to evaluate all criteria, if one of them reports no data then the // whole alert is in a No Data/Error state - const isNoData = results.some((result) => result[item].isNoData); + const isNoData = results.some((result) => last(result[item].isNoData)); const isError = results.some((result) => result[item].isError); const nextState = isError diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts index 5c654e2f47e78..562f344dbd060 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts @@ -59,28 +59,29 @@ export const previewInventoryMetricThresholdAlert = async ({ const inventoryItems = Object.keys(first(results) as any); const previewResults = inventoryItems.map((item) => { - const isNoData = results.some((result) => result[item].isNoData); - if (isNoData) { - return null; - } - const isError = results.some((result) => result[item].isError); - if (isError) { - return undefined; - } - const numberOfResultBuckets = lookbackSize; const numberOfExecutionBuckets = Math.floor(numberOfResultBuckets / alertResultsPerExecution); - return [...Array(numberOfExecutionBuckets)].reduce( - (totalFired, _, i) => - totalFired + - (results.every((result) => { - const shouldFire = result[item].shouldFire as boolean[]; - return shouldFire[Math.floor(i * alertResultsPerExecution)]; - }) - ? 1 - : 0), - 0 - ); + let numberOfTimesFired = 0; + let numberOfNoDataResults = 0; + let numberOfErrors = 0; + for (let i = 0; i < numberOfExecutionBuckets; i++) { + const mappedBucketIndex = Math.floor(i * alertResultsPerExecution); + const allConditionsFiredInMappedBucket = results.every((result) => { + const shouldFire = result[item].shouldFire as boolean[]; + return shouldFire[mappedBucketIndex]; + }); + const someConditionsNoDataInMappedBucket = results.some((result) => { + const hasNoData = result[item].isNoData as boolean[]; + return hasNoData[mappedBucketIndex]; + }); + const someConditionsErrorInMappedBucket = results.some((result) => { + return result[item].isError; + }); + if (allConditionsFiredInMappedBucket) numberOfTimesFired++; + if (someConditionsNoDataInMappedBucket) numberOfNoDataResults++; + if (someConditionsErrorInMappedBucket) numberOfErrors++; + } + return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors]; }); return previewResults; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts index ca46f6cc16547..49f82c7ccec0b 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts @@ -72,7 +72,9 @@ export const evaluateAlert = ( typeof point.value === 'number' && comparisonFunction(point.value, threshold) ) : [false], - isNoData: (Array.isArray(points) ? last(points)?.value : points) === null, + isNoData: Array.isArray(points) + ? points.map((point) => point?.value === null || point === null) + : [points === null], isError: isNaN(Array.isArray(points) ? last(points)?.value : points), }; }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index b4754a8624fd5..b2a8f0281b9e2 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -45,7 +45,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => ); // AND logic; because we need to evaluate all criteria, if one of them reports no data then the // whole alert is in a No Data/Error state - const isNoData = alertResults.some((result) => result[group].isNoData); + const isNoData = alertResults.some((result) => last(result[group].isNoData)); const isError = alertResults.some((result) => result[group].isError); const nextState = isError diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts index 0ecfa27d0f0a8..5aca7f0890940 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts @@ -36,7 +36,7 @@ export const previewMetricThresholdAlert: ( params: PreviewMetricThresholdAlertParams, iterations?: number, precalculatedNumberOfGroups?: number -) => Promise> = async ( +) => Promise = async ( { callCluster, params, @@ -77,15 +77,6 @@ export const previewMetricThresholdAlert: ( const alertResultsPerExecution = alertIntervalInSeconds / bucketIntervalInSeconds; const previewResults = await Promise.all( groups.map(async (group) => { - const isNoData = alertResults.some((alertResult) => alertResult[group].isNoData); - if (isNoData) { - return null; - } - const isError = alertResults.some((alertResult) => alertResult[group].isError); - if (isError) { - return NaN; - } - // Interpolate the buckets returned by evaluateAlert and return a count of how many of these // buckets would have fired the alert. If the alert interval and bucket interval are the same, // this will be a 1:1 evaluation of the alert results. If these are different, the interpolation @@ -95,14 +86,25 @@ export const previewMetricThresholdAlert: ( numberOfResultBuckets / alertResultsPerExecution ); let numberOfTimesFired = 0; + let numberOfNoDataResults = 0; + let numberOfErrors = 0; for (let i = 0; i < numberOfExecutionBuckets; i++) { const mappedBucketIndex = Math.floor(i * alertResultsPerExecution); const allConditionsFiredInMappedBucket = alertResults.every( (alertResult) => alertResult[group].shouldFire[mappedBucketIndex] ); + const someConditionsNoDataInMappedBucket = alertResults.some((alertResult) => { + const hasNoData = alertResult[group].isNoData as boolean[]; + return hasNoData[mappedBucketIndex]; + }); + const someConditionsErrorInMappedBucket = alertResults.some((alertResult) => { + return alertResult[group].isError; + }); if (allConditionsFiredInMappedBucket) numberOfTimesFired++; + if (someConditionsNoDataInMappedBucket) numberOfNoDataResults++; + if (someConditionsErrorInMappedBucket) numberOfErrors++; } - return numberOfTimesFired; + return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors]; }) ); return previewResults; @@ -152,9 +154,9 @@ export const previewMetricThresholdAlert: ( // so filter these results out entirely and only regard the resultA portion .filter((value) => typeof value !== 'undefined') .reduce((a, b) => { - if (typeof a !== 'number') return a; - if (typeof b !== 'number') return b; - return a + b; + if (!a) return b; + if (!b) return a; + return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]; }) ); return zippedResult as any; diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts index 8a3e9e4d0bedc..5594323d706de 100644 --- a/x-pack/plugins/infra/server/routes/alerting/preview.ts +++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts @@ -55,10 +55,13 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) const numberOfGroups = previewResult.length; const resultTotals = previewResult.reduce( - (totals, groupResult) => { - if (groupResult === null) return { ...totals, noData: totals.noData + 1 }; - if (isNaN(groupResult)) return { ...totals, error: totals.error + 1 }; - return { ...totals, fired: totals.fired + groupResult }; + (totals, [firedResult, noDataResult, errorResult]) => { + return { + ...totals, + fired: totals.fired + firedResult, + noData: totals.noData + noDataResult, + error: totals.error + errorResult, + }; }, { fired: 0, @@ -66,7 +69,6 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) error: 0, } ); - return response.ok({ body: alertPreviewSuccessResponsePayloadRT.encode({ numberOfGroups, @@ -86,10 +88,13 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) const numberOfGroups = previewResult.length; const resultTotals = previewResult.reduce( - (totals, groupResult) => { - if (groupResult === null) return { ...totals, noData: totals.noData + 1 }; - if (isNaN(groupResult)) return { ...totals, error: totals.error + 1 }; - return { ...totals, fired: totals.fired + groupResult }; + (totals, [firedResult, noDataResult, errorResult]) => { + return { + ...totals, + fired: totals.fired + firedResult, + noData: totals.noData + noDataResult, + error: totals.error + errorResult, + }; }, { fired: 0, From 877f2e082971ecd855acbf70039bcd39ba7a85a8 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Thu, 30 Jul 2020 17:05:43 -0400 Subject: [PATCH 18/33] [Security Solution][Exceptions] Adds autocomplete workaround for .text fields (#73761) ## Summary This PR provides a workaround for the autocomplete service not providing suggestions when the selected field is `someField.text`. As endpoint exceptions will be largely using `.text` for now, wanted to still provide the autocomplete service. Updates to the autocomplete components were done after seeing some React errors that were popping up related to memory leaks. This is due to the use of `debounce` I believe. The calls were still executed even after the builder component was unmounted. This resulted in the subsequent calls from the autocomplete service not always going through (sometimes being canceled) when reopening the builder. Moved the filtering of endpoint fields to occur in the existing helper function so that we would still have access to the corresponding `keyword` field of `text` fields when formatting the entries for the builder. --- .../autocomplete/field_value_match.test.tsx | 1 - .../autocomplete/field_value_match.tsx | 15 +- .../field_value_match_any.test.tsx | 1 - .../autocomplete/field_value_match_any.tsx | 15 +- .../use_field_value_autocomplete.test.ts | 17 +- .../hooks/use_field_value_autocomplete.ts | 92 +++-- .../builder/builder_entry_item.test.tsx | 70 ++++ .../exceptions/builder/builder_entry_item.tsx | 16 +- .../builder/builder_exception_item.test.tsx | 24 +- .../exceptions/builder/helpers.test.tsx | 343 +++++++++++++++--- .../components/exceptions/builder/helpers.tsx | 70 +++- .../components/exceptions/builder/index.tsx | 17 +- .../common/components/exceptions/types.ts | 1 + 13 files changed, 540 insertions(+), 142 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx index 72467a62f57c1..998ed1f3351c8 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx @@ -232,7 +232,6 @@ describe('AutocompleteFieldMatchComponent', () => { fields, }, value: 'value 1', - signal: new AbortController().signal, }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx index 137f6803dc54e..dfb3761bb3497 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx @@ -72,14 +72,13 @@ export const AutocompleteFieldMatchComponent: React.FC { - const signal = new AbortController().signal; - - updateSuggestions({ - fieldSelected: selectedField, - value: `${searchVal}`, - patterns: indexPattern, - signal, - }); + if (updateSuggestions != null) { + updateSuggestions({ + fieldSelected: selectedField, + value: searchVal, + patterns: indexPattern, + }); + } }; const isValid = useMemo( diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx index f3f0f2e2a44b1..0a0281a9c4a51 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx @@ -232,7 +232,6 @@ describe('AutocompleteFieldMatchAnyComponent', () => { fields, }, value: 'value 1', - signal: new AbortController().signal, }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx index 5a15c1f7238de..1952ef865e045 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx @@ -65,14 +65,13 @@ export const AutocompleteFieldMatchAnyComponent: React.FC { - const signal = new AbortController().signal; - - updateSuggestions({ - fieldSelected: selectedField, - value: `${searchVal}`, - patterns: indexPattern, - signal, - }); + if (updateSuggestions != null) { + updateSuggestions({ + fieldSelected: selectedField, + value: searchVal, + patterns: indexPattern, + }); + } }; const onCreateOption = (option: string) => onChange([...(selectedValue || []), option]); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts index def2a303f6038..a76b50d11a875 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts @@ -199,12 +199,17 @@ describe('useFieldValueAutocomplete', () => { await waitForNextUpdate(); await waitForNextUpdate(); - result.current[2]({ - fieldSelected: getField('@tags'), - value: 'hello', - patterns: stubIndexPatternWithFields, - signal: new AbortController().signal, - }); + expect(result.current[2]).not.toBeNull(); + + // Added check for typescripts sake, if null, + // would not reach below logic as test would stop above + if (result.current[2] != null) { + result.current[2]({ + fieldSelected: getField('@tags'), + value: 'hello', + patterns: stubIndexPatternWithFields, + }); + } await waitForNextUpdate(); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts index 541c0a8d3fbae..a53914da93f27 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts @@ -11,16 +11,13 @@ import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/d import { useKibana } from '../../../../common/lib/kibana'; import { OperatorTypeEnum } from '../../../../lists_plugin_deps'; -export type UseFieldValueAutocompleteReturn = [ - boolean, - string[], - (args: { - fieldSelected: IFieldType | undefined; - value: string | string[] | undefined; - patterns: IIndexPattern | undefined; - signal: AbortSignal; - }) => void -]; +type Func = (args: { + fieldSelected: IFieldType | undefined; + value: string | string[] | undefined; + patterns: IIndexPattern | undefined; +}) => void; + +export type UseFieldValueAutocompleteReturn = [boolean, string[], Func | null]; export interface UseFieldValueAutocompleteProps { selectedField: IFieldType | undefined; @@ -41,62 +38,77 @@ export const useFieldValueAutocomplete = ({ const { services } = useKibana(); const [isLoading, setIsLoading] = useState(false); const [suggestions, setSuggestions] = useState([]); - const updateSuggestions = useRef( - debounce( + const updateSuggestions = useRef(null); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + + const fetchSuggestions = debounce( async ({ fieldSelected, value, patterns, - signal, }: { fieldSelected: IFieldType | undefined; value: string | string[] | undefined; patterns: IIndexPattern | undefined; - signal: AbortSignal; }) => { - if (fieldSelected == null || patterns == null) { - return; - } + const inputValue: string | string[] = value ?? ''; + const userSuggestion: string = Array.isArray(inputValue) + ? inputValue[inputValue.length - 1] ?? '' + : inputValue; - setIsLoading(true); + try { + if (isSubscribed) { + if (fieldSelected == null || patterns == null) { + return; + } - // Fields of type boolean should only display two options - if (fieldSelected.type === 'boolean') { - setIsLoading(false); - setSuggestions(['true', 'false']); - return; - } + setIsLoading(true); - const newSuggestions = await services.data.autocomplete.getValueSuggestions({ - indexPattern: patterns, - field: fieldSelected, - query: '', - signal, - }); + // Fields of type boolean should only display two options + if (fieldSelected.type === 'boolean') { + setIsLoading(false); + setSuggestions(['true', 'false']); + return; + } - setIsLoading(false); - setSuggestions(newSuggestions); + const newSuggestions = await services.data.autocomplete.getValueSuggestions({ + indexPattern: patterns, + field: fieldSelected, + query: userSuggestion.trim(), + signal: abortCtrl.signal, + }); + + setIsLoading(false); + setSuggestions([...newSuggestions]); + } + } catch (error) { + if (isSubscribed) { + setSuggestions([]); + setIsLoading(false); + } + } }, 500 - ) - ); - - useEffect(() => { - const abortCtrl = new AbortController(); + ); if (operatorType !== OperatorTypeEnum.EXISTS) { - updateSuggestions.current({ + fetchSuggestions({ fieldSelected: selectedField, value: fieldValue, patterns: indexPattern, - signal: abortCtrl.signal, }); } + updateSuggestions.current = fetchSuggestions; + return (): void => { + isSubscribed = false; abortCtrl.abort(); }; - }, [updateSuggestions, selectedField, operatorType, fieldValue, indexPattern]); + }, [services.data.autocomplete, selectedField, operatorType, fieldValue, indexPattern]); return [isLoading, suggestions, updateSuggestions.current]; }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx index 3dcc3eb5a8329..0f54ec29cc540 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx @@ -55,6 +55,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -81,6 +82,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -111,6 +113,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -143,6 +146,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -175,6 +179,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -207,6 +212,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -239,6 +245,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -271,6 +278,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -306,6 +314,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -331,6 +340,62 @@ describe('BuilderEntryItem', () => { ).toBeTruthy(); }); + test('it uses "correspondingKeywordField" if it exists', () => { + const wrapper = mount( + + ); + + expect( + wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatchAny"]').prop('selectedField') + ).toEqual({ + name: 'extension', + type: 'string', + esTypes: ['keyword'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }); + }); + test('it invokes "onChange" when new field is selected and resets operator and value fields', () => { const mockOnChange = jest.fn(); const wrapper = mount( @@ -342,6 +407,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -376,6 +442,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -410,6 +477,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -444,6 +512,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -478,6 +547,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx index 5939a5a1b576e..3883a2fad2cf2 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx @@ -95,7 +95,7 @@ export const BuilderEntryItem: React.FC = ({ const renderFieldInput = useCallback( (isFirst: boolean): JSX.Element => { - const filteredIndexPatterns = getFilteredIndexPatterns(indexPattern, entry); + const filteredIndexPatterns = getFilteredIndexPatterns(indexPattern, entry, listType); const comboBox = ( = ({ return comboBox; } }, - [handleFieldChange, indexPattern, entry] + [handleFieldChange, indexPattern, entry, listType] ); const renderOperatorInput = (isFirst: boolean): JSX.Element => { @@ -170,7 +170,11 @@ export const BuilderEntryItem: React.FC = ({ return ( = ({ return ( { + const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']); + + beforeAll(() => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + data: { + autocomplete: { + getValueSuggestions: getValueSuggestionsMock, + }, + }, + }, + }); + }); + + afterEach(() => { + getValueSuggestionsMock.mockClear(); + }); + describe('and badge logic', () => { test('it renders "and" badge with extra top padding for the first exception item when "andLogicIncluded" is "true"', () => { const exceptionItem = { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx index 17c94adf42648..e8a5196a418d6 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx @@ -41,6 +41,7 @@ import { getOperatorOptions, getUpdatedEntriesOnDelete, isEntryNested, + getCorrespondingKeywordField, } from './helpers'; import { OperatorOption } from '../../autocomplete/types'; @@ -57,6 +58,7 @@ const getMockBuilderEntry = (): FormattedBuilderEntry => ({ nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }); const getMockNestedBuilderEntry = (): FormattedBuilderEntry => ({ @@ -73,6 +75,7 @@ const getMockNestedBuilderEntry = (): FormattedBuilderEntry => ({ parentIndex: 0, }, entryIndex: 0, + correspondingKeywordField: undefined, }); const getMockNestedParentBuilderEntry = (): FormattedBuilderEntry => ({ @@ -82,69 +85,305 @@ const getMockNestedParentBuilderEntry = (): FormattedBuilderEntry => ({ nested: 'parent', parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }); +const mockEndpointFields = [ + { + name: 'file.path.text', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + name: 'file.Ext.code_signature.status', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + subType: { nested: { path: 'file.Ext.code_signature' } }, + }, +]; + +export const getEndpointField = (name: string) => + mockEndpointFields.find((field) => field.name === name) as IFieldType; + describe('Exception builder helpers', () => { + describe('#getCorrespondingKeywordField', () => { + test('it returns matching keyword field if "selectedFieldIsTextType" is true and keyword field exists', () => { + const output = getCorrespondingKeywordField({ + fields, + selectedField: 'machine.os.raw.text', + }); + + expect(output).toEqual(getField('machine.os.raw')); + }); + + test('it returns undefined if "selectedFieldIsTextType" is false', () => { + const output = getCorrespondingKeywordField({ + fields, + selectedField: 'machine.os.raw', + }); + + expect(output).toEqual(undefined); + }); + + test('it returns undefined if "selectedField" is empty string', () => { + const output = getCorrespondingKeywordField({ + fields, + selectedField: '', + }); + + expect(output).toEqual(undefined); + }); + + test('it returns undefined if "selectedField" is undefined', () => { + const output = getCorrespondingKeywordField({ + fields, + selectedField: undefined, + }); + + expect(output).toEqual(undefined); + }); + }); + describe('#getFilteredIndexPatterns', () => { - test('it returns nested fields that match parent value when "item.nested" is "child"', () => { - const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); - const payloadItem: FormattedBuilderEntry = getMockNestedBuilderEntry(); - const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem); - const expected: IIndexPattern = { - fields: [ - { ...getField('nestedField.child') }, - { ...getField('nestedField.nestedChild.doublyNestedChild') }, - ], - id: '1234', - title: 'logstash-*', - }; - expect(output).toEqual(expected); + describe('list type detections', () => { + test('it returns nested fields that match parent value when "item.nested" is "child"', () => { + const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); + const payloadItem: FormattedBuilderEntry = getMockNestedBuilderEntry(); + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'detection'); + const expected: IIndexPattern = { + fields: [ + { ...getField('nestedField.child') }, + { ...getField('nestedField.nestedChild.doublyNestedChild') }, + ], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns only parent nested field when "item.nested" is "parent" and nested parent field is not undefined', () => { + const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); + const payloadItem: FormattedBuilderEntry = getMockNestedParentBuilderEntry(); + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'detection'); + const expected: IIndexPattern = { + fields: [{ ...getField('nestedField.child'), name: 'nestedField', esTypes: ['nested'] }], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns only nested fields when "item.nested" is "parent" and nested parent field is undefined', () => { + const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); + const payloadItem: FormattedBuilderEntry = { + ...getMockNestedParentBuilderEntry(), + field: undefined, + }; + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'detection'); + const expected: IIndexPattern = { + fields: [ + { ...getField('nestedField.child') }, + { ...getField('nestedField.nestedChild.doublyNestedChild') }, + ], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns all fields unfiletered if "item.nested" is not "child" or "parent"', () => { + const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); + const payloadItem: FormattedBuilderEntry = getMockBuilderEntry(); + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'detection'); + const expected: IIndexPattern = { + fields: [...fields], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); }); - test('it returns only parent nested field when "item.nested" is "parent" and nested parent field is not undefined', () => { - const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); - const payloadItem: FormattedBuilderEntry = getMockNestedParentBuilderEntry(); - const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem); - const expected: IIndexPattern = { - fields: [{ ...getField('nestedField.child'), name: 'nestedField', esTypes: ['nested'] }], - id: '1234', - title: 'logstash-*', - }; - expect(output).toEqual(expected); + describe('list type endpoint', () => { + let payloadIndexPattern: IIndexPattern = getMockIndexPattern(); + + beforeAll(() => { + payloadIndexPattern = { + ...payloadIndexPattern, + fields: [...payloadIndexPattern.fields, ...mockEndpointFields], + }; + }); + + test('it returns nested fields that match parent value when "item.nested" is "child"', () => { + const payloadItem: FormattedBuilderEntry = { + field: getEndpointField('file.Ext.code_signature.status'), + operator: isOperator, + value: 'some value', + nested: 'child', + parent: { + parent: { + ...getEntryNestedMock(), + field: 'file.Ext.code_signature', + entries: [{ ...getEntryMatchMock(), field: 'child' }], + }, + parentIndex: 0, + }, + entryIndex: 0, + correspondingKeywordField: undefined, + }; + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'endpoint'); + const expected: IIndexPattern = { + fields: [getEndpointField('file.Ext.code_signature.status')], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns only parent nested field when "item.nested" is "parent" and nested parent field is not undefined', () => { + const payloadItem: FormattedBuilderEntry = { + ...getMockNestedParentBuilderEntry(), + field: { + ...getEndpointField('file.Ext.code_signature.status'), + name: 'file.Ext.code_signature', + esTypes: ['nested'], + }, + }; + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'endpoint'); + const expected: IIndexPattern = { + fields: [ + { + aggregatable: false, + count: 0, + esTypes: ['nested'], + name: 'file.Ext.code_signature', + readFromDocValues: false, + scripted: false, + searchable: true, + subType: { + nested: { + path: 'file.Ext.code_signature', + }, + }, + type: 'string', + }, + ], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns only nested fields when "item.nested" is "parent" and nested parent field is undefined', () => { + const payloadItem: FormattedBuilderEntry = { + ...getMockNestedParentBuilderEntry(), + field: undefined, + }; + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'endpoint'); + const expected: IIndexPattern = { + fields: [getEndpointField('file.Ext.code_signature.status')], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns all fields that matched those in "exceptionable_fields.json" with no further filtering if "item.nested" is not "child" or "parent"', () => { + const payloadItem: FormattedBuilderEntry = getMockBuilderEntry(); + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'endpoint'); + const expected: IIndexPattern = { + fields: [ + { + aggregatable: false, + count: 0, + esTypes: ['text'], + name: 'file.path.text', + readFromDocValues: false, + scripted: false, + searchable: true, + type: 'string', + }, + { + name: 'file.Ext.code_signature.status', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + subType: { nested: { path: 'file.Ext.code_signature' } }, + }, + ], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); }); + }); - test('it returns only nested fields when "item.nested" is "parent" and nested parent field is undefined', () => { - const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); - const payloadItem: FormattedBuilderEntry = { - ...getMockNestedParentBuilderEntry(), - field: undefined, - }; - const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem); - const expected: IIndexPattern = { + describe('#getFormattedBuilderEntry', () => { + test('it returns entry with a value for "correspondingKeywordField" when "item.field" is of type "text" and matching keyword field exists', () => { + const payloadIndexPattern: IIndexPattern = { + ...getMockIndexPattern(), fields: [ - { ...getField('nestedField.child') }, - { ...getField('nestedField.nestedChild.doublyNestedChild') }, + ...fields, + { + name: 'machine.os.raw.text', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: true, + }, ], - id: '1234', - title: 'logstash-*', }; - expect(output).toEqual(expected); - }); - - test('it returns all fields unfiletered if "item.nested" is not "child" or "parent"', () => { - const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); - const payloadItem: FormattedBuilderEntry = getMockBuilderEntry(); - const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem); - const expected: IIndexPattern = { - fields: [...fields], - id: '1234', - title: 'logstash-*', + const payloadItem: BuilderEntry = { + ...getEntryMatchMock(), + field: 'machine.os.raw.text', + value: 'some os', + }; + const output = getFormattedBuilderEntry( + payloadIndexPattern, + payloadItem, + 0, + undefined, + undefined + ); + const expected: FormattedBuilderEntry = { + entryIndex: 0, + field: { + name: 'machine.os.raw.text', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: true, + }, + nested: undefined, + operator: isOperator, + parent: undefined, + value: 'some os', + correspondingKeywordField: getField('machine.os.raw'), }; expect(output).toEqual(expected); }); - }); - describe('#getFormattedBuilderEntry', () => { test('it returns "FormattedBuilderEntry" with value "nested" of "child" when "parent" and "parentIndex" are defined', () => { const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); const payloadItem: BuilderEntry = { ...getEntryMatchMock(), field: 'child' }; @@ -188,6 +427,7 @@ describe('Exception builder helpers', () => { parentIndex: 1, }, value: 'some host name', + correspondingKeywordField: undefined, }; expect(output).toEqual(expected); }); @@ -218,6 +458,7 @@ describe('Exception builder helpers', () => { operator: isOperator, parent: undefined, value: 'some ip', + correspondingKeywordField: undefined, }; expect(output).toEqual(expected); }); @@ -252,6 +493,7 @@ describe('Exception builder helpers', () => { operator: isOperator, parent: undefined, value: 'some host name', + correspondingKeywordField: undefined, }, ]; expect(output).toEqual(expected); @@ -281,6 +523,7 @@ describe('Exception builder helpers', () => { operator: isOperator, parent: undefined, value: 'some ip', + correspondingKeywordField: undefined, }, { entryIndex: 1, @@ -298,6 +541,7 @@ describe('Exception builder helpers', () => { operator: isOneOfOperator, parent: undefined, value: ['some extension'], + correspondingKeywordField: undefined, }, ]; expect(output).toEqual(expected); @@ -333,6 +577,7 @@ describe('Exception builder helpers', () => { operator: isOperator, parent: undefined, value: 'some ip', + correspondingKeywordField: undefined, }, { entryIndex: 1, @@ -347,6 +592,7 @@ describe('Exception builder helpers', () => { operator: isOperator, parent: undefined, value: undefined, + correspondingKeywordField: undefined, }, { entryIndex: 0, @@ -383,6 +629,7 @@ describe('Exception builder helpers', () => { parentIndex: 1, }, value: 'some host name', + correspondingKeywordField: undefined, }, ]; expect(output).toEqual(expected); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx index 93bae091885c1..8585f58504e31 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx @@ -33,6 +33,8 @@ import { EmptyNestedEntry, } from '../types'; import { getEntryValue, getExceptionOperatorSelect } from '../helpers'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import exceptionableFields from '../exceptionable_fields.json'; /** * Returns filtered index patterns based on the field - if a user selects to @@ -45,13 +47,21 @@ import { getEntryValue, getExceptionOperatorSelect } from '../helpers'; */ export const getFilteredIndexPatterns = ( patterns: IIndexPattern, - item: FormattedBuilderEntry + item: FormattedBuilderEntry, + type: ExceptionListType ): IIndexPattern => { + const indexPatterns = { + ...patterns, + fields: patterns.fields.filter(({ name }) => + type === 'endpoint' ? exceptionableFields.includes(name) : true + ), + }; + if (item.nested === 'child' && item.parent != null) { // when user has selected a nested entry, only fields with the common parent are shown return { - ...patterns, - fields: patterns.fields.filter( + ...indexPatterns, + fields: indexPatterns.fields.filter( (field) => field.subType != null && field.subType.nested != null && @@ -61,20 +71,53 @@ export const getFilteredIndexPatterns = ( }; } else if (item.nested === 'parent' && item.field != null) { // when user has selected a nested entry, right above it we show the common parent - return { ...patterns, fields: [item.field] }; + return { ...indexPatterns, fields: [item.field] }; } else if (item.nested === 'parent' && item.field == null) { // when user selects to add a nested entry, only nested fields are shown as options return { - ...patterns, - fields: patterns.fields.filter( + ...indexPatterns, + fields: indexPatterns.fields.filter( (field) => field.subType != null && field.subType.nested != null ), }; } else { - return patterns; + return indexPatterns; } }; +/** + * Fields of type 'text' do not generate autocomplete values, we want + * to find it's corresponding keyword type (if available) which does + * generate autocomplete values + * + * @param fields IFieldType fields + * @param selectedField the field name that was selected + * @param isTextType we only want a corresponding keyword field if + * the selected field is of type 'text' + * + */ +export const getCorrespondingKeywordField = ({ + fields, + selectedField, +}: { + fields: IFieldType[]; + selectedField: string | undefined; +}): IFieldType | undefined => { + const selectedFieldBits = + selectedField != null && selectedField !== '' ? selectedField.split('.') : []; + const selectedFieldIsTextType = selectedFieldBits.slice(-1)[0] === 'text'; + + if (selectedFieldIsTextType && selectedFieldBits.length > 0) { + const keywordField = selectedFieldBits.slice(0, selectedFieldBits.length - 1).join('.'); + const [foundKeywordField] = fields.filter( + ({ name }) => keywordField !== '' && keywordField === name + ); + return foundKeywordField; + } + + return undefined; +}; + /** * Formats the entry into one that is easily usable for the UI, most of the * complexity was introduced with nested fields @@ -95,11 +138,16 @@ export const getFormattedBuilderEntry = ( ): FormattedBuilderEntry => { const { fields } = indexPattern; const field = parent != null ? `${parent.field}.${item.field}` : item.field; - const [selectedField] = fields.filter(({ name }) => field != null && field === name); + const [foundField] = fields.filter(({ name }) => field != null && field === name); + const correspondingKeywordField = getCorrespondingKeywordField({ + fields, + selectedField: field, + }); if (parent != null && parentIndex != null) { return { - field: selectedField, + field: foundField, + correspondingKeywordField, operator: getExceptionOperatorSelect(item), value: getEntryValue(item), nested: 'child', @@ -108,7 +156,8 @@ export const getFormattedBuilderEntry = ( }; } else { return { - field: selectedField, + field: foundField, + correspondingKeywordField, operator: getExceptionOperatorSelect(item), value: getEntryValue(item), nested: undefined, @@ -167,6 +216,7 @@ export const getFormattedBuilderEntries = ( value: undefined, entryIndex: index, parent: undefined, + correspondingKeywordField: undefined, }; // User has selected to add a nested field, but not yet selected the field diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx index 734434484fb4c..b82607a541aaa 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.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, useEffect, useMemo, useReducer } from 'react'; +import React, { useCallback, useEffect, useReducer } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; @@ -29,8 +29,6 @@ import { getDefaultEmptyEntry, getDefaultNestedEmptyEntry, } from './helpers'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import exceptionableFields from '../exceptionable_fields.json'; const MyInvisibleAndBadge = styled(EuiFlexItem)` visibility: hidden; @@ -244,17 +242,6 @@ export const ExceptionBuilder = ({ setUpdateExceptions([...exceptions, { ...newException }]); }, [setUpdateExceptions, exceptions, listType, listId, listNamespaceType, ruleName]); - // Filters index pattern fields by exceptionable fields if list type is endpoint - const filterIndexPatterns = useMemo((): IIndexPattern => { - if (listType === 'endpoint') { - return { - ...indexPatterns, - fields: indexPatterns.fields.filter(({ name }) => exceptionableFields.includes(name)), - }; - } - return indexPatterns; - }, [indexPatterns, listType]); - // The builder can have existing exception items, or new exception items that have yet // to be created (and thus lack an id), this was creating some React bugs with relying // on the index, as a result, created a temporary id when new exception items are first @@ -368,7 +355,7 @@ export const ExceptionBuilder = ({ key={getExceptionListItemId(exceptionListItem, index)} exceptionItem={exceptionListItem} exceptionId={getExceptionListItemId(exceptionListItem, index)} - indexPattern={filterIndexPatterns} + indexPattern={indexPatterns} listType={listType} addNested={addNested} exceptionItemIndex={index} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts index 83367e5b9e739..9b7c68848c41f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts @@ -62,6 +62,7 @@ export interface FormattedBuilderEntry { nested: 'parent' | 'child' | undefined; entryIndex: number; parent: { parent: EntryNested; parentIndex: number } | undefined; + correspondingKeywordField: IFieldType | undefined; } export interface EmptyEntry { From 744afcef7d9d5438485640a01588399d15e4b272 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Thu, 30 Jul 2020 17:22:10 -0400 Subject: [PATCH 19/33] [Security Solution][Exceptions] - Update how nested entries are displayed in exceptions viewer (#73745) ### Summary Small PR updating how nested entries are displayed in exception viewer. Doesn't change anything functionally, just cosmetic. --- .../viewer/exception_item/exception_entries.tsx | 16 ++++++++++++---- .../exceptions/viewer/helpers.test.tsx | 8 ++++---- .../components/exceptions/viewer/helpers.tsx | 5 +---- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx index 7069e99943f7b..bcc4adf54c9d7 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx @@ -52,6 +52,14 @@ const MyActionButton = styled(EuiFlexItem)` align-self: flex-end; `; +const MyNestedValueContainer = styled.div` + margin-left: ${({ theme }) => theme.eui.euiSizeL}; +`; + +const MyNestedValue = styled.span` + margin-left: ${({ theme }) => theme.eui.euiSizeS}; +`; + interface ExceptionEntriesComponentProps { entries: FormattedEntry[]; disableDelete: boolean; @@ -78,10 +86,10 @@ const ExceptionEntriesComponent = ({ render: (value: string | null, data: FormattedEntry) => { if (value != null && data.isNested) { return ( - <> - - {value} - + + + {value} + ); } else { return value ?? getEmptyValue(); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx index 5d4340db9a448..5f6e54b0d3cff 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx @@ -106,13 +106,13 @@ describe('Exception viewer helpers', () => { value: undefined, }, { - fieldName: 'host.name.host.name', + fieldName: 'host.name', isNested: true, operator: 'is', value: 'some host name', }, { - fieldName: 'host.name.host.name', + fieldName: 'host.name', isNested: true, operator: 'is one of', value: ['some host name'], @@ -138,9 +138,9 @@ describe('Exception viewer helpers', () => { test('it formats as expected when "isNested" is "true"', () => { const payload = getEntryMatchMock(); - const formattedEntry = formatEntry({ isNested: true, parent: 'parent', item: payload }); + const formattedEntry = formatEntry({ isNested: true, item: payload }); const expected: FormattedEntry = { - fieldName: 'parent.host.name', + fieldName: 'host.name', isNested: true, operator: 'is', value: 'some host name', diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx index 345db5bf1e75e..86b0512410e6f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx @@ -20,18 +20,16 @@ import * as i18n from '../translations'; */ export const formatEntry = ({ isNested, - parent, item, }: { isNested: boolean; - parent?: string; item: BuilderEntry; }): FormattedEntry => { const operator = getExceptionOperatorSelect(item); const value = getEntryValue(item); return { - fieldName: isNested ? `${parent}.${item.field}` : item.field ?? '', + fieldName: item.field ?? '', operator: operator.message, value, isNested, @@ -57,7 +55,6 @@ export const getFormattedEntries = (entries: BuilderEntry[]): FormattedEntry[] = (acc, nestedEntry) => { const formattedEntry = formatEntry({ isNested: true, - parent: item.field, item: nestedEntry, }); return [...acc, { ...formattedEntry }]; From 14355ab14b75e65ec3d597e2af54a3a93573385a Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Thu, 30 Jul 2020 17:44:03 -0400 Subject: [PATCH 20/33] [Security Solution][Telemetry] Concurrent telemetry requests (#73558) --- .../server/usage/collector.ts | 11 ++- .../usage/detections/detections_helpers.ts | 10 ++- .../server/usage/detections/index.ts | 24 ++++- .../server/usage/endpoints/endpoint.mocks.ts | 9 +- .../usage/endpoints/fleet_saved_objects.ts | 2 + .../server/usage/endpoints/index.ts | 90 +++++++++++-------- 6 files changed, 94 insertions(+), 52 deletions(-) diff --git a/x-pack/plugins/security_solution/server/usage/collector.ts b/x-pack/plugins/security_solution/server/usage/collector.ts index 9a7ad6fc2db74..6fadf956ccaf1 100644 --- a/x-pack/plugins/security_solution/server/usage/collector.ts +++ b/x-pack/plugins/security_solution/server/usage/collector.ts @@ -6,7 +6,7 @@ import { LegacyAPICaller, CoreSetup } from '../../../../../src/core/server'; import { CollectorDependencies } from './types'; -import { DetectionsUsage, fetchDetectionsUsage } from './detections'; +import { DetectionsUsage, fetchDetectionsUsage, defaultDetectionsUsage } from './detections'; import { EndpointUsage, getEndpointTelemetryFromFleet } from './endpoints'; export type RegisterCollector = (deps: CollectorDependencies) => void; @@ -76,9 +76,14 @@ export const registerCollector: RegisterCollector = ({ isReady: () => kibanaIndex.length > 0, fetch: async (callCluster: LegacyAPICaller): Promise => { const savedObjectsClient = await getInternalSavedObjectsClient(core); + const [detections, endpoints] = await Promise.allSettled([ + fetchDetectionsUsage(kibanaIndex, callCluster, ml), + getEndpointTelemetryFromFleet(savedObjectsClient), + ]); + return { - detections: await fetchDetectionsUsage(kibanaIndex, callCluster, ml), - endpoints: await getEndpointTelemetryFromFleet(savedObjectsClient), + detections: detections.status === 'fulfilled' ? detections.value : defaultDetectionsUsage, + endpoints: endpoints.status === 'fulfilled' ? endpoints.value : {}, }; }, }); diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts index f9905c373291c..80a9dba26df8e 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts @@ -23,7 +23,10 @@ interface DetectionsMetric { const isElasticRule = (tags: string[]) => tags.includes(`${INTERNAL_IMMUTABLE_KEY}:true`); -const initialRulesUsage: DetectionRulesUsage = { +/** + * Default detection rule usage count + */ +export const initialRulesUsage: DetectionRulesUsage = { custom: { enabled: 0, disabled: 0, @@ -34,7 +37,10 @@ const initialRulesUsage: DetectionRulesUsage = { }, }; -const initialMlJobsUsage: MlJobsUsage = { +/** + * Default ml job usage count + */ +export const initialMlJobsUsage: MlJobsUsage = { custom: { enabled: 0, disabled: 0, diff --git a/x-pack/plugins/security_solution/server/usage/detections/index.ts b/x-pack/plugins/security_solution/server/usage/detections/index.ts index dd50e79e22cc9..a366c86299b91 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/index.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/index.ts @@ -5,7 +5,12 @@ */ import { LegacyAPICaller } from '../../../../../../src/core/server'; -import { getMlJobsUsage, getRulesUsage } from './detections_helpers'; +import { + getMlJobsUsage, + getRulesUsage, + initialRulesUsage, + initialMlJobsUsage, +} from './detections_helpers'; import { MlPluginSetup } from '../../../../ml/server'; interface FeatureUsage { @@ -28,12 +33,23 @@ export interface DetectionsUsage { ml_jobs: MlJobsUsage; } +export const defaultDetectionsUsage = { + detection_rules: initialRulesUsage, + ml_jobs: initialMlJobsUsage, +}; + export const fetchDetectionsUsage = async ( kibanaIndex: string, callCluster: LegacyAPICaller, ml: MlPluginSetup | undefined ): Promise => { - const rulesUsage = await getRulesUsage(kibanaIndex, callCluster); - const mlJobsUsage = await getMlJobsUsage(ml); - return { detection_rules: rulesUsage, ml_jobs: mlJobsUsage }; + const [rulesUsage, mlJobsUsage] = await Promise.allSettled([ + getRulesUsage(kibanaIndex, callCluster), + getMlJobsUsage(ml), + ]); + + return { + detection_rules: rulesUsage.status === 'fulfilled' ? rulesUsage.value : initialRulesUsage, + ml_jobs: mlJobsUsage.status === 'fulfilled' ? mlJobsUsage.value : initialMlJobsUsage, + }; }; diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts b/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts index e3f0f7bde2fed..d753eeee93594 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts @@ -15,6 +15,7 @@ import { FLEET_ENDPOINT_PACKAGE_CONSTANT } from './fleet_saved_objects'; const testAgentId = 'testAgentId'; const testConfigId = 'testConfigId'; const testHostId = 'randoHostId'; +const testHostName = 'testDesktop'; /** Mock OS Platform for endpoint telemetry */ export const MockOSPlatform = 'somePlatform'; @@ -56,8 +57,8 @@ export const mockFleetObjectsResponse = ( }, }, host: { - hostname: 'testDesktop', - name: 'testDesktop', + hostname: testHostName, + name: testHostName, id: testHostId, }, os: { @@ -93,8 +94,8 @@ export const mockFleetObjectsResponse = ( }, }, host: { - hostname: 'testDesktop', - name: 'testDesktop', + hostname: hasDuplicates ? testHostName : 'oldRandoHostName', + name: hasDuplicates ? testHostName : 'oldRandoHostName', id: hasDuplicates ? testHostId : 'oldRandoHostId', }, os: { diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts b/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts index 42c1ec0e2eed2..c46610ec9388e 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts @@ -23,6 +23,8 @@ export const getFleetSavedObjectsMetadata = async (savedObjectsClient: ISavedObj 'last_checkin', 'local_metadata.agent.id', 'local_metadata.host.id', + 'local_metadata.host.name', + 'local_metadata.host.hostname', 'local_metadata.elastic.agent.id', 'local_metadata.os', ], diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/index.ts b/x-pack/plugins/security_solution/server/usage/endpoints/index.ts index 9e071f4adff25..19beda4554d93 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/index.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/index.ts @@ -42,7 +42,9 @@ export interface AgentLocalMetadata extends AgentMetadata { }; }; host: { + hostname: string; id: string; + name: string; }; os: { name: string; @@ -78,17 +80,20 @@ export const updateEndpointOSTelemetry = ( os: AgentLocalMetadata['os'], osTracker: OSTracker ): OSTracker => { - const updatedOSTracker = cloneDeep(osTracker); - const { version: osVersion, platform: osPlatform, full: osFullName } = os; - if (osFullName && osVersion) { - if (updatedOSTracker[osFullName]) updatedOSTracker[osFullName].count += 1; - else { - updatedOSTracker[osFullName] = { - full_name: osFullName, - platform: osPlatform, - version: osVersion, - count: 1, - }; + let updatedOSTracker = osTracker; + if (os && typeof os === 'object') { + updatedOSTracker = cloneDeep(osTracker); + const { version: osVersion, platform: osPlatform, full: osFullName } = os; + if (osFullName && osVersion) { + if (updatedOSTracker[osFullName]) updatedOSTracker[osFullName].count += 1; + else { + updatedOSTracker[osFullName] = { + full_name: osFullName, + platform: osPlatform, + version: osVersion, + count: 1, + }; + } } } @@ -211,46 +216,53 @@ export const getEndpointTelemetryFromFleet = async ( if (!endpointAgents || endpointAgentsCount < 1) return endpointTelemetry; // Use unique hosts to prevent any potential duplicates - const uniqueHostIds: Set = new Set(); + const uniqueHosts: Set = new Set(); let osTracker: OSTracker = {}; let dailyActiveCount = 0; let policyTracker: PoliciesTelemetry = { malware: { active: 0, inactive: 0, failure: 0 } }; for (let i = 0; i < endpointAgentsCount; i += 1) { - const { attributes: metadataAttributes } = endpointAgents[i]; - const { last_checkin: lastCheckin, local_metadata: localMetadata } = metadataAttributes; - const { host, os, elastic } = localMetadata as AgentLocalMetadata; // AgentMetadata is just an empty blob, casting for our use case - - if (!uniqueHostIds.has(host.id)) { - uniqueHostIds.add(host.id); - const agentId = elastic?.agent?.id; - osTracker = updateEndpointOSTelemetry(os, osTracker); - - if (agentId) { - let agentEvents; - try { - const response = await getLatestFleetEndpointEvent(soClient, agentId); - agentEvents = response.saved_objects; - } catch (error) { - // If the request fails we do not obtain `active within last 24 hours for this agent` or policy specifics - } - - // AgentEvents will have a max length of 1 - if (agentEvents && agentEvents.length > 0) { - const latestEndpointEvent = agentEvents[0]; - dailyActiveCount = updateEndpointDailyActiveCount( - latestEndpointEvent, - lastCheckin, - dailyActiveCount + try { + const { attributes: metadataAttributes } = endpointAgents[i]; + const { last_checkin: lastCheckin, local_metadata: localMetadata } = metadataAttributes; + const { host, os, elastic } = localMetadata as AgentLocalMetadata; + + // Although not perfect, the goal is to dedupe hosts to get the most recent data for a host + // An agent re-installed on the same host will have the same id and hostname + // A cloned VM will have the same id, but "may" have the same hostname, but it's really up to the user. + const compoundUniqueId = `${host?.id}-${host?.hostname}`; + if (!uniqueHosts.has(compoundUniqueId)) { + uniqueHosts.add(compoundUniqueId); + const agentId = elastic?.agent?.id; + osTracker = updateEndpointOSTelemetry(os, osTracker); + + if (agentId) { + const { saved_objects: agentEvents } = await getLatestFleetEndpointEvent( + soClient, + agentId ); - policyTracker = updateEndpointPolicyTelemetry(latestEndpointEvent, policyTracker); + + // AgentEvents will have a max length of 1 + if (agentEvents && agentEvents.length > 0) { + const latestEndpointEvent = agentEvents[0]; + dailyActiveCount = updateEndpointDailyActiveCount( + latestEndpointEvent, + lastCheckin, + dailyActiveCount + ); + policyTracker = updateEndpointPolicyTelemetry(latestEndpointEvent, policyTracker); + } } } + } catch (error) { + // All errors thrown in the loop would be handled here + // Not logging any errors to avoid leaking any potential PII + // Depending on when the error is thrown in the loop some specifics may be missing, but it allows the loop to continue } } // All unique hosts with an endpoint installed, thus all unique endpoint installs - endpointTelemetry.total_installed = uniqueHostIds.size; + endpointTelemetry.total_installed = uniqueHosts.size; // Set the daily active count for the endpoints endpointTelemetry.active_within_last_24_hours = dailyActiveCount; // Get the objects to populate our OS Telemetry From 7e2e78b54e71db6925d362dae0fca14c186b06d8 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 30 Jul 2020 15:51:01 -0600 Subject: [PATCH 21/33] [Maps] upgrade turf (#73816) * [Maps] upgrade turf * clean up ts-ignore comments * fix license check and review feedback Co-authored-by: Elastic Machine --- src/dev/license_checker/config.ts | 2 +- x-pack/package.json | 5 +- .../public/actions/data_request_actions.ts | 6 +- .../maps/public/actions/map_actions.ts | 10 +- .../es_pew_pew_source/es_pew_pew_source.js | 5 +- .../public/classes/util/can_skip_fetch.ts | 7 +- .../util/get_feature_collection_bounds.ts | 5 +- .../map/mb/draw_control/draw_circle.ts | 10 +- yarn.lock | 575 +----------------- 9 files changed, 47 insertions(+), 578 deletions(-) diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts index efc42405688d4..60172a3106276 100644 --- a/src/dev/license_checker/config.ts +++ b/src/dev/license_checker/config.ts @@ -79,7 +79,7 @@ export const DEV_ONLY_LICENSE_WHITELIST = ['MPL-2.0']; // Globally overrides a license for a given package@version export const LICENSE_OVERRIDES = { - 'jsts@1.1.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts + 'jsts@1.6.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts '@mapbox/jsonlint-lines-primitives@2.0.2': ['MIT'], // license in readme https://github.com/tmcw/jsonlint // TODO can be removed if the https://github.com/jindw/xmldom/issues/239 is released diff --git a/x-pack/package.json b/x-pack/package.json index 2d7cb148c43b0..e3104aabbb02b 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -218,8 +218,12 @@ "@mapbox/mapbox-gl-rtl-text": "^0.2.3", "@scant/router": "^0.1.0", "@slack/webhook": "^5.0.0", + "@turf/bbox": "6.0.1", + "@turf/bbox-polygon": "6.0.1", "@turf/boolean-contains": "6.0.1", "@turf/circle": "6.0.1", + "@turf/distance": "6.0.1", + "@turf/helpers": "6.0.1", "angular": "^1.7.9", "angular-resource": "1.7.9", "angular-sanitize": "1.7.9", @@ -367,7 +371,6 @@ "tinymath": "1.2.1", "topojson-client": "3.0.0", "tslib": "^2.0.0", - "turf": "3.0.14", "typescript-fsa": "^3.0.0", "typescript-fsa-reducers": "^1.2.1", "ui-select": "0.19.8", diff --git a/x-pack/plugins/maps/public/actions/data_request_actions.ts b/x-pack/plugins/maps/public/actions/data_request_actions.ts index f91e272d625f6..4c829f8e75c20 100644 --- a/x-pack/plugins/maps/public/actions/data_request_actions.ts +++ b/x-pack/plugins/maps/public/actions/data_request_actions.ts @@ -6,8 +6,8 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ import { Dispatch } from 'redux'; -// @ts-ignore -import turf from 'turf'; +import bbox from '@turf/bbox'; +import { multiPoint } from '@turf/helpers'; import { FeatureCollection } from 'geojson'; import { MapStoreState } from '../reducers/store'; import { LAYER_TYPE, SOURCE_DATA_REQUEST_ID } from '../../common/constants'; @@ -368,7 +368,7 @@ export function fitToDataBounds() { return; } - const dataBounds = turfBboxToBounds(turf.bbox(turf.multiPoint(corners))); + const dataBounds = turfBboxToBounds(bbox(multiPoint(corners))); dispatch(setGotoWithBounds(scaleBounds(dataBounds, FIT_TO_BOUNDS_SCALE_FACTOR))); }; diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index f3619fd1bd23e..4914432f02de0 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -6,10 +6,10 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ import { Dispatch } from 'redux'; -// @ts-ignore -import turf from 'turf'; -import uuid from 'uuid/v4'; +import turfBboxPolygon from '@turf/bbox-polygon'; import turfBooleanContains from '@turf/boolean-contains'; +import uuid from 'uuid/v4'; + import { Filter, Query, TimeRange } from 'src/plugins/data/public'; import { MapStoreState } from '../reducers/store'; import { @@ -126,13 +126,13 @@ export function mapExtentChanged(newMapConstants: { zoom: number; extent: MapExt if (extent) { let doesBufferContainExtent = false; if (buffer) { - const bufferGeometry = turf.bboxPolygon([ + const bufferGeometry = turfBboxPolygon([ buffer.minLon, buffer.minLat, buffer.maxLon, buffer.maxLat, ]); - const extentGeometry = turf.bboxPolygon([ + const extentGeometry = turfBboxPolygon([ extent.minLon, extent.minLat, extent.maxLon, diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js index 33d5deef2e39f..79eccf09b2888 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js @@ -6,7 +6,8 @@ import React from 'react'; import uuid from 'uuid/v4'; -import turf from 'turf'; +import turfBbox from '@turf/bbox'; +import { multiPoint } from '@turf/helpers'; import { UpdateSourceEditor } from './update_source_editor'; import { i18n } from '@kbn/i18n'; @@ -216,7 +217,7 @@ export class ESPewPewSource extends AbstractESAggSource { return null; } - return turfBboxToBounds(turf.bbox(turf.multiPoint(corners))); + return turfBboxToBounds(turfBbox(multiPoint(corners))); } canFormatFeatureProperties() { diff --git a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts index 8398bd7af39ad..147870dbef371 100644 --- a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts +++ b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import _ from 'lodash'; -// @ts-ignore -import turf from 'turf'; +import turfBboxPolygon from '@turf/bbox-polygon'; import turfBooleanContains from '@turf/boolean-contains'; import { isRefreshOnlyQuery } from './is_refresh_only_query'; import { ISource } from '../sources/source'; @@ -27,13 +26,13 @@ export function updateDueToExtent(prevMeta: DataMeta = {}, nextMeta: DataMeta = return NO_SOURCE_UPDATE_REQUIRED; } - const previousBufferGeometry = turf.bboxPolygon([ + const previousBufferGeometry = turfBboxPolygon([ previousBuffer.minLon, previousBuffer.minLat, previousBuffer.maxLon, previousBuffer.maxLat, ]); - const newBufferGeometry = turf.bboxPolygon([ + const newBufferGeometry = turfBboxPolygon([ newBuffer.minLon, newBuffer.minLat, newBuffer.maxLon, diff --git a/x-pack/plugins/maps/public/classes/util/get_feature_collection_bounds.ts b/x-pack/plugins/maps/public/classes/util/get_feature_collection_bounds.ts index aa78d7064fb0a..76d305f0162d2 100644 --- a/x-pack/plugins/maps/public/classes/util/get_feature_collection_bounds.ts +++ b/x-pack/plugins/maps/public/classes/util/get_feature_collection_bounds.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore -import turf from 'turf'; +import turfBbox from '@turf/bbox'; import { FeatureCollection } from 'geojson'; import { MapExtent } from '../../../common/descriptor_types'; import { FEATURE_VISIBLE_PROPERTY_NAME } from '../../../common/constants'; @@ -28,7 +27,7 @@ export function getFeatureCollectionBounds( return null; } - const bbox = turf.bbox({ + const bbox = turfBbox({ type: 'FeatureCollection', features: visibleFeatures, }); diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts index f2ceb8685d43e..3e89d67e11504 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts +++ b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts @@ -6,9 +6,9 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -// @ts-ignore -import turf from 'turf'; -// @ts-ignore +// @ts-expect-error +import turfDistance from '@turf/distance'; +// @ts-expect-error import turfCircle from '@turf/circle'; type DrawCircleState = { @@ -75,7 +75,7 @@ export const DrawCircle = { // second click, finish draw // @ts-ignore this.updateUIClasses({ mouse: 'pointer' }); - state.circle.properties.radiusKm = turf.distance(state.circle.properties.center, [ + state.circle.properties.radiusKm = turfDistance(state.circle.properties.center, [ e.lngLat.lng, e.lngLat.lat, ]); @@ -90,7 +90,7 @@ export const DrawCircle = { } const mouseLocation = [e.lngLat.lng, e.lngLat.lat]; - state.circle.properties.radiusKm = turf.distance(state.circle.properties.center, mouseLocation); + state.circle.properties.radiusKm = turfDistance(state.circle.properties.center, mouseLocation); const newCircleFeature = turfCircle( state.circle.properties.center, state.circle.properties.radiusKm diff --git a/yarn.lock b/yarn.lock index 60d073330b35d..4e9f732f1e0a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4092,7 +4092,14 @@ "@testing-library/dom" "^6.3.0" "@types/testing-library__react" "^9.1.0" -"@turf/bbox@6.x": +"@turf/bbox-polygon@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@turf/bbox-polygon/-/bbox-polygon-6.0.1.tgz#ae0fbb14558831fb34538aae089a23d3336c6379" + integrity sha512-f6BK6GOzUNjmJeOYHklk/5LNcQMQbo51gvAM10dTM5IqzKP01KM5bgV88uOKfSZB0HRQVpaRV1tgXk2bg5cPRg== + dependencies: + "@turf/helpers" "6.x" + +"@turf/bbox@6.0.1", "@turf/bbox@6.x": version "6.0.1" resolved "https://registry.yarnpkg.com/@turf/bbox/-/bbox-6.0.1.tgz#b966075771475940ee1c16be2a12cf389e6e923a" integrity sha512-EGgaRLettBG25Iyx7VyUINsPpVj1x3nFQFiGS3ER8KCI1MximzNLsam3eXRabqQDjyAKyAE1bJ4EZEpGvspQxw== @@ -4143,6 +4150,19 @@ "@turf/helpers" "6.x" "@turf/invariant" "6.x" +"@turf/distance@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@turf/distance/-/distance-6.0.1.tgz#0761f28784286e7865a427c4e7e3593569c2dea8" + integrity sha512-q7t7rWIWfkg7MP1Vt4uLjSEhe5rPfCO2JjpKmk7JC+QZKEQkuvHEqy3ejW1iC7Kw5ZcZNR3qdMGGz+6HnVwqvg== + dependencies: + "@turf/helpers" "6.x" + "@turf/invariant" "6.x" + +"@turf/helpers@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-6.0.1.tgz#625112616159e519033dc5d24c094ccbce7a457f" + integrity sha512-EQtcbwiNbkBnvxvlxcTcrwTzaq2eR4fnDVlzx/hsw5tYxDiMJfuL9goy1rDCmXxcyDnk8gCyZapYfEQWYLl1pQ== + "@turf/helpers@6.x": version "6.1.4" resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-6.1.4.tgz#d6fd7ebe6782dd9c87dca5559bda5c48ae4c3836" @@ -6229,13 +6249,6 @@ adm-zip@0.4.11: resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.11.tgz#2aa54c84c4b01a9d0fb89bb11982a51f13e3d62a" integrity sha512-L8vcjDTCOIJk7wFvmlEUN7AsSb8T+2JrdP7KINBjzr24TJ5Mwj590sLu3BC7zNZowvJWa/JtPmD8eJCzdtDWjA== -affine-hull@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/affine-hull/-/affine-hull-1.0.0.tgz#763ff1d38d063ceb7e272f17ee4d7bbcaf905c5d" - integrity sha1-dj/x040GPOt+Jy8X7k17vK+QXF0= - dependencies: - robust-orientation "^1.1.3" - after-all-results@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/after-all-results/-/after-all-results-2.0.0.tgz#6ac2fc202b500f88da8f4f5530cfa100f4c6a2d0" @@ -8216,11 +8229,6 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -bit-twiddle@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bit-twiddle/-/bit-twiddle-1.0.2.tgz#0c6c1fabe2b23d17173d9a61b7b7093eb9e1769e" - integrity sha1-DGwfq+KyPRcXPZpht7cJPrnhdp4= - bl@^1.0.0: version "1.2.1" resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.1.tgz#cac328f7bee45730d404b692203fcb590e172d5e" @@ -10374,15 +10382,6 @@ convert-source-map@^0.3.3: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190" integrity sha1-8dgClQr33SYxof6+BZZVDIarMZA= -convex-hull@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/convex-hull/-/convex-hull-1.0.3.tgz#20a3aa6ce87f4adea2ff7d17971c9fc1c67e1fff" - integrity sha1-IKOqbOh/St6i/30XlxyfwcZ+H/8= - dependencies: - affine-hull "^1.0.0" - incremental-convex-hull "^1.0.1" - monotone-convex-hull-2d "^1.0.1" - cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -12300,11 +12299,6 @@ eachr@^3.2.0: editions "^1.1.1" typechecker "^4.3.0" -earcut@^2.0.0: - version "2.1.3" - resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.1.3.tgz#ca579545f351941af7c3d0df49c9f7d34af99b0c" - integrity sha512-AxdCdWUk1zzK/NuZ7e1ljj6IGC+VAdC3Qb7QQDsXpfNrc5IM8tL9nNXUmEGE6jRHTfZ10zhzRhtDmWVsR5pd3A== - earcut@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.2.2.tgz#41b0bc35f63e0fe80da7cddff28511e7e2e80d11" @@ -14897,13 +14891,6 @@ gensync@^1.0.0-beta.1: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== -geojson-area@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/geojson-area/-/geojson-area-0.2.1.tgz#2537b0982db86309f21d2c428a4257c7a6282cc6" - integrity sha1-JTewmC24YwnyHSxCikJXx6YoLMY= - dependencies: - wgs84 "0.0.0" - geojson-flatten@~0.2.1: version "0.2.4" resolved "https://registry.yarnpkg.com/geojson-flatten/-/geojson-flatten-0.2.4.tgz#8f3396f31a0f5b747e39c9e6a14088f43ba4ecfb" @@ -14912,16 +14899,6 @@ geojson-flatten@~0.2.1: get-stdin "^6.0.0" minimist "1.2.0" -geojson-normalize@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/geojson-normalize/-/geojson-normalize-0.0.0.tgz#2dbc3678cd1b31b8179e876bda70cd120dde35c0" - integrity sha1-Lbw2eM0bMbgXnodr2nDNEg3eNcA= - -geojson-random@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/geojson-random/-/geojson-random-0.2.2.tgz#ab4838f126adc5e16f8f94e655def820f9119dbc" - integrity sha1-q0g48SatxeFvj5TmVd74IPkRnbw= - geojson-vt@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/geojson-vt/-/geojson-vt-3.2.1.tgz#f8adb614d2c1d3f6ee7c4265cad4bbf3ad60c8b7" @@ -17083,14 +17060,6 @@ in-publish@^2.0.0: resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51" integrity sha1-4g/146KvwmkDILbcVSaCqcf631E= -incremental-convex-hull@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/incremental-convex-hull/-/incremental-convex-hull-1.0.1.tgz#51428c14cb9d9a6144bfe69b2851fb377334be1e" - integrity sha1-UUKMFMudmmFEv+abKFH7N3M0vh4= - dependencies: - robust-orientation "^1.1.2" - simplicial-complex "^1.0.0" - indent-string@3.2.0, indent-string@^3.0.0, indent-string@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" @@ -19410,11 +19379,6 @@ jstransformer@1.0.0, jstransformer@^1.0.0: is-promise "^2.0.0" promise "^7.0.1" -jsts@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/jsts/-/jsts-1.1.2.tgz#d205d2cc8393081d9e484ae36282110695edc230" - integrity sha1-0gXSzIOTCB2eSErjYoIRBpXtwjA= - jsts@^1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/jsts/-/jsts-1.6.2.tgz#c0efc885edae06ae84f78cbf2a0110ba929c5925" @@ -21560,13 +21524,6 @@ monocle-ts@^1.0.0: resolved "https://registry.yarnpkg.com/monocle-ts/-/monocle-ts-1.7.1.tgz#03a615938aa90983a4fa29749969d30f72d80ba1" integrity sha512-X9OzpOyd/R83sYex8NYpJjUzi/MLQMvGNVfxDYiIvs+QMXMEUDwR61MQoARFN10Cqz5h/mbFSPnIQNUIGhYd2Q== -monotone-convex-hull-2d@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/monotone-convex-hull-2d/-/monotone-convex-hull-2d-1.0.1.tgz#47f5daeadf3c4afd37764baa1aa8787a40eee08c" - integrity sha1-R/Xa6t88Sv03dkuqGqh4ekDu4Iw= - dependencies: - robust-orientation "^1.1.3" - moo@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e" @@ -26869,34 +26826,6 @@ rison-node@1.0.2: resolved "https://registry.yarnpkg.com/rison-node/-/rison-node-1.0.2.tgz#b7b5f37f39f5ae2a51a973a33c9aa17239a33e4b" integrity sha1-t7Xzfzn1ripRqXOjPJqhcjmjPks= -robust-orientation@^1.1.2, robust-orientation@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/robust-orientation/-/robust-orientation-1.1.3.tgz#daff5b00d3be4e60722f0e9c0156ef967f1c2049" - integrity sha1-2v9bANO+TmByLw6cAVbvln8cIEk= - dependencies: - robust-scale "^1.0.2" - robust-subtract "^1.0.0" - robust-sum "^1.0.0" - two-product "^1.0.2" - -robust-scale@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/robust-scale/-/robust-scale-1.0.2.tgz#775132ed09542d028e58b2cc79c06290bcf78c32" - integrity sha1-d1Ey7QlULQKOWLLMecBikLz3jDI= - dependencies: - two-product "^1.0.2" - two-sum "^1.0.0" - -robust-subtract@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/robust-subtract/-/robust-subtract-1.0.0.tgz#e0b164e1ed8ba4e3a5dda45a12038348dbed3e9a" - integrity sha1-4LFk4e2LpOOl3aRaEgODSNvtPpo= - -robust-sum@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/robust-sum/-/robust-sum-1.0.0.tgz#16646e525292b4d25d82757a286955e0bbfa53d9" - integrity sha1-FmRuUlKStNJdgnV6KGlV4Lv6U9k= - rollup@^0.25.8: version "0.25.8" resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.25.8.tgz#bf6ce83b87510d163446eeaa577ed6a6fc5835e0" @@ -27757,19 +27686,6 @@ simplebar@^4.2.0: lodash.throttle "^4.1.1" resize-observer-polyfill "^1.5.1" -simplicial-complex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/simplicial-complex/-/simplicial-complex-1.0.0.tgz#6c33a4ed69fcd4d91b7bcadd3b30b63683eae241" - integrity sha1-bDOk7Wn81Nkbe8rdOzC2NoPq4kE= - dependencies: - bit-twiddle "^1.0.0" - union-find "^1.0.0" - -simplify-js@^1.2.1: - version "1.2.3" - resolved "https://registry.yarnpkg.com/simplify-js/-/simplify-js-1.2.3.tgz#a3422c1b9884d60421345eb44d2b872662df27f5" - integrity sha512-0IkEqs+5c5vROkHaifGfbqHf5tYDcsTBy6oJPRbFCSwp2uzEr+PpH3dNP7wD8O3d7zdUCjLVq1/xHkwA/JjlFA== - sinon@^7.4.2: version "7.5.0" resolved "https://registry.yarnpkg.com/sinon/-/sinon-7.5.0.tgz#e9488ea466070ea908fd44a3d6478fd4923c67ec" @@ -30027,440 +29943,6 @@ tunnel@^0.0.6: resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== -turf-along@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-along/-/turf-along-3.0.12.tgz#e622bde7a4bd138c09647d4b14aa0ea700485de6" - integrity sha1-5iK956S9E4wJZH1LFKoOpwBIXeY= - dependencies: - turf-bearing "^3.0.12" - turf-destination "^3.0.12" - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-area@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-area/-/turf-area-3.0.12.tgz#9b7e469ef9fb558fd147bb0c214823263bdbf13c" - integrity sha1-m35Gnvn7VY/RR7sMIUgjJjvb8Tw= - dependencies: - geojson-area "^0.2.1" - -turf-bbox-polygon@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-bbox-polygon/-/turf-bbox-polygon-3.0.12.tgz#330dc0bb38322d61545df966ce6c80f685acf4f2" - integrity sha1-Mw3AuzgyLWFUXflmzmyA9oWs9PI= - dependencies: - turf-helpers "^3.0.12" - -turf-bbox@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-bbox/-/turf-bbox-3.0.12.tgz#3fa06117c8443860ec80ac60fd5d2f1320bfb1be" - integrity sha1-P6BhF8hEOGDsgKxg/V0vEyC/sb4= - dependencies: - turf-meta "^3.0.12" - -turf-bearing@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-bearing/-/turf-bearing-3.0.12.tgz#65f609dd850e7364c7771aa6ded87b0e1917fd20" - integrity sha1-ZfYJ3YUOc2THdxqm3th7DhkX/SA= - dependencies: - turf-invariant "^3.0.12" - -turf-bezier@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-bezier/-/turf-bezier-3.0.12.tgz#102efdd4a63b265ee9c8c1727631920b36f4dd02" - integrity sha1-EC791KY7Jl7pyMFydjGSCzb03QI= - dependencies: - turf-helpers "^3.0.12" - -turf-buffer@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-buffer/-/turf-buffer-3.0.12.tgz#20840fe7c6aa67b24be1cab7ffcc5a82fd6bd971" - integrity sha1-IIQP58aqZ7JL4cq3/8xagv1r2XE= - dependencies: - geojson-normalize "0.0.0" - jsts "1.1.2" - turf-combine "^3.0.12" - turf-helpers "^3.0.12" - -turf-center@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-center/-/turf-center-3.0.12.tgz#45dd6c1729bb867291e3e002e9c7506f8c440196" - integrity sha1-Rd1sFym7hnKR4+AC6cdQb4xEAZY= - dependencies: - turf-bbox "^3.0.12" - turf-helpers "^3.0.12" - -turf-centroid@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-centroid/-/turf-centroid-3.0.12.tgz#eaee0d698204b57fc33994bb1bc867b8da293f8f" - integrity sha1-6u4NaYIEtX/DOZS7G8hnuNopP48= - dependencies: - turf-helpers "^3.0.12" - turf-meta "^3.0.12" - -turf-circle@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-circle/-/turf-circle-3.0.12.tgz#140b21cb4950f2d3cbc70d2df012936867f58930" - integrity sha1-FAshy0lQ8tPLxw0t8BKTaGf1iTA= - dependencies: - turf-destination "^3.0.12" - turf-helpers "^3.0.12" - -turf-collect@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-collect/-/turf-collect-3.0.12.tgz#6e986d1a707da319cc83e7238d0bcdf19aa3c7f2" - integrity sha1-bphtGnB9oxnMg+cjjQvN8Zqjx/I= - dependencies: - turf-inside "^3.0.12" - -turf-combine@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-combine/-/turf-combine-3.0.12.tgz#1670746f0fdce0d1ea8aa6a29ffe5438d446cf73" - integrity sha1-FnB0bw/c4NHqiqain/5UONRGz3M= - dependencies: - turf-meta "^3.0.12" - -turf-concave@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-concave/-/turf-concave-3.0.12.tgz#fcab6056965b0a8319f6cd802601095f2fd3a8eb" - integrity sha1-/KtgVpZbCoMZ9s2AJgEJXy/TqOs= - dependencies: - turf-distance "^3.0.12" - turf-meta "^3.0.12" - turf-tin "^3.0.12" - turf-union "^3.0.12" - -turf-convex@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-convex/-/turf-convex-3.0.12.tgz#a88ddc3e22d1cb658796a9c85d3ada3bd3eca357" - integrity sha1-qI3cPiLRy2WHlqnIXTraO9Pso1c= - dependencies: - convex-hull "^1.0.3" - turf-helpers "^3.0.12" - turf-meta "^3.0.12" - -turf-destination@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-destination/-/turf-destination-3.0.12.tgz#7dd6fbf97e86f831a26c83ef2d5a2f8d1d8a6de2" - integrity sha1-fdb7+X6G+DGibIPvLVovjR2KbeI= - dependencies: - turf-helpers "^3.0.12" - turf-invariant "^3.0.12" - -turf-difference@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-difference/-/turf-difference-3.0.12.tgz#9c3d0d7630421005b8b25b7f068ed9efb4bc6ea7" - integrity sha1-nD0NdjBCEAW4slt/Bo7Z77S8bqc= - dependencies: - jsts "1.1.2" - turf-helpers "^3.0.12" - -turf-distance@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-distance/-/turf-distance-3.0.12.tgz#fb97b8705facd993b145e014b41862610eeca449" - integrity sha1-+5e4cF+s2ZOxReAUtBhiYQ7spEk= - dependencies: - turf-helpers "^3.0.12" - turf-invariant "^3.0.12" - -turf-envelope@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-envelope/-/turf-envelope-3.0.12.tgz#96921d278cc8c664692e320e2543b914080d786b" - integrity sha1-lpIdJ4zIxmRpLjIOJUO5FAgNeGs= - dependencies: - turf-bbox "^3.0.12" - turf-bbox-polygon "^3.0.12" - -turf-explode@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-explode/-/turf-explode-3.0.12.tgz#c5ae28c284cd006c56511ec7d408c48a5414ecfe" - integrity sha1-xa4owoTNAGxWUR7H1AjEilQU7P4= - dependencies: - turf-helpers "^3.0.12" - turf-meta "^3.0.12" - -turf-flip@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-flip/-/turf-flip-3.0.12.tgz#deb868177b9ff3bb310c5d41aaac61a9156a3cbb" - integrity sha1-3rhoF3uf87sxDF1BqqxhqRVqPLs= - dependencies: - turf-meta "^3.0.12" - -turf-grid@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/turf-grid/-/turf-grid-1.0.1.tgz#b904abc564b939b627a66ac15eb16e053829b80f" - integrity sha1-uQSrxWS5ObYnpmrBXrFuBTgpuA8= - dependencies: - turf-point "^2.0.0" - -turf-helpers@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-helpers/-/turf-helpers-3.0.12.tgz#dd4272e74b3ad7c96eecb7ae5c57fe8eca544b7b" - integrity sha1-3UJy50s618lu7LeuXFf+jspUS3s= - -turf-hex-grid@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-hex-grid/-/turf-hex-grid-3.0.12.tgz#0698ef669020bb31d8e9cc2056d0abfcafc84e8f" - integrity sha1-BpjvZpAguzHY6cwgVtCr/K/ITo8= - dependencies: - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-inside@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-inside/-/turf-inside-3.0.12.tgz#9ba40fa6eed63bec7e7d88aa6427622c4df07066" - integrity sha1-m6QPpu7WO+x+fYiqZCdiLE3wcGY= - dependencies: - turf-invariant "^3.0.12" - -turf-intersect@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-intersect/-/turf-intersect-3.0.12.tgz#c0d7fb305843a19275670057a39d268b17830d83" - integrity sha1-wNf7MFhDoZJ1ZwBXo50mixeDDYM= - dependencies: - jsts "1.1.2" - -turf-invariant@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-invariant/-/turf-invariant-3.0.12.tgz#3b95253953991ebd962dd35d4f6704c287de8ebe" - integrity sha1-O5UlOVOZHr2WLdNdT2cEwofejr4= - -turf-isolines@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-isolines/-/turf-isolines-3.0.12.tgz#00b233dfe2eebd4ecb47a94fc923c6ecec89c7ab" - integrity sha1-ALIz3+LuvU7LR6lPySPG7OyJx6s= - dependencies: - turf-bbox "^3.0.12" - turf-grid "1.0.1" - turf-helpers "^3.0.12" - turf-inside "^3.0.12" - turf-planepoint "^3.0.12" - turf-square "^3.0.12" - turf-tin "^3.0.12" - -turf-kinks@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-kinks/-/turf-kinks-3.0.12.tgz#e9c9a8dba5724d98f2350fc5bdeba069ec333755" - integrity sha1-6cmo26VyTZjyNQ/FveugaewzN1U= - dependencies: - turf-helpers "^3.0.12" - -turf-line-distance@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-line-distance/-/turf-line-distance-3.0.12.tgz#7108f5b26907f7b8c2dd1b3997866dd3a60e8f5f" - integrity sha1-cQj1smkH97jC3Rs5l4Zt06YOj18= - dependencies: - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-line-slice@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-line-slice/-/turf-line-slice-3.0.12.tgz#f5f1accc92adae69ea1ac0b29f07529a28dde916" - integrity sha1-9fGszJKtrmnqGsCynwdSmijd6RY= - dependencies: - turf-bearing "^3.0.12" - turf-destination "^3.0.12" - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - turf-point-on-line "^3.0.12" - -turf-meta@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-meta/-/turf-meta-3.0.12.tgz#0aa9a1caf82b2a5a08d54e0830b5b5a3fa0e8a38" - integrity sha1-CqmhyvgrKloI1U4IMLW1o/oOijg= - -turf-midpoint@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-midpoint/-/turf-midpoint-3.0.12.tgz#b12765ae89acdee8556fd5e26c9c5fa041a02cbe" - integrity sha1-sSdlroms3uhVb9XibJxfoEGgLL4= - dependencies: - turf-bearing "^3.0.12" - turf-destination "^3.0.12" - turf-distance "^3.0.12" - turf-invariant "^3.0.12" - -turf-nearest@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-nearest/-/turf-nearest-3.0.12.tgz#700207f4443f05096f86cd246f929f170dfaf46d" - integrity sha1-cAIH9EQ/BQlvhs0kb5KfFw369G0= - dependencies: - turf-distance "^3.0.12" - -turf-planepoint@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-planepoint/-/turf-planepoint-3.0.12.tgz#2c37ae0f17fcb30db6e38f0d59ee6c0dd6caa9af" - integrity sha1-LDeuDxf8sw22448NWe5sDdbKqa8= - -turf-point-grid@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-point-grid/-/turf-point-grid-3.0.12.tgz#d604978be10bc9e53306ae02cef7098431db4971" - integrity sha1-1gSXi+ELyeUzBq4CzvcJhDHbSXE= - dependencies: - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-point-on-line@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-point-on-line/-/turf-point-on-line-3.0.12.tgz#1d8663354e70372db1863e6253e9040c47127b0f" - integrity sha1-HYZjNU5wNy2xhj5iU+kEDEcSew8= - dependencies: - turf-bearing "^3.0.12" - turf-destination "^3.0.12" - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-point-on-surface@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-point-on-surface/-/turf-point-on-surface-3.0.12.tgz#9be505b6b0ba78e98565001de3b3a4267115240a" - integrity sha1-m+UFtrC6eOmFZQAd47OkJnEVJAo= - dependencies: - turf-center "^3.0.12" - turf-distance "^3.0.12" - turf-explode "^3.0.12" - turf-helpers "^3.0.12" - turf-inside "^3.0.12" - -turf-point@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/turf-point/-/turf-point-2.0.1.tgz#a2dcc30a2d20f44cf5c6271df7bae2c0e2146069" - integrity sha1-otzDCi0g9Ez1xicd97riwOIUYGk= - dependencies: - minimist "^1.1.0" - -turf-random@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-random/-/turf-random-3.0.12.tgz#34dbb141c3f1eaeae1424fd6c5eaba1f6fb9b1e8" - integrity sha1-NNuxQcPx6urhQk/Wxeq6H2+5seg= - dependencies: - geojson-random "^0.2.2" - -turf-sample@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-sample/-/turf-sample-3.0.12.tgz#7949f8620612047e1314c1ced87e99c142463cd2" - integrity sha1-eUn4YgYSBH4TFMHO2H6ZwUJGPNI= - dependencies: - turf-helpers "^3.0.12" - -turf-simplify@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-simplify/-/turf-simplify-3.0.12.tgz#85e443c8b46aa2b7526389444c7381daa2ad19e7" - integrity sha1-heRDyLRqordSY4lETHOB2qKtGec= - dependencies: - simplify-js "^1.2.1" - -turf-square-grid@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-square-grid/-/turf-square-grid-3.0.12.tgz#3c1d80ac14556c6813b478bda012512ed4b93ec8" - integrity sha1-PB2ArBRVbGgTtHi9oBJRLtS5Psg= - dependencies: - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-square@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-square/-/turf-square-3.0.12.tgz#1a38b1e0fb05ffe0fcaa43188e2f37942a515b64" - integrity sha1-Gjix4PsF/+D8qkMYji83lCpRW2Q= - dependencies: - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-tag@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-tag/-/turf-tag-3.0.12.tgz#2284fff0e8a1e92a27d4ac7fd7471b3c48ddd1a8" - integrity sha1-IoT/8Oih6Son1Kx/10cbPEjd0ag= - dependencies: - turf-inside "^3.0.12" - -turf-tesselate@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-tesselate/-/turf-tesselate-3.0.12.tgz#41474b7b5b3820bcf273fb71e1894d8c3cd40d35" - integrity sha1-QUdLe1s4ILzyc/tx4YlNjDzUDTU= - dependencies: - earcut "^2.0.0" - turf-helpers "^3.0.12" - -turf-tin@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-tin/-/turf-tin-3.0.12.tgz#b6534644763ace1c9df241c958d2384855257385" - integrity sha1-tlNGRHY6zhyd8kHJWNI4SFUlc4U= - dependencies: - turf-helpers "^3.0.12" - -turf-triangle-grid@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-triangle-grid/-/turf-triangle-grid-3.0.12.tgz#80647e57dafe09346879a29a18a0e6294acf1159" - integrity sha1-gGR+V9r+CTRoeaKaGKDmKUrPEVk= - dependencies: - turf-distance "^3.0.12" - turf-helpers "^3.0.12" - -turf-union@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-union/-/turf-union-3.0.12.tgz#dfed0e5540b8c2855e4994c14621e3a60c829c8e" - integrity sha1-3+0OVUC4woVeSZTBRiHjpgyCnI4= - dependencies: - jsts "1.1.2" - -turf-within@^3.0.12: - version "3.0.12" - resolved "https://registry.yarnpkg.com/turf-within/-/turf-within-3.0.12.tgz#f77eeaf377238561b7fb1338e76e9d1298741f94" - integrity sha1-937q83cjhWG3+xM4526dEph0H5Q= - dependencies: - turf-helpers "^3.0.12" - turf-inside "^3.0.12" - -turf@3.0.14: - version "3.0.14" - resolved "https://registry.yarnpkg.com/turf/-/turf-3.0.14.tgz#eb2f4a80a2d583b8c6486bc7b5c7190466866c27" - integrity sha1-6y9KgKLVg7jGSGvHtccZBGaGbCc= - dependencies: - turf-along "^3.0.12" - turf-area "^3.0.12" - turf-bbox "^3.0.12" - turf-bbox-polygon "^3.0.12" - turf-bearing "^3.0.12" - turf-bezier "^3.0.12" - turf-buffer "^3.0.12" - turf-center "^3.0.12" - turf-centroid "^3.0.12" - turf-circle "^3.0.12" - turf-collect "^3.0.12" - turf-combine "^3.0.12" - turf-concave "^3.0.12" - turf-convex "^3.0.12" - turf-destination "^3.0.12" - turf-difference "^3.0.12" - turf-distance "^3.0.12" - turf-envelope "^3.0.12" - turf-explode "^3.0.12" - turf-flip "^3.0.12" - turf-helpers "^3.0.12" - turf-hex-grid "^3.0.12" - turf-inside "^3.0.12" - turf-intersect "^3.0.12" - turf-isolines "^3.0.12" - turf-kinks "^3.0.12" - turf-line-distance "^3.0.12" - turf-line-slice "^3.0.12" - turf-meta "^3.0.12" - turf-midpoint "^3.0.12" - turf-nearest "^3.0.12" - turf-planepoint "^3.0.12" - turf-point-grid "^3.0.12" - turf-point-on-line "^3.0.12" - turf-point-on-surface "^3.0.12" - turf-random "^3.0.12" - turf-sample "^3.0.12" - turf-simplify "^3.0.12" - turf-square "^3.0.12" - turf-square-grid "^3.0.12" - turf-tag "^3.0.12" - turf-tesselate "^3.0.12" - turf-tin "^3.0.12" - turf-triangle-grid "^3.0.12" - turf-union "^3.0.12" - turf-within "^3.0.12" - tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" @@ -30475,16 +29957,6 @@ twig@^1.10.5: minimatch "3.0.x" walk "2.3.x" -two-product@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/two-product/-/two-product-1.0.2.tgz#67d95d4b257a921e2cb4bd7af9511f9088522eaa" - integrity sha1-Z9ldSyV6kh4stL16+VEfkIhSLqo= - -two-sum@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/two-sum/-/two-sum-1.0.0.tgz#31d3f32239e4f731eca9df9155e2b297f008ab64" - integrity sha1-MdPzIjnk9zHsqd+RVeKyl/AIq2Q= - type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" @@ -30839,11 +30311,6 @@ unified@^7.1.0: vfile "^3.0.0" x-is-string "^0.1.0" -union-find@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/union-find/-/union-find-1.0.2.tgz#292bac415e6ad3a89535d237010db4a536284e58" - integrity sha1-KSusQV5q06iVNdI3AQ20pTYoTlg= - union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" From 18795b2bf3247a9b1cd9e908ddaef3db374fd32f Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Thu, 30 Jul 2020 19:10:26 -0400 Subject: [PATCH 22/33] [Canvas][tech-debt] Fix SVG not shrinking vertically properly (#73867) Co-authored-by: Elastic Machine --- .../plugins/canvas/canvas_plugin_src/renderers/shape/index.js | 1 + .../canvas/public/components/render_with_fn/render_with_fn.tsx | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/index.js b/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/index.js index 02c86afd7182b..5684c8c4602b5 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/index.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/index.js @@ -75,6 +75,7 @@ export const shape = () => ({ domNode.removeChild(oldShape); } + domNode.style.lineHeight = 0; domNode.appendChild(shapeSvg); }; diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx b/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx index 7939c1d04631a..c5fe7074fea0b 100644 --- a/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx +++ b/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx @@ -5,7 +5,6 @@ */ import React, { useState, useEffect, useRef, FC, useCallback } from 'react'; -import { useDebounce } from 'react-use'; import { useNotifyService } from '../../services'; import { RenderToDom } from '../render_to_dom'; @@ -73,7 +72,7 @@ export const RenderWithFn: FC = ({ firstRender.current = true; }, [domNode]); - useDebounce(() => handlers.current.resize({ height, width }), 150, [height, width]); + useEffect(() => handlers.current.resize({ height, width }), [height, width]); useEffect( () => () => { From 9c9080c11e869c2452fe938af100105f3ee05924 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Thu, 30 Jul 2020 19:15:26 -0400 Subject: [PATCH 23/33] [Ingest Management] main branch uses epr-snapshot. Others production (#73555) * Same behavior as now. Just refactored. * main branch uses epr-snapshot. Others use prod * Link some types vs repeating them * replace DEFAULT_REGISTRY_URL with getRegistryUrl in Endpoint tests * Make an Endpoint test helper name more clear * try/catch around getKibanaBranch * Use branch & version from package.json as fallback * No guards b/c kibana{Branch,Version} have defaults Co-authored-by: Elastic Machine --- .../ingest_manager/common/constants/epm.ts | 1 - .../ingest_manager/server/constants/index.ts | 1 - x-pack/plugins/ingest_manager/server/index.ts | 2 +- .../plugins/ingest_manager/server/plugin.ts | 12 +++---- .../server/services/app_context.ts | 13 +++---- .../services/epm/registry/registry_url.ts | 35 ++++++++++++++----- .../ingest_manager/server/services/index.ts | 2 ++ .../apps/endpoint/index.ts | 6 ++-- .../apis/index.ts | 6 ++-- .../registry.ts | 6 ++-- 10 files changed, 48 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/ingest_manager/common/constants/epm.ts b/x-pack/plugins/ingest_manager/common/constants/epm.ts index 3d3c91a4310f8..73cd8463bb6aa 100644 --- a/x-pack/plugins/ingest_manager/common/constants/epm.ts +++ b/x-pack/plugins/ingest_manager/common/constants/epm.ts @@ -6,5 +6,4 @@ export const PACKAGES_SAVED_OBJECT_TYPE = 'epm-packages'; export const INDEX_PATTERN_SAVED_OBJECT_TYPE = 'index-pattern'; -export const DEFAULT_REGISTRY_URL = 'https://epr-snapshot.ea-web.elastic.dev'; export const INDEX_PATTERN_PLACEHOLDER_SUFFIX = '-index_pattern_placeholder'; diff --git a/x-pack/plugins/ingest_manager/server/constants/index.ts b/x-pack/plugins/ingest_manager/server/constants/index.ts index ce81736f2e84f..1ec13bd80f0fb 100644 --- a/x-pack/plugins/ingest_manager/server/constants/index.ts +++ b/x-pack/plugins/ingest_manager/server/constants/index.ts @@ -43,5 +43,4 @@ export { // Defaults DEFAULT_AGENT_CONFIG, DEFAULT_OUTPUT, - DEFAULT_REGISTRY_URL, } from '../../common'; diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index 40e0153a26581..6f8c4948559d3 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -6,7 +6,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from 'src/core/server'; import { IngestManagerPlugin } from './plugin'; -export { AgentService, ESIndexPatternService } from './services'; +export { AgentService, ESIndexPatternService, getRegistryUrl } from './services'; export { IngestManagerSetupContract, IngestManagerSetupDeps, diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index e7495df254a09..e5e1194d59ecb 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -83,9 +83,9 @@ export interface IngestManagerAppContext { security?: SecurityPluginSetup; config$?: Observable; savedObjects: SavedObjectsServiceStart; - isProductionMode: boolean; - kibanaVersion: string; - kibanaBranch: string; + isProductionMode: PluginInitializerContext['env']['mode']['prod']; + kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; + kibanaBranch: PluginInitializerContext['env']['packageInfo']['branch']; cloud?: CloudSetup; logger?: Logger; httpSetup?: HttpServiceSetup; @@ -144,9 +144,9 @@ export class IngestManagerPlugin private cloud: CloudSetup | undefined; private logger: Logger | undefined; - private isProductionMode: boolean; - private kibanaVersion: string; - private kibanaBranch: string; + private isProductionMode: IngestManagerAppContext['isProductionMode']; + private kibanaVersion: IngestManagerAppContext['kibanaVersion']; + private kibanaBranch: IngestManagerAppContext['kibanaBranch']; private httpSetup: HttpServiceSetup | undefined; private encryptedSavedObjectsSetup: EncryptedSavedObjectsPluginSetup | undefined; diff --git a/x-pack/plugins/ingest_manager/server/services/app_context.ts b/x-pack/plugins/ingest_manager/server/services/app_context.ts index bdc7a443ba6dd..7f82670a4d02c 100644 --- a/x-pack/plugins/ingest_manager/server/services/app_context.ts +++ b/x-pack/plugins/ingest_manager/server/services/app_context.ts @@ -10,6 +10,7 @@ import { EncryptedSavedObjectsClient, EncryptedSavedObjectsPluginSetup, } from '../../../encrypted_saved_objects/server'; +import packageJSON from '../../../../../package.json'; import { SecurityPluginSetup } from '../../../security/server'; import { IngestManagerConfigType } from '../../common'; import { ExternalCallback, ExternalCallbacksStorage, IngestManagerAppContext } from '../plugin'; @@ -22,9 +23,9 @@ class AppContextService { private config$?: Observable; private configSubject$?: BehaviorSubject; private savedObjects: SavedObjectsServiceStart | undefined; - private isProductionMode: boolean = false; - private kibanaVersion: string | undefined; - private kibanaBranch: string | undefined; + private isProductionMode: IngestManagerAppContext['isProductionMode'] = false; + private kibanaVersion: IngestManagerAppContext['kibanaVersion'] = packageJSON.version; + private kibanaBranch: IngestManagerAppContext['kibanaBranch'] = packageJSON.branch; private cloud?: CloudSetup; private logger: Logger | undefined; private httpSetup?: HttpServiceSetup; @@ -121,16 +122,10 @@ class AppContextService { } public getKibanaVersion() { - if (!this.kibanaVersion) { - throw new Error('Kibana version is not set.'); - } return this.kibanaVersion; } public getKibanaBranch() { - if (!this.kibanaBranch) { - throw new Error('Kibana branch is not set.'); - } return this.kibanaBranch; } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts index 47c9121808988..b788d1bcbb4a9 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts @@ -3,20 +3,37 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_REGISTRY_URL } from '../../../constants'; import { appContextService, licenseService } from '../../'; +// from https://github.com/elastic/package-registry#docker (maybe from OpenAPI one day) +// the unused variables cause a TS warning about unused values +// chose to comment them out vs @ts-ignore or @ts-expect-error on each line + +const PRODUCTION_REGISTRY_URL_CDN = 'https://epr.elastic.co'; +// const STAGING_REGISTRY_URL_CDN = 'https://epr-staging.elastic.co'; +// const EXPERIMENTAL_REGISTRY_URL_CDN = 'https://epr-experimental.elastic.co/'; +const SNAPSHOT_REGISTRY_URL_CDN = 'https://epr-snapshot.elastic.co'; + +// const PRODUCTION_REGISTRY_URL_NO_CDN = 'https://epr.ea-web.elastic.dev'; +// const STAGING_REGISTRY_URL_NO_CDN = 'https://epr-staging.ea-web.elastic.dev'; +// const EXPERIMENTAL_REGISTRY_URL_NO_CDN = 'https://epr-experimental.ea-web.elastic.dev/'; +// const SNAPSHOT_REGISTRY_URL_NO_CDN = 'https://epr-snapshot.ea-web.elastic.dev'; + +const getDefaultRegistryUrl = (): string => { + const branch = appContextService.getKibanaBranch(); + if (branch === 'master') { + return SNAPSHOT_REGISTRY_URL_CDN; + } else { + return PRODUCTION_REGISTRY_URL_CDN; + } +}; + export const getRegistryUrl = (): string => { const license = licenseService.getLicenseInformation(); const customUrl = appContextService.getConfig()?.registryUrl; + const isGoldPlus = license?.isAvailable && license?.isActive && license?.hasAtLeast('gold'); - if ( - customUrl && - license && - license.isAvailable && - license.hasAtLeast('gold') && - license.isActive - ) { + if (customUrl && isGoldPlus) { return customUrl; } @@ -24,5 +41,5 @@ export const getRegistryUrl = (): string => { appContextService.getLogger().warn('Gold license is required to use a custom registry url.'); } - return DEFAULT_REGISTRY_URL; + return getDefaultRegistryUrl(); }; diff --git a/x-pack/plugins/ingest_manager/server/services/index.ts b/x-pack/plugins/ingest_manager/server/services/index.ts index 74adab09d12eb..f6ca9e7bbbe71 100644 --- a/x-pack/plugins/ingest_manager/server/services/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/index.ts @@ -9,6 +9,8 @@ import { AgentStatus, Agent } from '../types'; import * as settingsService from './settings'; export { ESIndexPatternSavedObjectService } from './es_index_pattern'; +export { getRegistryUrl } from './epm/registry/registry_url'; + /** * Service to return the index pattern of EPM packages */ diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts index 7962ec60ff57e..ad1980cd7218b 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts @@ -3,11 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_REGISTRY_URL } from '../../../../plugins/ingest_manager/common'; +import { getRegistryUrl as getRegistryUrlFromIngest } from '../../../../plugins/ingest_manager/server'; import { FtrProviderContext } from '../../ftr_provider_context'; import { isRegistryEnabled, - getRegistryUrl, + getRegistryUrlFromTestEnv, } from '../../../security_solution_endpoint_api_int/registry'; export default function (providerContext: FtrProviderContext) { @@ -22,7 +22,7 @@ export default function (providerContext: FtrProviderContext) { log.warning('These tests are being run with an external package registry'); } - const registryUrl = getRegistryUrl() ?? DEFAULT_REGISTRY_URL; + const registryUrl = getRegistryUrlFromTestEnv() ?? getRegistryUrlFromIngest(); log.info(`Package registry URL for tests: ${registryUrl}`); before(async () => { diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts index b1317c2d9f1c1..9cdef1c938890 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { FtrProviderContext } from '../ftr_provider_context'; -import { isRegistryEnabled, getRegistryUrl } from '../registry'; -import { DEFAULT_REGISTRY_URL } from '../../../plugins/ingest_manager/common'; +import { isRegistryEnabled, getRegistryUrlFromTestEnv } from '../registry'; +import { getRegistryUrl as getRegistryUrlFromIngest } from '../../../plugins/ingest_manager/server'; export default function endpointAPIIntegrationTests(providerContext: FtrProviderContext) { const { loadTestFile, getService } = providerContext; @@ -20,7 +20,7 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider log.warning('These tests are being run with an external package registry'); } - const registryUrl = getRegistryUrl() ?? DEFAULT_REGISTRY_URL; + const registryUrl = getRegistryUrlFromTestEnv() ?? getRegistryUrlFromIngest(); log.info(`Package registry URL for tests: ${registryUrl}`); before(async () => { diff --git a/x-pack/test/security_solution_endpoint_api_int/registry.ts b/x-pack/test/security_solution_endpoint_api_int/registry.ts index cc474cbf29aaf..9a9d184b9c297 100644 --- a/x-pack/test/security_solution_endpoint_api_int/registry.ts +++ b/x-pack/test/security_solution_endpoint_api_int/registry.ts @@ -57,7 +57,7 @@ export function createEndpointDockerConfig( }); } -export function getRegistryUrl(): string | undefined { +export function getRegistryUrlFromTestEnv(): string | undefined { let registryUrl: string | undefined; if (dockerRegistryPort !== undefined) { registryUrl = `--xpack.ingestManager.registryUrl=http://localhost:${dockerRegistryPort}`; @@ -68,10 +68,10 @@ export function getRegistryUrl(): string | undefined { } export function getRegistryUrlAsArray(): string[] { - const registryUrl: string | undefined = getRegistryUrl(); + const registryUrl: string | undefined = getRegistryUrlFromTestEnv(); return registryUrl !== undefined ? [registryUrl] : []; } export function isRegistryEnabled() { - return getRegistryUrl() !== undefined; + return getRegistryUrlFromTestEnv() !== undefined; } From 84884a93981f683a583cc96e52f63c50dacb13b2 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Thu, 30 Jul 2020 19:16:51 -0400 Subject: [PATCH 24/33] [Security Solution][Lists] - Tests cleanup and remove unnecessary import (#73865) ## Summary Addresses feedback from https://github.com/elastic/kibana/pull/72748 - Updates `plugins/lists` tests text from `should not validate` to `should FAIL validation` after feedback that previous text is a bit confusing and can be interpreted to mean that validation is not conducted - Remove unnecessary spreads from one of my late night PRs - Removes `siem_common_deps` in favor of `shared_imports` in `plugins/lists` - Updates `build_exceptions_query.test.ts` to use existing mocks --- .../common/schemas/common/schemas.test.ts | 2 +- .../lists/common/schemas/common/schemas.ts | 2 +- .../search_es_list_item_schema.test.ts | 4 +- .../search_es_list_schema.test.ts | 4 +- .../create_endpoint_list_item_schema.test.ts | 2 +- .../create_endpoint_list_item_schema.ts | 2 +- .../create_exception_list_item_schema.test.ts | 2 +- .../create_exception_list_item_schema.ts | 2 +- .../create_exception_list_schema.test.ts | 2 +- .../request/create_exception_list_schema.ts | 2 +- .../request/create_list_item_schema.test.ts | 2 +- .../request/create_list_schema.test.ts | 2 +- .../schemas/request/create_list_schema.ts | 2 +- .../delete_endpoint_list_item_schema.test.ts | 2 +- .../delete_exception_list_item_schema.test.ts | 2 +- .../delete_exception_list_schema.test.ts | 2 +- .../request/delete_list_item_schema.test.ts | 2 +- .../request/delete_list_schema.test.ts | 2 +- .../export_list_item_query_schema.test.ts | 2 +- .../find_endpoint_list_item_schema.test.ts | 2 +- .../find_exception_list_item_schema.test.ts | 2 +- .../find_exception_list_schema.test.ts | 2 +- .../request/find_list_item_schema.test.ts | 2 +- .../schemas/request/find_list_schema.test.ts | 2 +- .../import_list_item_query_schema.test.ts | 2 +- .../request/import_list_item_schema.test.ts | 2 +- .../request/patch_list_item_schema.test.ts | 2 +- .../schemas/request/patch_list_schema.test.ts | 2 +- .../read_endpoint_list_item_schema.test.ts | 2 +- .../read_exception_list_item_schema.test.ts | 2 +- .../read_exception_list_schema.test.ts | 2 +- .../request/read_list_item_schema.test.ts | 2 +- .../schemas/request/read_list_schema.test.ts | 2 +- .../update_endpoint_list_item_schema.test.ts | 2 +- .../update_exception_list_item_schema.test.ts | 2 +- .../update_exception_list_schema.test.ts | 2 +- .../request/update_list_item_schema.test.ts | 2 +- .../response/acknowledge_schema.test.ts | 2 +- .../create_endpoint_list_schema.test.ts | 2 +- .../exception_list_item_schema.test.ts | 2 +- .../response/exception_list_schema.test.ts | 2 +- .../found_exception_list_item_schema.test.ts | 2 +- .../found_exception_list_schema.test.ts | 2 +- .../list_item_index_exist_schema.test.ts | 2 +- .../schemas/response/list_item_schema.test.ts | 2 +- .../schemas/response/list_schema.test.ts | 2 +- .../common/schemas/types/comment.test.ts | 2 +- .../lists/common/schemas/types/comment.ts | 2 +- .../schemas/types/create_comment.test.ts | 2 +- .../common/schemas/types/create_comment.ts | 2 +- .../types/default_comments_array.test.ts | 2 +- .../default_create_comments_array.test.ts | 2 +- .../schemas/types/default_namespace.test.ts | 4 +- .../types/default_namespace_array.test.ts | 6 +- .../default_update_comments_array.test.ts | 2 +- .../schemas/types/empty_string_array.test.ts | 4 +- .../common/schemas/types/entries.mock.ts | 23 +- .../common/schemas/types/entries.test.ts | 26 +- .../common/schemas/types/entry_exists.test.ts | 16 +- .../common/schemas/types/entry_exists.ts | 2 +- .../common/schemas/types/entry_list.test.ts | 18 +- .../lists/common/schemas/types/entry_list.ts | 2 +- .../common/schemas/types/entry_match.test.ts | 20 +- .../lists/common/schemas/types/entry_match.ts | 2 +- .../schemas/types/entry_match_any.test.ts | 20 +- .../common/schemas/types/entry_match_any.ts | 2 +- .../common/schemas/types/entry_nested.mock.ts | 2 +- .../common/schemas/types/entry_nested.test.ts | 20 +- .../common/schemas/types/entry_nested.ts | 2 +- .../types/non_empty_entries_array.test.ts | 20 +- .../non_empty_nested_entries_array.test.ts | 26 +- ...non_empty_or_nullable_string_array.test.ts | 12 +- .../types/non_empty_string_array.test.ts | 10 +- .../schemas/types/update_comment.test.ts | 2 +- .../common/schemas/types/update_comment.ts | 2 +- .../plugins/lists/common/siem_common_deps.ts | 9 - x-pack/plugins/lists/public/exceptions/api.ts | 2 +- x-pack/plugins/lists/public/lists/api.ts | 2 +- .../routes/create_endpoint_list_item_route.ts | 2 +- .../routes/create_endpoint_list_route.ts | 2 +- .../create_exception_list_item_route.ts | 2 +- .../routes/create_exception_list_route.ts | 2 +- .../server/routes/create_list_index_route.ts | 2 +- .../server/routes/create_list_item_route.ts | 2 +- .../lists/server/routes/create_list_route.ts | 2 +- .../routes/delete_endpoint_list_item_route.ts | 2 +- .../delete_exception_list_item_route.ts | 2 +- .../routes/delete_exception_list_route.ts | 2 +- .../server/routes/delete_list_index_route.ts | 2 +- .../server/routes/delete_list_item_route.ts | 2 +- .../lists/server/routes/delete_list_route.ts | 2 +- .../routes/find_endpoint_list_item_route.ts | 2 +- .../routes/find_exception_list_item_route.ts | 2 +- .../routes/find_exception_list_route.ts | 2 +- .../server/routes/find_list_item_route.ts | 2 +- .../lists/server/routes/find_list_route.ts | 2 +- .../server/routes/import_list_item_route.ts | 2 +- .../server/routes/patch_list_item_route.ts | 2 +- .../lists/server/routes/patch_list_route.ts | 2 +- .../routes/read_endpoint_list_item_route.ts | 2 +- .../routes/read_exception_list_item_route.ts | 2 +- .../routes/read_exception_list_route.ts | 2 +- .../server/routes/read_list_index_route.ts | 2 +- .../server/routes/read_list_item_route.ts | 2 +- .../lists/server/routes/read_list_route.ts | 2 +- .../routes/update_endpoint_list_item_route.ts | 2 +- .../update_exception_list_item_route.ts | 2 +- .../routes/update_exception_list_route.ts | 2 +- .../server/routes/update_list_item_route.ts | 2 +- .../lists/server/routes/update_list_route.ts | 2 +- .../plugins/lists/server/routes/validate.ts | 2 +- .../services/utils/encode_decode_cursor.ts | 2 +- .../build_exceptions_query.test.ts | 294 ++++++++---------- .../exceptions/builder/helpers.test.tsx | 4 +- .../components/exceptions/helpers.test.tsx | 2 +- 115 files changed, 340 insertions(+), 392 deletions(-) delete mode 100644 x-pack/plugins/lists/common/siem_common_deps.ts diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.test.ts b/x-pack/plugins/lists/common/schemas/common/schemas.test.ts index d450debd56293..fad8ecc86277b 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.test.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { EsDataTypeGeoPoint, diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts index 26511f89c32b8..76aa896a741f6 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -9,7 +9,7 @@ import * as t from 'io-ts'; import { DefaultNamespace } from '../types/default_namespace'; -import { DefaultStringArray, NonEmptyString } from '../../siem_common_deps'; +import { DefaultStringArray, NonEmptyString } from '../../shared_imports'; export const name = t.string; export type Name = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.test.ts index 7ac75b077acb5..d8e3793ac9bd6 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { SearchEsListItemSchema, searchEsListItemSchema } from './search_es_list_item_schema'; import { getSearchEsListItemMock } from './search_es_list_item_schema.mock'; @@ -22,7 +22,7 @@ describe('search_es_list_item_schema', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate with a madeup value', () => { + test('it should FAIL validation when a madeup value', () => { const payload: SearchEsListItemSchema & { madeupValue: string } = { ...getSearchEsListItemMock(), madeupValue: 'madeupvalue', diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.test.ts index 739f102e6a872..27a6c5ef52460 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { SearchEsListSchema, searchEsListSchema } from './search_es_list_schema'; import { getSearchEsListMock } from './search_es_list_schema.mock'; @@ -22,7 +22,7 @@ describe('search_es_list_schema', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate with a madeup value', () => { + test('it should FAIL validation when a madeup value', () => { const payload: SearchEsListSchema & { madeupValue: string } = { ...getSearchEsListMock(), madeupValue: 'madeupvalue', diff --git a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts index 75e0410be610a..e40a80a0d589d 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getCreateCommentsArrayMock } from '../types/create_comment.mock'; import { getCommentsMock } from '../types/comment.mock'; import { CommentsArray } from '../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts index ab30e8e35548d..626b9e3e624ef 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts @@ -22,7 +22,7 @@ import { import { RequiredKeepUndefined } from '../../types'; import { CreateCommentsArray, DefaultCreateCommentsArray, nonEmptyEntriesArray } from '../types'; import { EntriesArray } from '../types/entries'; -import { DefaultUuid } from '../../siem_common_deps'; +import { DefaultUuid } from '../../shared_imports'; export const createEndpointListItemSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts index cf4c1fea0306f..d2ad69d1ee7b6 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getCreateCommentsArrayMock } from '../types/create_comment.mock'; import { getCommentsMock } from '../types/comment.mock'; import { CommentsArray } from '../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts index c3f41cac90c64..039a38594a367 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts @@ -29,7 +29,7 @@ import { nonEmptyEntriesArray, } from '../types'; import { EntriesArray } from '../types/entries'; -import { DefaultUuid } from '../../siem_common_deps'; +import { DefaultUuid } from '../../shared_imports'; export const createExceptionListItemSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.test.ts index 21270f526900b..c9e2aa37a132b 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { CreateExceptionListSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts index 94a4e1588f5ab..7009fbd709e54 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts @@ -25,7 +25,7 @@ import { DefaultUuid, DefaultVersionNumber, DefaultVersionNumberDecoded, -} from '../../siem_common_deps'; +} from '../../shared_imports'; import { NamespaceType } from '../types'; export const createExceptionListSchema = t.intersection([ diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.test.ts index 8178d49690e39..813d5e349e7e6 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getCreateListItemSchemaMock } from './create_list_item_schema.mock'; import { CreateListItemSchema, createListItemSchema } from './create_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts index 9b496a01045de..82340453a98f1 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { CreateListSchema, createListSchema } from './create_list_schema'; import { getCreateListSchemaMock } from './create_list_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts index 18ed0f42ccd6f..bfe3ecdcb623b 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { description, deserializer, id, meta, name, serializer, type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; -import { DefaultVersionNumber, DefaultVersionNumberDecoded } from '../../siem_common_deps'; +import { DefaultVersionNumber, DefaultVersionNumberDecoded } from '../../shared_imports'; export const createListSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts index fa75be8bc541e..fa3c1ef3b02f5 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { DeleteEndpointListItemSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts index 042f62a8d129b..d249cd779e862 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { DeleteExceptionListItemSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts index 2bb0a23173bd6..ec781d59af120 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { DeleteExceptionListSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts index 9bc2825d774ed..7b2263863e1f6 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { DeleteListItemSchema, deleteListItemSchema } from './delete_list_item_schema'; import { getDeleteListItemSchemaMock } from './delete_list_item_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.test.ts index 278508305c6f0..65ca2f3f457e9 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { DeleteListSchema, deleteListSchema } from './delete_list_schema'; import { getDeleteListSchemaMock } from './delete_list_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.test.ts index 1ffe2e2fc4ecc..cd6f4c1b147db 100644 --- a/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { ExportListItemQuerySchema, diff --git a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts index 8249b1e2d49c2..79449b136d066 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getFindEndpointListItemSchemaDecodedMock, diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts index f402f22b093ad..1e971a4eebc33 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { LIST_ID } from '../../constants.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts index ef96346c732b8..6f5d34d6be73e 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getFindExceptionListSchemaDecodedMock, diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts index 59d4b4485b578..8c119aeb14e24 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { LIST_ID } from '../../constants.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts index 63f29a64b4bf9..086e457e8f6b8 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getFindListSchemaDecodedMock, getFindListSchemaMock } from './find_list_schema.mock'; import { FindListSchemaEncoded, findListSchema } from './find_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.test.ts index 9d03229b4d1d9..9945dc03c2e14 100644 --- a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { ImportListItemQuerySchema, diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.test.ts index 7f7c6368a1c5e..4de77b66610d3 100644 --- a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { ImportListItemSchema, importListItemSchema } from './import_list_item_schema'; import { getImportListItemSchemaMock } from './import_list_item_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.test.ts index 58c19e8f9cb4f..b148f19da8a86 100644 --- a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getPathListItemSchemaMock } from './patch_list_item_schema.mock'; import { PatchListItemSchema, patchListItemSchema } from './patch_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.test.ts index 3ab658014bbfa..dea48df3f1702 100644 --- a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getPathListSchemaMock } from './patch_list_schema.mock'; import { PatchListSchema, patchListSchema } from './patch_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts index 70a1d783c87d6..adec476ea5ad7 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getReadEndpointListItemSchemaMock } from './read_endpoint_list_item_schema.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.test.ts index 86c80a527be0d..b7c2715f14e1c 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getReadExceptionListItemSchemaMock } from './read_exception_list_item_schema.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.test.ts index 86cebc3cd3f8e..3bc61e3a5e90a 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getReadExceptionListSchemaMock } from './read_exception_list_schema.mock'; import { ReadExceptionListSchema, readExceptionListSchema } from './read_exception_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.test.ts index 5c71c9820cc1e..1d140719ad939 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getReadListItemSchemaMock } from './read_list_item_schema.mock'; import { ReadListItemSchema, readListItemSchema } from './read_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_list_schema.test.ts index a1ba2655dd723..0b7e92c23f77a 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getReadListSchemaMock } from './read_list_schema.mock'; import { ReadListSchema, readListSchema } from './read_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts index db5bc45ad028b..ecbbb250a88f6 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { UpdateEndpointListItemSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts index ce589fb097a60..a49a5552603fd 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { UpdateExceptionListItemSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts index 892f277045a69..650cbd439ad2b 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { UpdateExceptionListSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts index 6127e20343834..cb6cd76dd3f03 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { UpdateListItemSchema, updateListItemSchema } from './update_list_item_schema'; import { getUpdateListItemSchemaMock } from './update_list_item_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.test.ts index 6e7fb158767b5..a59a93b06e34d 100644 --- a/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getAcknowledgeSchemaResponseMock } from './acknowledge_schema.mock'; import { AcknowledgeSchema, acknowledgeSchema } from './acknowledge_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts index 5fccaaac22e3a..8c1392109979e 100644 --- a/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getExceptionListSchemaMock } from './exception_list_schema.mock'; import { CreateEndpointListSchema, createEndpointListSchema } from './create_endpoint_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts index c8bf73cf842e1..32b55104e4fdf 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getExceptionListItemSchemaMock } from './exception_list_item_schema.mock'; import { ExceptionListItemSchema, exceptionListItemSchema } from './exception_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts index b773dd498ed01..1b5ef08b02d5f 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getExceptionListSchemaMock } from './exception_list_schema.mock'; import { ExceptionListSchema, exceptionListSchema } from './exception_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.test.ts index 70fcf9a86122c..5da3accccd9c2 100644 --- a/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getExceptionListItemSchemaMock } from './exception_list_item_schema.mock'; import { getFoundExceptionListItemSchemaMock } from './found_exception_list_item_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.test.ts index a96ee07c4613b..d4fa8ee0e3481 100644 --- a/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getExceptionListSchemaMock } from './exception_list_schema.mock'; import { getFoundExceptionListSchemaMock } from './found_exception_list_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.test.ts index 9cb130ec0e8ad..2b072d8f95cd8 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getListItemIndexExistSchemaResponseMock } from './list_item_index_exist_schema.mock'; import { ListItemIndexExistSchema, listItemIndexExistSchema } from './list_item_index_exist_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/list_item_schema.test.ts index 8b73506d13750..ec4c8d2c2d1ea 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getListItemResponseMock } from './list_item_schema.mock'; import { ListItemSchema, listItemSchema } from './list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/list_schema.test.ts index e7ae9b45a5e15..87e56e5dd95ac 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getListResponseMock } from './list_schema.mock'; import { ListSchema, listSchema } from './list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/types/comment.test.ts b/x-pack/plugins/lists/common/schemas/types/comment.test.ts index c7c945277f756..081bb9b4bae54 100644 --- a/x-pack/plugins/lists/common/schemas/types/comment.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/comment.test.ts @@ -8,7 +8,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DATE_NOW } from '../../constants.mock'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getCommentsArrayMock, getCommentsMock } from './comment.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/types/comment.ts b/x-pack/plugins/lists/common/schemas/types/comment.ts index 6b0b0166b9ee1..4d7aba3b3ad98 100644 --- a/x-pack/plugins/lists/common/schemas/types/comment.ts +++ b/x-pack/plugins/lists/common/schemas/types/comment.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { created_at, created_by, id, updated_at, updated_by } from '../common/schemas'; export const comment = t.intersection([ diff --git a/x-pack/plugins/lists/common/schemas/types/create_comment.test.ts b/x-pack/plugins/lists/common/schemas/types/create_comment.test.ts index 366bf84d48bbf..8bca8df437871 100644 --- a/x-pack/plugins/lists/common/schemas/types/create_comment.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/create_comment.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getCreateCommentsArrayMock, getCreateCommentsMock } from './create_comment.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/types/create_comment.ts b/x-pack/plugins/lists/common/schemas/types/create_comment.ts index fd33313430ce6..4ccc28b2c4a6d 100644 --- a/x-pack/plugins/lists/common/schemas/types/create_comment.ts +++ b/x-pack/plugins/lists/common/schemas/types/create_comment.ts @@ -5,7 +5,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; export const createComment = t.exact( t.type({ diff --git a/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts index 541b8ab1c799c..ee2dc0cf2a478 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { DefaultCommentsArray } from './default_comments_array'; import { CommentsArray } from './comment'; diff --git a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts index eb960b5411904..4aac3cc84a3a2 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { DefaultCreateCommentsArray } from './default_create_comments_array'; import { CreateCommentsArray } from './create_comment'; diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts index 152f85233aa1a..8e7ffdbdaea7b 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { DefaultNamespace } from './default_namespace'; @@ -48,7 +48,7 @@ describe('default_namespace', () => { expect(message.schema).toEqual('single'); }); - test('it should NOT validate if not "single" or "agnostic"', () => { + test('it should FAIL validation if not "single" or "agnostic"', () => { const payload = 'something else'; const decoded = DefaultNamespace.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts index 255c89959b610..e377faae87947 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { DefaultNamespaceArray, DefaultNamespaceArrayType } from './default_namespace_array'; @@ -21,7 +21,7 @@ describe('default_namespace_array', () => { expect(message.schema).toEqual(['single']); }); - test('it should NOT validate a numeric value', () => { + test('it should FAIL validation of numeric value', () => { const payload = 5; const decoded = DefaultNamespaceArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -86,7 +86,7 @@ describe('default_namespace_array', () => { expect(message.schema).toEqual(['single', 'agnostic', 'single']); }); - test('it should not validate 3 elements of "single,agnostic,junk" since the 3rd value is junk', () => { + test('it should FAIL validation when given 3 elements of "single,agnostic,junk" since the 3rd value is junk', () => { const payload: DefaultNamespaceArrayType = 'single,agnostic,junk'; const decoded = DefaultNamespaceArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts index 612148dc4ccab..25c84af8c9ee3 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { DefaultUpdateCommentsArray } from './default_update_comments_array'; import { UpdateCommentsArray } from './update_comment'; diff --git a/x-pack/plugins/lists/common/schemas/types/empty_string_array.test.ts b/x-pack/plugins/lists/common/schemas/types/empty_string_array.test.ts index b14afab327fb0..3ddeeebfceda7 100644 --- a/x-pack/plugins/lists/common/schemas/types/empty_string_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/empty_string_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { EmptyStringArray, EmptyStringArrayEncoded } from './empty_string_array'; @@ -57,7 +57,7 @@ describe('empty_string_array', () => { expect(message.schema).toEqual(['a', 'b', 'c']); }); - test('it should NOT validate a number', () => { + test('it should FAIL validation of number', () => { const payload: number = 5; const decoded = EmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/entries.mock.ts b/x-pack/plugins/lists/common/schemas/types/entries.mock.ts index 16794415138b2..c0093ed750b62 100644 --- a/x-pack/plugins/lists/common/schemas/types/entries.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entries.mock.ts @@ -12,21 +12,18 @@ import { getEntryExistsMock } from './entry_exists.mock'; import { getEntryNestedMock } from './entry_nested.mock'; export const getListAndNonListEntriesArrayMock = (): EntriesArray => [ - { ...getEntryMatchMock() }, - { ...getEntryMatchAnyMock() }, - { ...getEntryListMock() }, - { ...getEntryExistsMock() }, - { ...getEntryNestedMock() }, + getEntryMatchMock(), + getEntryMatchAnyMock(), + getEntryListMock(), + getEntryExistsMock(), + getEntryNestedMock(), ]; -export const getListEntriesArrayMock = (): EntriesArray => [ - { ...getEntryListMock() }, - { ...getEntryListMock() }, -]; +export const getListEntriesArrayMock = (): EntriesArray => [getEntryListMock(), getEntryListMock()]; export const getEntriesArrayMock = (): EntriesArray => [ - { ...getEntryMatchMock() }, - { ...getEntryMatchAnyMock() }, - { ...getEntryExistsMock() }, - { ...getEntryNestedMock() }, + getEntryMatchMock(), + getEntryMatchAnyMock(), + getEntryExistsMock(), + getEntryNestedMock(), ]; diff --git a/x-pack/plugins/lists/common/schemas/types/entries.test.ts b/x-pack/plugins/lists/common/schemas/types/entries.test.ts index cad94220a232c..f5c022c7a394f 100644 --- a/x-pack/plugins/lists/common/schemas/types/entries.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entries.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryMatchMock } from './entry_match.mock'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; @@ -20,7 +20,7 @@ import { entriesArray, entriesArrayOrUndefined, entry } from './entries'; describe('Entries', () => { describe('entry', () => { test('it should validate a match entry', () => { - const payload = { ...getEntryMatchMock() }; + const payload = getEntryMatchMock(); const decoded = entry.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -29,7 +29,7 @@ describe('Entries', () => { }); test('it should validate a match_any entry', () => { - const payload = { ...getEntryMatchAnyMock() }; + const payload = getEntryMatchAnyMock(); const decoded = entry.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -38,7 +38,7 @@ describe('Entries', () => { }); test('it should validate a exists entry', () => { - const payload = { ...getEntryExistsMock() }; + const payload = getEntryExistsMock(); const decoded = entry.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -47,7 +47,7 @@ describe('Entries', () => { }); test('it should validate a list entry', () => { - const payload = { ...getEntryListMock() }; + const payload = getEntryListMock(); const decoded = entry.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -55,8 +55,8 @@ describe('Entries', () => { expect(message.schema).toEqual(payload); }); - test('it should NOT validate a nested entry', () => { - const payload = { ...getEntryNestedMock() }; + test('it should FAIL validation of nested entry', () => { + const payload = getEntryNestedMock(); const decoded = entry.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -79,7 +79,7 @@ describe('Entries', () => { describe('entriesArray', () => { test('it should validate an array with match entry', () => { - const payload = [{ ...getEntryMatchMock() }]; + const payload = [getEntryMatchMock()]; const decoded = entriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -88,7 +88,7 @@ describe('Entries', () => { }); test('it should validate an array with match_any entry', () => { - const payload = [{ ...getEntryMatchAnyMock() }]; + const payload = [getEntryMatchAnyMock()]; const decoded = entriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -97,7 +97,7 @@ describe('Entries', () => { }); test('it should validate an array with exists entry', () => { - const payload = [{ ...getEntryExistsMock() }]; + const payload = [getEntryExistsMock()]; const decoded = entriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -106,7 +106,7 @@ describe('Entries', () => { }); test('it should validate an array with list entry', () => { - const payload = [{ ...getEntryListMock() }]; + const payload = [getEntryListMock()]; const decoded = entriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -115,7 +115,7 @@ describe('Entries', () => { }); test('it should validate an array with nested entry', () => { - const payload = [{ ...getEntryNestedMock() }]; + const payload = [getEntryNestedMock()]; const decoded = entriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -144,7 +144,7 @@ describe('Entries', () => { }); test('it should validate an array with nested entry', () => { - const payload = [{ ...getEntryNestedMock() }]; + const payload = [getEntryNestedMock()]; const decoded = entriesArrayOrUndefined.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_exists.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_exists.test.ts index 9d5b669333db8..0eb35b0768cf4 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_exists.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_exists.test.ts @@ -7,14 +7,14 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryExistsMock } from './entry_exists.mock'; import { EntryExists, entriesExists } from './entry_exists'; describe('entriesExists', () => { test('it should validate an entry', () => { - const payload = { ...getEntryExistsMock() }; + const payload = getEntryExistsMock(); const decoded = entriesExists.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('entriesExists', () => { }); test('it should validate when "operator" is "included"', () => { - const payload = { ...getEntryExistsMock() }; + const payload = getEntryExistsMock(); const decoded = entriesExists.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -32,7 +32,7 @@ describe('entriesExists', () => { }); test('it should validate when "operator" is "excluded"', () => { - const payload = { ...getEntryExistsMock() }; + const payload = getEntryExistsMock(); payload.operator = 'excluded'; const decoded = entriesExists.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -41,7 +41,7 @@ describe('entriesExists', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate when "field" is empty string', () => { + test('it should FAIL validation when "field" is empty string', () => { const payload: Omit & { field: string } = { ...getEntryExistsMock(), field: '', @@ -56,16 +56,16 @@ describe('entriesExists', () => { test('it should strip out extra keys', () => { const payload: EntryExists & { extraKey?: string; - } = { ...getEntryExistsMock() }; + } = getEntryExistsMock(); payload.extraKey = 'some extra key'; const decoded = entriesExists.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ ...getEntryExistsMock() }); + expect(message.schema).toEqual(getEntryExistsMock()); }); - test('it should not validate when "type" is not "exists"', () => { + test('it should FAIL validation when "type" is not "exists"', () => { const payload: Omit & { type: string } = { ...getEntryExistsMock(), type: 'match', diff --git a/x-pack/plugins/lists/common/schemas/types/entry_exists.ts b/x-pack/plugins/lists/common/schemas/types/entry_exists.ts index 05c82d2532218..4d9c09cc93574 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_exists.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_exists.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { operator } from '../common/schemas'; export const entriesExists = t.exact( diff --git a/x-pack/plugins/lists/common/schemas/types/entry_list.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_list.test.ts index 14857edad5e3b..834fed3550e3f 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_list.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_list.test.ts @@ -7,14 +7,14 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryListMock } from './entry_list.mock'; import { EntryList, entriesList } from './entry_list'; describe('entriesList', () => { test('it should validate an entry', () => { - const payload = { ...getEntryListMock() }; + const payload = getEntryListMock(); const decoded = entriesList.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('entriesList', () => { }); test('it should validate when operator is "included"', () => { - const payload = { ...getEntryListMock() }; + const payload = getEntryListMock(); const decoded = entriesList.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -32,7 +32,7 @@ describe('entriesList', () => { }); test('it should validate when "operator" is "excluded"', () => { - const payload = { ...getEntryListMock() }; + const payload = getEntryListMock(); payload.operator = 'excluded'; const decoded = entriesList.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -41,7 +41,7 @@ describe('entriesList', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate when "list" is not expected value', () => { + test('it should FAIL validation when "list" is not expected value', () => { const payload: Omit & { list: string } = { ...getEntryListMock(), list: 'someListId', @@ -55,7 +55,7 @@ describe('entriesList', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "list.id" is empty string', () => { + test('it should FAIL validation when "list.id" is empty string', () => { const payload: Omit & { list: { id: string; type: 'ip' } } = { ...getEntryListMock(), list: { id: '', type: 'ip' }, @@ -67,7 +67,7 @@ describe('entriesList', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "type" is not "lists"', () => { + test('it should FAIL validation when "type" is not "lists"', () => { const payload: Omit & { type: 'match_any' } = { ...getEntryListMock(), type: 'match_any', @@ -84,12 +84,12 @@ describe('entriesList', () => { test('it should strip out extra keys', () => { const payload: EntryList & { extraKey?: string; - } = { ...getEntryListMock() }; + } = getEntryListMock(); payload.extraKey = 'some extra key'; const decoded = entriesList.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ ...getEntryListMock() }); + expect(message.schema).toEqual(getEntryListMock()); }); }); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_list.ts b/x-pack/plugins/lists/common/schemas/types/entry_list.ts index ae9de967db027..fcfec5e0cccdf 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_list.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_list.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { operator, type } from '../common/schemas'; export const entriesList = t.exact( diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_match.test.ts index 2c64592518eb7..7b49c418b547f 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match.test.ts @@ -7,14 +7,14 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryMatchMock } from './entry_match.mock'; import { EntryMatch, entriesMatch } from './entry_match'; describe('entriesMatch', () => { test('it should validate an entry', () => { - const payload = { ...getEntryMatchMock() }; + const payload = getEntryMatchMock(); const decoded = entriesMatch.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('entriesMatch', () => { }); test('it should validate when operator is "included"', () => { - const payload = { ...getEntryMatchMock() }; + const payload = getEntryMatchMock(); const decoded = entriesMatch.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -32,7 +32,7 @@ describe('entriesMatch', () => { }); test('it should validate when "operator" is "excluded"', () => { - const payload = { ...getEntryMatchMock() }; + const payload = getEntryMatchMock(); payload.operator = 'excluded'; const decoded = entriesMatch.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -41,7 +41,7 @@ describe('entriesMatch', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate when "field" is empty string', () => { + test('it should FAIL validation when "field" is empty string', () => { const payload: Omit & { field: string } = { ...getEntryMatchMock(), field: '', @@ -53,7 +53,7 @@ describe('entriesMatch', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "value" is not string', () => { + test('it should FAIL validation when "value" is not string', () => { const payload: Omit & { value: string[] } = { ...getEntryMatchMock(), value: ['some value'], @@ -67,7 +67,7 @@ describe('entriesMatch', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "value" is empty string', () => { + test('it should FAIL validation when "value" is empty string', () => { const payload: Omit & { value: string } = { ...getEntryMatchMock(), value: '', @@ -79,7 +79,7 @@ describe('entriesMatch', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "type" is not "match"', () => { + test('it should FAIL validation when "type" is not "match"', () => { const payload: Omit & { type: string } = { ...getEntryMatchMock(), type: 'match_any', @@ -96,12 +96,12 @@ describe('entriesMatch', () => { test('it should strip out extra keys', () => { const payload: EntryMatch & { extraKey?: string; - } = { ...getEntryMatchMock() }; + } = getEntryMatchMock(); payload.extraKey = 'some value'; const decoded = entriesMatch.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ ...getEntryMatchMock() }); + expect(message.schema).toEqual(getEntryMatchMock()); }); }); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match.ts b/x-pack/plugins/lists/common/schemas/types/entry_match.ts index a21f83f317e35..247d64674e27d 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { operator } from '../common/schemas'; export const entriesMatch = t.exact( diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_any.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_any.test.ts index 4dab2f45711f0..628ccfd74b606 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match_any.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match_any.test.ts @@ -7,14 +7,14 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; import { EntryMatchAny, entriesMatchAny } from './entry_match_any'; describe('entriesMatchAny', () => { test('it should validate an entry', () => { - const payload = { ...getEntryMatchAnyMock() }; + const payload = getEntryMatchAnyMock(); const decoded = entriesMatchAny.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('entriesMatchAny', () => { }); test('it should validate when operator is "included"', () => { - const payload = { ...getEntryMatchAnyMock() }; + const payload = getEntryMatchAnyMock(); const decoded = entriesMatchAny.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -32,7 +32,7 @@ describe('entriesMatchAny', () => { }); test('it should validate when operator is "excluded"', () => { - const payload = { ...getEntryMatchAnyMock() }; + const payload = getEntryMatchAnyMock(); payload.operator = 'excluded'; const decoded = entriesMatchAny.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -41,7 +41,7 @@ describe('entriesMatchAny', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate when field is empty string', () => { + test('it should FAIL validation when field is empty string', () => { const payload: Omit & { field: string } = { ...getEntryMatchAnyMock(), field: '', @@ -53,7 +53,7 @@ describe('entriesMatchAny', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when value is empty array', () => { + test('it should FAIL validation when value is empty array', () => { const payload: Omit & { value: string[] } = { ...getEntryMatchAnyMock(), value: [], @@ -65,7 +65,7 @@ describe('entriesMatchAny', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when value is not string array', () => { + test('it should FAIL validation when value is not string array', () => { const payload: Omit & { value: string } = { ...getEntryMatchAnyMock(), value: 'some string', @@ -79,7 +79,7 @@ describe('entriesMatchAny', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "type" is not "match_any"', () => { + test('it should FAIL validation when "type" is not "match_any"', () => { const payload: Omit & { type: string } = { ...getEntryMatchAnyMock(), type: 'match', @@ -94,12 +94,12 @@ describe('entriesMatchAny', () => { test('it should strip out extra keys', () => { const payload: EntryMatchAny & { extraKey?: string; - } = { ...getEntryMatchAnyMock() }; + } = getEntryMatchAnyMock(); payload.extraKey = 'some extra key'; const decoded = entriesMatchAny.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ ...getEntryMatchAnyMock() }); + expect(message.schema).toEqual(getEntryMatchAnyMock()); }); }); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_any.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_any.ts index e93ad4aa131d1..b6c4ef509c477 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match_any.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match_any.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { operator } from '../common/schemas'; import { nonEmptyOrNullableStringArray } from './non_empty_or_nullable_string_array'; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts index f645bc9e40d78..d0e7712301ee1 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts @@ -11,7 +11,7 @@ import { getEntryMatchMock } from './entry_match.mock'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; export const getEntryNestedMock = (): EntryNested => ({ - entries: [{ ...getEntryMatchMock() }, { ...getEntryMatchAnyMock() }], + entries: [getEntryMatchMock(), getEntryMatchAnyMock()], field: FIELD, type: NESTED, }); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_nested.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_nested.test.ts index d9b58855413b1..d77440b207d03 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_nested.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_nested.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryNestedMock } from './entry_nested.mock'; import { EntryNested, entriesNested } from './entry_nested'; @@ -16,7 +16,7 @@ import { getEntryExistsMock } from './entry_exists.mock'; describe('entriesNested', () => { test('it should validate a nested entry', () => { - const payload = { ...getEntryNestedMock() }; + const payload = getEntryNestedMock(); const decoded = entriesNested.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -24,7 +24,7 @@ describe('entriesNested', () => { expect(message.schema).toEqual(payload); }); - test('it should NOT validate when "type" is not "nested"', () => { + test('it should FAIL validation when "type" is not "nested"', () => { const payload: Omit & { type: 'match' } = { ...getEntryNestedMock(), type: 'match', @@ -36,7 +36,7 @@ describe('entriesNested', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate when "field" is empty string', () => { + test('it should FAIL validation when "field" is empty string', () => { const payload: Omit & { field: string; } = { ...getEntryNestedMock(), field: '' }; @@ -47,7 +47,7 @@ describe('entriesNested', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate when "field" is not a string', () => { + test('it should FAIL validation when "field" is not a string', () => { const payload: Omit & { field: number; } = { ...getEntryNestedMock(), field: 1 }; @@ -58,7 +58,7 @@ describe('entriesNested', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate when "entries" is not a an array', () => { + test('it should FAIL validation when "entries" is not a an array', () => { const payload: Omit & { entries: string; } = { ...getEntryNestedMock(), entries: 'im a string' }; @@ -72,7 +72,7 @@ describe('entriesNested', () => { }); test('it should validate when "entries" contains an entry item that is type "match"', () => { - const payload = { ...getEntryNestedMock(), entries: [{ ...getEntryMatchAnyMock() }] }; + const payload = { ...getEntryNestedMock(), entries: [getEntryMatchAnyMock()] }; const decoded = entriesNested.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -92,7 +92,7 @@ describe('entriesNested', () => { }); test('it should validate when "entries" contains an entry item that is type "exists"', () => { - const payload = { ...getEntryNestedMock(), entries: [{ ...getEntryExistsMock() }] }; + const payload = { ...getEntryNestedMock(), entries: [getEntryExistsMock()] }; const decoded = entriesNested.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -113,12 +113,12 @@ describe('entriesNested', () => { test('it should strip out extra keys', () => { const payload: EntryNested & { extraKey?: string; - } = { ...getEntryNestedMock() }; + } = getEntryNestedMock(); payload.extraKey = 'some extra key'; const decoded = entriesNested.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ ...getEntryNestedMock() }); + expect(message.schema).toEqual(getEntryNestedMock()); }); }); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_nested.ts b/x-pack/plugins/lists/common/schemas/types/entry_nested.ts index 9989f501d4338..f9e8e4356b811 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_nested.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_nested.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { nonEmptyNestedEntriesArray } from './non_empty_nested_entries_array'; diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts index a2697286aa038..42d476a9fefb2 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryMatchMock } from './entry_match.mock'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; @@ -22,7 +22,7 @@ import { nonEmptyEntriesArray } from './non_empty_entries_array'; import { EntriesArray } from './entries'; describe('non_empty_entries_array', () => { - test('it should NOT validate an empty array', () => { + test('it should FAIL validation when given an empty array', () => { const payload: EntriesArray = []; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -33,7 +33,7 @@ describe('non_empty_entries_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "undefined"', () => { + test('it should FAIL validation when given "undefined"', () => { const payload = undefined; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -44,7 +44,7 @@ describe('non_empty_entries_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "null"', () => { + test('it should FAIL validation when given "null"', () => { const payload = null; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -56,7 +56,7 @@ describe('non_empty_entries_array', () => { }); test('it should validate an array of "match" entries', () => { - const payload: EntriesArray = [{ ...getEntryMatchMock() }, { ...getEntryMatchMock() }]; + const payload: EntriesArray = [getEntryMatchMock(), getEntryMatchMock()]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -65,7 +65,7 @@ describe('non_empty_entries_array', () => { }); test('it should validate an array of "match_any" entries', () => { - const payload: EntriesArray = [{ ...getEntryMatchAnyMock() }, { ...getEntryMatchAnyMock() }]; + const payload: EntriesArray = [getEntryMatchAnyMock(), getEntryMatchAnyMock()]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -74,7 +74,7 @@ describe('non_empty_entries_array', () => { }); test('it should validate an array of "exists" entries', () => { - const payload: EntriesArray = [{ ...getEntryExistsMock() }, { ...getEntryExistsMock() }]; + const payload: EntriesArray = [getEntryExistsMock(), getEntryExistsMock()]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -92,7 +92,7 @@ describe('non_empty_entries_array', () => { }); test('it should validate an array of "nested" entries', () => { - const payload: EntriesArray = [{ ...getEntryNestedMock() }, { ...getEntryNestedMock() }]; + const payload: EntriesArray = [getEntryNestedMock(), getEntryNestedMock()]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -109,7 +109,7 @@ describe('non_empty_entries_array', () => { expect(message.schema).toEqual(payload); }); - test('it should NOT validate an array of entries of value list and non-value list entries', () => { + test('it should FAIL validation when given an array of entries of value list and non-value list entries', () => { const payload: EntriesArray = [...getListAndNonListEntriesArrayMock()]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -118,7 +118,7 @@ describe('non_empty_entries_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate an array of non entries', () => { + test('it should FAIL validation when given an array of non entries', () => { const payload = [1]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts index 1154f2b6098da..7dbc3465610c0 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryMatchMock } from './entry_match.mock'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; @@ -17,7 +17,7 @@ import { nonEmptyNestedEntriesArray } from './non_empty_nested_entries_array'; import { EntriesArray } from './entries'; describe('non_empty_nested_entries_array', () => { - test('it should NOT validate an empty array', () => { + test('it should FAIL validation when given an empty array', () => { const payload: EntriesArray = []; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -28,7 +28,7 @@ describe('non_empty_nested_entries_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "undefined"', () => { + test('it should FAIL validation when given "undefined"', () => { const payload = undefined; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -39,7 +39,7 @@ describe('non_empty_nested_entries_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "null"', () => { + test('it should FAIL validation when given "null"', () => { const payload = null; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -51,7 +51,7 @@ describe('non_empty_nested_entries_array', () => { }); test('it should validate an array of "match" entries', () => { - const payload: EntriesArray = [{ ...getEntryMatchMock() }, { ...getEntryMatchMock() }]; + const payload: EntriesArray = [getEntryMatchMock(), getEntryMatchMock()]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -60,7 +60,7 @@ describe('non_empty_nested_entries_array', () => { }); test('it should validate an array of "match_any" entries', () => { - const payload: EntriesArray = [{ ...getEntryMatchAnyMock() }, { ...getEntryMatchAnyMock() }]; + const payload: EntriesArray = [getEntryMatchAnyMock(), getEntryMatchAnyMock()]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -69,7 +69,7 @@ describe('non_empty_nested_entries_array', () => { }); test('it should validate an array of "exists" entries', () => { - const payload: EntriesArray = [{ ...getEntryExistsMock() }, { ...getEntryExistsMock() }]; + const payload: EntriesArray = [getEntryExistsMock(), getEntryExistsMock()]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -77,8 +77,8 @@ describe('non_empty_nested_entries_array', () => { expect(message.schema).toEqual(payload); }); - test('it should NOT validate an array of "nested" entries', () => { - const payload: EntriesArray = [{ ...getEntryNestedMock() }, { ...getEntryNestedMock() }]; + test('it should FAIL validation when given an array of "nested" entries', () => { + const payload: EntriesArray = [getEntryNestedMock(), getEntryNestedMock()]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -105,9 +105,9 @@ describe('non_empty_nested_entries_array', () => { test('it should validate an array of entries', () => { const payload: EntriesArray = [ - { ...getEntryExistsMock() }, - { ...getEntryMatchAnyMock() }, - { ...getEntryMatchMock() }, + getEntryExistsMock(), + getEntryMatchAnyMock(), + getEntryMatchMock(), ]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -116,7 +116,7 @@ describe('non_empty_nested_entries_array', () => { expect(message.schema).toEqual(payload); }); - test('it should NOT validate an array of non entries', () => { + test('it should FAIL validation when given an array of non entries', () => { const payload = [1]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.test.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.test.ts index e3cc9104853e5..4b31b649556b2 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.test.ts @@ -7,12 +7,12 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { nonEmptyOrNullableStringArray } from './non_empty_or_nullable_string_array'; describe('nonEmptyOrNullableStringArray', () => { - test('it should NOT validate an empty array', () => { + test('it should FAIL validation when given an empty array', () => { const payload: string[] = []; const decoded = nonEmptyOrNullableStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('nonEmptyOrNullableStringArray', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "undefined"', () => { + test('it should FAIL validation when given "undefined"', () => { const payload = undefined; const decoded = nonEmptyOrNullableStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -34,7 +34,7 @@ describe('nonEmptyOrNullableStringArray', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "null"', () => { + test('it should FAIL validation when given "null"', () => { const payload = null; const decoded = nonEmptyOrNullableStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -45,7 +45,7 @@ describe('nonEmptyOrNullableStringArray', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate an array of with an empty string', () => { + test('it should FAIL validation when given an array of with an empty string', () => { const payload: string[] = ['im good', '']; const decoded = nonEmptyOrNullableStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -56,7 +56,7 @@ describe('nonEmptyOrNullableStringArray', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate an array of non strings', () => { + test('it should FAIL validation when given an array of non strings', () => { const payload = [1]; const decoded = nonEmptyOrNullableStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts index fac088568f85e..db81b0d469859 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts @@ -7,12 +7,12 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { NonEmptyStringArray } from './non_empty_string_array'; describe('non_empty_string_array', () => { - test('it should NOT validate "null"', () => { + test('it should FAIL validation when given "null"', () => { const payload: NonEmptyStringArray | null = null; const decoded = NonEmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('non_empty_string_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "undefined"', () => { + test('it should FAIL validation when given "undefined"', () => { const payload: NonEmptyStringArray | undefined = undefined; const decoded = NonEmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -34,7 +34,7 @@ describe('non_empty_string_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate a single value of an empty string ""', () => { + test('it should FAIL validation of single value of an empty string ""', () => { const payload: NonEmptyStringArray = ''; const decoded = NonEmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -72,7 +72,7 @@ describe('non_empty_string_array', () => { expect(message.schema).toEqual(['a', 'b', 'c']); }); - test('it should NOT validate a number', () => { + test('it should FAIL validation of number', () => { const payload: number = 5; const decoded = NonEmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts b/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts index ac7716af40966..ac4d0304cbb8e 100644 --- a/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getUpdateCommentMock, getUpdateCommentsArrayMock } from './update_comment.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/types/update_comment.ts b/x-pack/plugins/lists/common/schemas/types/update_comment.ts index b95812cb35bf9..dc14bf480857f 100644 --- a/x-pack/plugins/lists/common/schemas/types/update_comment.ts +++ b/x-pack/plugins/lists/common/schemas/types/update_comment.ts @@ -5,7 +5,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { id } from '../common/schemas'; export const updateComment = t.intersection([ diff --git a/x-pack/plugins/lists/common/siem_common_deps.ts b/x-pack/plugins/lists/common/siem_common_deps.ts deleted file mode 100644 index 2b37e2b7bf106..0000000000000 --- a/x-pack/plugins/lists/common/siem_common_deps.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. - */ - -// DEPRECATED: Do not add exports to this file; please import from shared_imports instead - -export * from './shared_imports'; diff --git a/x-pack/plugins/lists/public/exceptions/api.ts b/x-pack/plugins/lists/public/exceptions/api.ts index d661cb103fad8..203c84b2943fd 100644 --- a/x-pack/plugins/lists/public/exceptions/api.ts +++ b/x-pack/plugins/lists/public/exceptions/api.ts @@ -29,7 +29,7 @@ import { updateExceptionListItemSchema, updateExceptionListSchema, } from '../../common/schemas'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { AddEndpointExceptionListProps, diff --git a/x-pack/plugins/lists/public/lists/api.ts b/x-pack/plugins/lists/public/lists/api.ts index 606109f1910c4..211b2445a0429 100644 --- a/x-pack/plugins/lists/public/lists/api.ts +++ b/x-pack/plugins/lists/public/lists/api.ts @@ -29,7 +29,7 @@ import { listSchema, } from '../../common/schemas'; import { LIST_INDEX, LIST_ITEM_URL, LIST_PRIVILEGES_URL, LIST_URL } from '../../common/constants'; -import { validateEither } from '../../common/siem_common_deps'; +import { validateEither } from '../../common/shared_imports'; import { toError, toPromise } from '../common/fp_utils'; import { diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts index 22aa1fb59858b..7fd07ed5fb8cd 100644 --- a/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_ID, ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { CreateEndpointListItemSchemaDecoded, createEndpointListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts index b1e589be67cd1..91b6a328c8649 100644 --- a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_URL } from '../../common/constants'; import { buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { createEndpointListSchema } from '../../common/schemas'; import { getExceptionListClient } from './utils/get_exception_list_client'; diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts index ed58621dae973..fc0473b2b3704 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { CreateExceptionListItemSchemaDecoded, createExceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts index fbe9c6ec9d83b..08db0825e07bd 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { CreateExceptionListSchemaDecoded, createExceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/create_list_index_route.ts b/x-pack/plugins/lists/server/routes/create_list_index_route.ts index 1bffdd6bd5b5f..be08093dc7055 100644 --- a/x-pack/plugins/lists/server/routes/create_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_index_route.ts @@ -7,7 +7,7 @@ import { IRouter } from 'kibana/server'; import { buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { LIST_INDEX } from '../../common/constants'; import { acknowledgeSchema } from '../../common/schemas'; diff --git a/x-pack/plugins/lists/server/routes/create_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_list_item_route.ts index 656d6af2c6c9a..0a4a1c739ae7c 100644 --- a/x-pack/plugins/lists/server/routes/create_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_item_route.ts @@ -9,7 +9,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { createListItemSchema, listItemSchema } from '../../common/schemas'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/create_list_route.ts b/x-pack/plugins/lists/server/routes/create_list_route.ts index 297dcfc49db34..90f5bf9b2c650 100644 --- a/x-pack/plugins/lists/server/routes/create_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { CreateListSchemaDecoded, createListSchema, listSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts index 2d5028bd9525a..380fdcf862060 100644 --- a/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { DeleteEndpointListItemSchemaDecoded, deleteEndpointListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts index 06ff051925407..07e0fad20c900 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { DeleteExceptionListItemSchemaDecoded, deleteExceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts index f2bf517f55ae3..769ce732240b7 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { DeleteExceptionListSchemaDecoded, deleteExceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/delete_list_index_route.ts b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts index be58d8aeed17d..aa587273036ae 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_INDEX } from '../../common/constants'; import { buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { acknowledgeSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/delete_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts index 50313cd1294ae..2284068552485 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { deleteListItemSchema, listItemArraySchema, listItemSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/delete_list_route.ts b/x-pack/plugins/lists/server/routes/delete_list_route.ts index 4eeb6d8f126ad..f87645b79fc75 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { deleteListSchema, listSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts index 9f83761cc501a..d6a459b3ac961 100644 --- a/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_ID, ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { FindEndpointListItemSchemaDecoded, findEndpointListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts index 270aad85796b2..88643e53ff0a7 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { FindExceptionListItemSchemaDecoded, findExceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts index c5cae7a1e0bb8..41342261ef681 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { FindExceptionListSchemaDecoded, findExceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/find_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_list_item_route.ts index 533dc74aa3694..454ea891857c3 100644 --- a/x-pack/plugins/lists/server/routes/find_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { FindListItemSchemaDecoded, findListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/find_list_route.ts b/x-pack/plugins/lists/server/routes/find_list_route.ts index 268eb36a5e26e..d751214006dcc 100644 --- a/x-pack/plugins/lists/server/routes/find_list_route.ts +++ b/x-pack/plugins/lists/server/routes/find_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { findListSchema, foundListSchema } from '../../common/schemas'; import { decodeCursor } from '../services/utils'; diff --git a/x-pack/plugins/lists/server/routes/import_list_item_route.ts b/x-pack/plugins/lists/server/routes/import_list_item_route.ts index e162e7829e456..ce5fdaccae251 100644 --- a/x-pack/plugins/lists/server/routes/import_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/import_list_item_route.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { importListItemQuerySchema, listSchema } from '../../common/schemas'; import { ConfigType } from '../config'; diff --git a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts index d975e80079ab7..58cca0313006d 100644 --- a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listItemSchema, patchListItemSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/patch_list_route.ts b/x-pack/plugins/lists/server/routes/patch_list_route.ts index 421f1279f2619..e33d8d7c9c598 100644 --- a/x-pack/plugins/lists/server/routes/patch_list_route.ts +++ b/x-pack/plugins/lists/server/routes/patch_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listSchema, patchListSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts index fd932746ce990..e80347d97bb7a 100644 --- a/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { ReadEndpointListItemSchemaDecoded, exceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts index fe8256fbda5cd..0cfac6467f089 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { ReadExceptionListItemSchemaDecoded, exceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts index 0512876d298d4..d9359881616f4 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { ReadExceptionListSchemaDecoded, exceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/read_list_index_route.ts b/x-pack/plugins/lists/server/routes/read_list_index_route.ts index 87a4d85e0d254..5524c1beeaa52 100644 --- a/x-pack/plugins/lists/server/routes/read_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_index_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_INDEX } from '../../common/constants'; import { buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listItemIndexExistSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/read_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_list_item_route.ts index b7cf2b9f7123b..99d34d0fd84a6 100644 --- a/x-pack/plugins/lists/server/routes/read_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_item_route.ts @@ -9,7 +9,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { listItemArraySchema, listItemSchema, readListItemSchema } from '../../common/schemas'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/read_list_route.ts b/x-pack/plugins/lists/server/routes/read_list_route.ts index 4bce09ecd3bde..da3cf73b56819 100644 --- a/x-pack/plugins/lists/server/routes/read_list_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listSchema, readListSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts index f717dc0fb3392..e0d6a0ffffa6b 100644 --- a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { UpdateEndpointListItemSchemaDecoded, exceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts index f5e0e7ae75700..7e15f694aee13 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { UpdateExceptionListItemSchemaDecoded, exceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts index 6fcee81ed573f..bead10802df4f 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { UpdateExceptionListSchemaDecoded, exceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/update_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_list_item_route.ts index d479bc63b64bd..3490027b12747 100644 --- a/x-pack/plugins/lists/server/routes/update_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listItemSchema, updateListItemSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/update_list_route.ts b/x-pack/plugins/lists/server/routes/update_list_route.ts index 6206c0943a8f3..816ad13d3770e 100644 --- a/x-pack/plugins/lists/server/routes/update_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listSchema, updateListSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/validate.ts b/x-pack/plugins/lists/server/routes/validate.ts index bbd4b0eaf0e33..a7f5c96e13d7b 100644 --- a/x-pack/plugins/lists/server/routes/validate.ts +++ b/x-pack/plugins/lists/server/routes/validate.ts @@ -8,7 +8,7 @@ import { ExceptionListClient } from '../services/exception_lists/exception_list_ import { MAX_EXCEPTION_LIST_SIZE } from '../../common/constants'; import { foundExceptionListItemSchema } from '../../common/schemas'; import { NamespaceType } from '../../common/schemas/types'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; export const validateExceptionListSize = async ( exceptionLists: ExceptionListClient, diff --git a/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts b/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts index 205d61f204ba6..5c7243a1d15a3 100644 --- a/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts +++ b/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts @@ -9,7 +9,7 @@ import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { CursorOrUndefined, SortFieldOrUndefined } from '../../../common/schemas'; -import { exactCheck } from '../../../common/siem_common_deps'; +import { exactCheck } from '../../../common/shared_imports'; /** * Used only internally for this ad-hoc opaque cursor structure to keep track of the diff --git a/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts b/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts index 2cebaacc67681..2d37d4a345fa1 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts @@ -15,86 +15,13 @@ import { getLanguageBooleanOperator, buildNested, } from './build_exceptions_query'; -import { - EntryNested, - EntryExists, - EntryMatch, - EntryMatchAny, - EntriesArray, - Operator, -} from '../../../lists/common/schemas'; +import { EntryNested, EntryMatchAny, EntriesArray } from '../../../lists/common/schemas'; import { getExceptionListItemSchemaMock } from '../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getEntryMatchMock } from '../../../lists/common/schemas/types/entry_match.mock'; import { getEntryMatchAnyMock } from '../../../lists/common/schemas/types/entry_match_any.mock'; import { getEntryExistsMock } from '../../../lists/common/schemas/types/entry_exists.mock'; describe('build_exceptions_query', () => { - const makeMatchEntry = ({ - field, - value = 'value-1', - operator = 'included', - }: { - field: string; - value?: string; - operator?: Operator; - }): EntryMatch => { - return { - field, - operator, - type: 'match', - value, - }; - }; - const makeMatchAnyEntry = ({ - field, - operator = 'included', - value = ['value-1', 'value-2'], - }: { - field: string; - operator?: Operator; - value?: string[]; - }): EntryMatchAny => { - return { - field, - operator, - value, - type: 'match_any', - }; - }; - const makeExistsEntry = ({ - field, - operator = 'included', - }: { - field: string; - operator?: Operator; - }): EntryExists => { - return { - field, - operator, - type: 'exists', - }; - }; - const matchEntryWithIncluded: EntryMatch = makeMatchEntry({ - field: 'host.name', - value: 'suricata', - }); - const matchEntryWithExcluded: EntryMatch = makeMatchEntry({ - field: 'host.name', - value: 'suricata', - operator: 'excluded', - }); - const matchAnyEntryWithIncludedAndTwoValues: EntryMatchAny = makeMatchAnyEntry({ - field: 'host.name', - value: ['suricata', 'auditd'], - }); - const existsEntryWithIncluded: EntryExists = makeExistsEntry({ - field: 'host.name', - }); - const existsEntryWithExcluded: EntryExists = makeExistsEntry({ - field: 'host.name', - operator: 'excluded', - }); - describe('getLanguageBooleanOperator', () => { test('it returns value as uppercase if language is "lucene"', () => { const result = getLanguageBooleanOperator({ language: 'lucene', value: 'not' }); @@ -137,14 +64,14 @@ describe('build_exceptions_query', () => { describe('kuery', () => { test('it returns formatted wildcard string when operator is "excluded"', () => { const query = buildExists({ - entry: existsEntryWithExcluded, + entry: { ...getEntryExistsMock(), operator: 'excluded' }, language: 'kuery', }); expect(query).toEqual('not host.name:*'); }); test('it returns formatted wildcard string when operator is "included"', () => { const query = buildExists({ - entry: existsEntryWithIncluded, + entry: { ...getEntryExistsMock(), operator: 'included' }, language: 'kuery', }); expect(query).toEqual('host.name:*'); @@ -154,14 +81,14 @@ describe('build_exceptions_query', () => { describe('lucene', () => { test('it returns formatted wildcard string when operator is "excluded"', () => { const query = buildExists({ - entry: existsEntryWithExcluded, + entry: { ...getEntryExistsMock(), operator: 'excluded' }, language: 'lucene', }); expect(query).toEqual('NOT _exists_host.name'); }); test('it returns formatted wildcard string when operator is "included"', () => { const query = buildExists({ - entry: existsEntryWithIncluded, + entry: { ...getEntryExistsMock(), operator: 'included' }, language: 'lucene', }); expect(query).toEqual('_exists_host.name'); @@ -173,52 +100,55 @@ describe('build_exceptions_query', () => { describe('kuery', () => { test('it returns formatted string when operator is "included"', () => { const query = buildMatch({ - entry: matchEntryWithIncluded, + entry: { ...getEntryMatchMock(), operator: 'included' }, language: 'kuery', }); - expect(query).toEqual('host.name:"suricata"'); + expect(query).toEqual('host.name:"some host name"'); }); test('it returns formatted string when operator is "excluded"', () => { const query = buildMatch({ - entry: matchEntryWithExcluded, + entry: { ...getEntryMatchMock(), operator: 'excluded' }, language: 'kuery', }); - expect(query).toEqual('not host.name:"suricata"'); + expect(query).toEqual('not host.name:"some host name"'); }); }); describe('lucene', () => { test('it returns formatted string when operator is "included"', () => { const query = buildMatch({ - entry: matchEntryWithIncluded, + entry: { ...getEntryMatchMock(), operator: 'included' }, language: 'lucene', }); - expect(query).toEqual('host.name:"suricata"'); + expect(query).toEqual('host.name:"some host name"'); }); test('it returns formatted string when operator is "excluded"', () => { const query = buildMatch({ - entry: matchEntryWithExcluded, + entry: { ...getEntryMatchMock(), operator: 'excluded' }, language: 'lucene', }); - expect(query).toEqual('NOT host.name:"suricata"'); + expect(query).toEqual('NOT host.name:"some host name"'); }); }); }); describe('buildMatchAny', () => { - const entryWithIncludedAndNoValues: EntryMatchAny = makeMatchAnyEntry({ + const entryWithIncludedAndNoValues: EntryMatchAny = { + ...getEntryMatchAnyMock(), field: 'host.name', value: [], - }); - const entryWithIncludedAndOneValue: EntryMatchAny = makeMatchAnyEntry({ + }; + const entryWithIncludedAndOneValue: EntryMatchAny = { + ...getEntryMatchAnyMock(), field: 'host.name', - value: ['suricata'], - }); - const entryWithExcludedAndTwoValues: EntryMatchAny = makeMatchAnyEntry({ + value: ['some host name'], + }; + const entryWithExcludedAndTwoValues: EntryMatchAny = { + ...getEntryMatchAnyMock(), field: 'host.name', - value: ['suricata', 'auditd'], + value: ['some host name', 'auditd'], operator: 'excluded', - }); + }; describe('kuery', () => { test('it returns empty string if given an empty array for "values"', () => { @@ -235,16 +165,16 @@ describe('build_exceptions_query', () => { language: 'kuery', }); - expect(exceptionSegment).toEqual('host.name:("suricata")'); + expect(exceptionSegment).toEqual('host.name:("some host name")'); }); test('it returns formatted string when operator is "included"', () => { const exceptionSegment = buildMatchAny({ - entry: matchAnyEntryWithIncludedAndTwoValues, + entry: { ...getEntryMatchAnyMock(), value: ['some host name', 'auditd'] }, language: 'kuery', }); - expect(exceptionSegment).toEqual('host.name:("suricata" or "auditd")'); + expect(exceptionSegment).toEqual('host.name:("some host name" or "auditd")'); }); test('it returns formatted string when operator is "excluded"', () => { @@ -253,18 +183,18 @@ describe('build_exceptions_query', () => { language: 'kuery', }); - expect(exceptionSegment).toEqual('not host.name:("suricata" or "auditd")'); + expect(exceptionSegment).toEqual('not host.name:("some host name" or "auditd")'); }); }); describe('lucene', () => { test('it returns formatted string when operator is "included"', () => { const exceptionSegment = buildMatchAny({ - entry: matchAnyEntryWithIncludedAndTwoValues, + entry: { ...getEntryMatchAnyMock(), value: ['some host name', 'auditd'] }, language: 'lucene', }); - expect(exceptionSegment).toEqual('host.name:("suricata" OR "auditd")'); + expect(exceptionSegment).toEqual('host.name:("some host name" OR "auditd")'); }); test('it returns formatted string when operator is "excluded"', () => { const exceptionSegment = buildMatchAny({ @@ -272,7 +202,7 @@ describe('build_exceptions_query', () => { language: 'lucene', }); - expect(exceptionSegment).toEqual('NOT host.name:("suricata" OR "auditd")'); + expect(exceptionSegment).toEqual('NOT host.name:("some host name" OR "auditd")'); }); test('it returns formatted string when "values" includes only one item', () => { const exceptionSegment = buildMatchAny({ @@ -280,7 +210,7 @@ describe('build_exceptions_query', () => { language: 'lucene', }); - expect(exceptionSegment).toEqual('host.name:("suricata")'); + expect(exceptionSegment).toEqual('host.name:("some host name")'); }); }); }); @@ -394,7 +324,7 @@ describe('build_exceptions_query', () => { describe('kuery', () => { test('it returns formatted wildcard string when "type" is "exists"', () => { const result = buildEntry({ - entry: existsEntryWithIncluded, + entry: { ...getEntryExistsMock(), operator: 'included' }, language: 'kuery', }); expect(result).toEqual('host.name:*'); @@ -402,25 +332,25 @@ describe('build_exceptions_query', () => { test('it returns formatted string when "type" is "match"', () => { const result = buildEntry({ - entry: matchEntryWithIncluded, + entry: { ...getEntryMatchMock(), operator: 'included' }, language: 'kuery', }); - expect(result).toEqual('host.name:"suricata"'); + expect(result).toEqual('host.name:"some host name"'); }); test('it returns formatted string when "type" is "match_any"', () => { const result = buildEntry({ - entry: matchAnyEntryWithIncludedAndTwoValues, + entry: { ...getEntryMatchAnyMock(), value: ['some host name', 'auditd'] }, language: 'kuery', }); - expect(result).toEqual('host.name:("suricata" or "auditd")'); + expect(result).toEqual('host.name:("some host name" or "auditd")'); }); }); describe('lucene', () => { test('it returns formatted wildcard string when "type" is "exists"', () => { const result = buildEntry({ - entry: existsEntryWithIncluded, + entry: { ...getEntryExistsMock(), operator: 'included' }, language: 'lucene', }); expect(result).toEqual('_exists_host.name'); @@ -428,18 +358,18 @@ describe('build_exceptions_query', () => { test('it returns formatted string when "type" is "match"', () => { const result = buildEntry({ - entry: matchEntryWithIncluded, + entry: { ...getEntryMatchMock(), operator: 'included' }, language: 'lucene', }); - expect(result).toEqual('host.name:"suricata"'); + expect(result).toEqual('host.name:"some host name"'); }); test('it returns formatted string when "type" is "match_any"', () => { const result = buildEntry({ - entry: matchAnyEntryWithIncludedAndTwoValues, + entry: { ...getEntryMatchAnyMock(), value: ['some host name', 'auditd'] }, language: 'lucene', }); - expect(result).toEqual('host.name:("suricata" OR "auditd")'); + expect(result).toEqual('host.name:("some host name" OR "auditd")'); }); }); }); @@ -456,26 +386,31 @@ describe('build_exceptions_query', () => { test('it returns expected query when more than one item in exception item', () => { const payload: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), - makeMatchEntry({ field: 'c', operator: 'excluded', value: 'value-3' }), + { ...getEntryMatchAnyMock(), field: 'b' }, + { ...getEntryMatchMock(), field: 'c', operator: 'excluded', value: 'value-3' }, ]; const query = buildExceptionItem({ language: 'kuery', entries: payload, }); - const expectedQuery = 'b:("value-1" or "value-2") and not c:"value-3"'; + const expectedQuery = 'b:("some host name") and not c:"value-3"'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when exception item includes nested value', () => { const entries: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), + { ...getEntryMatchAnyMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'nestedField', operator: 'included', value: 'value-3' }), + { + ...getEntryMatchMock(), + field: 'nestedField', + operator: 'included', + value: 'value-3', + }, ], }, ]; @@ -483,56 +418,65 @@ describe('build_exceptions_query', () => { language: 'kuery', entries, }); - const expectedQuery = 'b:("value-1" or "value-2") and parent:{ nestedField:"value-3" }'; + const expectedQuery = 'b:("some host name") and parent:{ nestedField:"value-3" }'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when exception item includes multiple items and nested "and" values', () => { const entries: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), + { ...getEntryMatchAnyMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'nestedField', operator: 'included', value: 'value-3' }), + { + ...getEntryMatchMock(), + field: 'nestedField', + operator: 'included', + value: 'value-3', + }, ], }, - makeExistsEntry({ field: 'd' }), + { ...getEntryExistsMock(), field: 'd' }, ]; const query = buildExceptionItem({ language: 'kuery', entries, }); - const expectedQuery = - 'b:("value-1" or "value-2") and parent:{ nestedField:"value-3" } and d:*'; + const expectedQuery = 'b:("some host name") and parent:{ nestedField:"value-3" } and d:*'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when language is "lucene"', () => { const entries: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), + { ...getEntryMatchAnyMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'nestedField', operator: 'excluded', value: 'value-3' }), + { + ...getEntryMatchMock(), + field: 'nestedField', + operator: 'excluded', + value: 'value-3', + }, ], }, - makeExistsEntry({ field: 'e', operator: 'excluded' }), + { ...getEntryExistsMock(), field: 'e', operator: 'excluded' }, ]; const query = buildExceptionItem({ language: 'lucene', entries, }); const expectedQuery = - 'b:("value-1" OR "value-2") AND parent:{ NOT nestedField:"value-3" } AND NOT _exists_e'; + 'b:("some host name") AND parent:{ NOT nestedField:"value-3" } AND NOT _exists_e'; expect(query).toEqual(expectedQuery); }); describe('exists', () => { test('it returns expected query when list includes single list item with operator of "included"', () => { - const entries: EntriesArray = [makeExistsEntry({ field: 'b' })]; + const entries: EntriesArray = [{ ...getEntryExistsMock(), field: 'b' }]; const query = buildExceptionItem({ language: 'kuery', entries, @@ -543,7 +487,9 @@ describe('build_exceptions_query', () => { }); test('it returns expected query when list includes single list item with operator of "excluded"', () => { - const entries: EntriesArray = [makeExistsEntry({ field: 'b', operator: 'excluded' })]; + const entries: EntriesArray = [ + { ...getEntryExistsMock(), field: 'b', operator: 'excluded' }, + ]; const query = buildExceptionItem({ language: 'kuery', entries, @@ -555,11 +501,13 @@ describe('build_exceptions_query', () => { test('it returns expected query when exception item includes entry item with "and" values', () => { const entries: EntriesArray = [ - makeExistsEntry({ field: 'b', operator: 'excluded' }), + { ...getEntryExistsMock(), field: 'b', operator: 'excluded' }, { field: 'parent', type: 'nested', - entries: [makeMatchEntry({ field: 'c', operator: 'included', value: 'value-1' })], + entries: [ + { ...getEntryMatchMock(), field: 'c', operator: 'included', value: 'value-1' }, + ], }, ]; const query = buildExceptionItem({ @@ -573,16 +521,16 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes multiple items', () => { const entries: EntriesArray = [ - makeExistsEntry({ field: 'b' }), + { ...getEntryExistsMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'c', operator: 'excluded', value: 'value-1' }), - makeMatchEntry({ field: 'd', value: 'value-2' }), + { ...getEntryMatchMock(), field: 'c', operator: 'excluded', value: 'value-1' }, + { ...getEntryMatchMock(), field: 'd', value: 'value-2' }, ], }, - makeExistsEntry({ field: 'e' }), + { ...getEntryExistsMock(), field: 'e' }, ]; const query = buildExceptionItem({ language: 'kuery', @@ -596,7 +544,7 @@ describe('build_exceptions_query', () => { describe('match', () => { test('it returns expected query when list includes single list item with operator of "included"', () => { - const entries: EntriesArray = [makeMatchEntry({ field: 'b', value: 'value' })]; + const entries: EntriesArray = [{ ...getEntryMatchMock(), field: 'b', value: 'value' }]; const query = buildExceptionItem({ language: 'kuery', entries, @@ -608,7 +556,7 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes single list item with operator of "excluded"', () => { const entries: EntriesArray = [ - makeMatchEntry({ field: 'b', operator: 'excluded', value: 'value' }), + { ...getEntryMatchMock(), field: 'b', operator: 'excluded', value: 'value' }, ]; const query = buildExceptionItem({ language: 'kuery', @@ -621,11 +569,13 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes list item with "and" values', () => { const entries: EntriesArray = [ - makeMatchEntry({ field: 'b', operator: 'excluded', value: 'value' }), + { ...getEntryMatchMock(), field: 'b', operator: 'excluded', value: 'value' }, { field: 'parent', type: 'nested', - entries: [makeMatchEntry({ field: 'c', operator: 'included', value: 'valueC' })], + entries: [ + { ...getEntryMatchMock(), field: 'c', operator: 'included', value: 'valueC' }, + ], }, ]; const query = buildExceptionItem({ @@ -639,16 +589,16 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes multiple items', () => { const entries: EntriesArray = [ - makeMatchEntry({ field: 'b', value: 'value' }), + { ...getEntryMatchMock(), field: 'b', value: 'value' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'c', operator: 'excluded', value: 'valueC' }), - makeMatchEntry({ field: 'd', operator: 'excluded', value: 'valueD' }), + { ...getEntryMatchMock(), field: 'c', operator: 'excluded', value: 'valueC' }, + { ...getEntryMatchMock(), field: 'd', operator: 'excluded', value: 'valueD' }, ], }, - makeMatchEntry({ field: 'e', value: 'valueE' }), + { ...getEntryMatchMock(), field: 'e', value: 'valueE' }, ]; const query = buildExceptionItem({ language: 'kuery', @@ -663,55 +613,59 @@ describe('build_exceptions_query', () => { describe('match_any', () => { test('it returns expected query when list includes single list item with operator of "included"', () => { - const entries: EntriesArray = [makeMatchAnyEntry({ field: 'b' })]; + const entries: EntriesArray = [{ ...getEntryMatchAnyMock(), field: 'b' }]; const query = buildExceptionItem({ language: 'kuery', entries, }); - const expectedQuery = 'b:("value-1" or "value-2")'; + const expectedQuery = 'b:("some host name")'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when list includes single list item with operator of "excluded"', () => { - const entries: EntriesArray = [makeMatchAnyEntry({ field: 'b', operator: 'excluded' })]; + const entries: EntriesArray = [ + { ...getEntryMatchAnyMock(), field: 'b', operator: 'excluded' }, + ]; const query = buildExceptionItem({ language: 'kuery', entries, }); - const expectedQuery = 'not b:("value-1" or "value-2")'; + const expectedQuery = 'not b:("some host name")'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when list includes list item with nested values', () => { const entries: EntriesArray = [ - makeMatchAnyEntry({ field: 'b', operator: 'excluded' }), + { ...getEntryMatchAnyMock(), field: 'b', operator: 'excluded' }, { field: 'parent', type: 'nested', - entries: [makeMatchEntry({ field: 'c', operator: 'excluded', value: 'valueC' })], + entries: [ + { ...getEntryMatchMock(), field: 'c', operator: 'excluded', value: 'valueC' }, + ], }, ]; const query = buildExceptionItem({ language: 'kuery', entries, }); - const expectedQuery = 'not b:("value-1" or "value-2") and parent:{ not c:"valueC" }'; + const expectedQuery = 'not b:("some host name") and parent:{ not c:"valueC" }'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when list includes multiple items', () => { const entries: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), - makeMatchAnyEntry({ field: 'c' }), + { ...getEntryMatchAnyMock(), field: 'b' }, + { ...getEntryMatchAnyMock(), field: 'c' }, ]; const query = buildExceptionItem({ language: 'kuery', entries, }); - const expectedQuery = 'b:("value-1" or "value-2") and c:("value-1" or "value-2")'; + const expectedQuery = 'b:("some host name") and c:("some host name")'; expect(query).toEqual(expectedQuery); }); @@ -735,16 +689,16 @@ describe('build_exceptions_query', () => { const payload = getExceptionListItemSchemaMock(); const payload2 = getExceptionListItemSchemaMock(); payload2.entries = [ - makeMatchAnyEntry({ field: 'b' }), + { ...getEntryMatchAnyMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'c', operator: 'included', value: 'valueC' }), - makeMatchEntry({ field: 'd', operator: 'included', value: 'valueD' }), + { ...getEntryMatchMock(), field: 'c', operator: 'included', value: 'valueC' }, + { ...getEntryMatchMock(), field: 'd', operator: 'included', value: 'valueD' }, ], }, - makeMatchAnyEntry({ field: 'e', operator: 'excluded' }), + { ...getEntryMatchAnyMock(), field: 'e', operator: 'excluded' }, ]; const queries = buildExceptionListQueries({ language: 'kuery', @@ -758,7 +712,7 @@ describe('build_exceptions_query', () => { }, { query: - 'b:("value-1" or "value-2") and parent:{ c:"valueC" and d:"valueD" } and not e:("value-1" or "value-2")', + 'b:("some host name") and parent:{ c:"valueC" and d:"valueD" } and not e:("some host name")', language: 'kuery', }, ]; @@ -768,20 +722,26 @@ describe('build_exceptions_query', () => { test('it returns expected query when lists exist and language is "lucene"', () => { const payload = getExceptionListItemSchemaMock(); - payload.entries = [makeMatchAnyEntry({ field: 'a' }), makeMatchAnyEntry({ field: 'b' })]; + payload.entries = [ + { ...getEntryMatchAnyMock(), field: 'a' }, + { ...getEntryMatchAnyMock(), field: 'b' }, + ]; const payload2 = getExceptionListItemSchemaMock(); - payload2.entries = [makeMatchAnyEntry({ field: 'c' }), makeMatchAnyEntry({ field: 'd' })]; + payload2.entries = [ + { ...getEntryMatchAnyMock(), field: 'c' }, + { ...getEntryMatchAnyMock(), field: 'd' }, + ]; const queries = buildExceptionListQueries({ language: 'lucene', lists: [payload, payload2], }); const expectedQueries = [ { - query: 'a:("value-1" OR "value-2") AND b:("value-1" OR "value-2")', + query: 'a:("some host name") AND b:("some host name")', language: 'lucene', }, { - query: 'c:("value-1" OR "value-2") AND d:("value-1" OR "value-2")', + query: 'c:("some host name") AND d:("some host name")', language: 'lucene', }, ]; @@ -793,17 +753,17 @@ describe('build_exceptions_query', () => { const payload = getExceptionListItemSchemaMock(); const payload2 = getExceptionListItemSchemaMock(); payload2.entries = [ - makeMatchAnyEntry({ field: 'b' }), + { ...getEntryMatchAnyMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ // TODO: these operators are not being respected. buildNested needs to be updated - makeMatchEntry({ field: 'c', operator: 'excluded', value: 'valueC' }), - makeMatchEntry({ field: 'd', operator: 'excluded', value: 'valueD' }), + { ...getEntryMatchMock(), field: 'c', operator: 'excluded', value: 'valueC' }, + { ...getEntryMatchMock(), field: 'd', operator: 'excluded', value: 'valueD' }, ], }, - makeMatchAnyEntry({ field: 'e' }), + { ...getEntryMatchAnyMock(), field: 'e' }, ]; const queries = buildExceptionListQueries({ language: 'kuery', @@ -817,7 +777,7 @@ describe('build_exceptions_query', () => { }, { query: - 'b:("value-1" or "value-2") and parent:{ not c:"valueC" and not d:"valueD" } and e:("value-1" or "value-2")', + 'b:("some host name") and parent:{ not c:"valueC" and not d:"valueD" } and e:("some host name")', language: 'kuery', }, ]; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx index e8a5196a418d6..224c99756eb5c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx @@ -466,7 +466,7 @@ describe('Exception builder helpers', () => { describe('#isEntryNested', () => { test('it returns "false" if payload is not of type EntryNested', () => { - const payload: BuilderEntry = { ...getEntryMatchMock() }; + const payload: BuilderEntry = getEntryMatchMock(); const output = isEntryNested(payload); const expected = false; expect(output).toEqual(expected); @@ -483,7 +483,7 @@ describe('Exception builder helpers', () => { describe('#getFormattedBuilderEntries', () => { test('it returns formatted entry with field undefined if it unable to find a matching index pattern field', () => { const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); - const payloadItems: BuilderEntry[] = [{ ...getEntryMatchMock() }]; + const payloadItems: BuilderEntry[] = [getEntryMatchMock()]; const output = getFormattedBuilderEntries(payloadIndexPattern, payloadItems); const expected: FormattedBuilderEntry[] = [ { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 18b509d16b352..4236f347ac7ff 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -365,7 +365,7 @@ describe('Exception helpers', () => { const mockEmptyException: EntryNested = { field: '', type: OperatorTypeEnum.NESTED, - entries: [{ ...getEntryMatchMock() }], + entries: [getEntryMatchMock()], }; const output: Array< ExceptionListItemSchema | CreateExceptionListItemSchema From aa668db796ccc0fd63bb41b94548740305106cce Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Thu, 30 Jul 2020 19:01:46 -0700 Subject: [PATCH 25/33] [Metrics UI] Fix alert management to open without refresh (#73739) * [Metrics UI] Fix alert management to open without refresh * removing unecessary code * Deleting unused imports Co-authored-by: Elastic Machine --- .../inventory/components/alert_dropdown.tsx | 31 +++++-------------- .../manage_alerts_context_menu_item.tsx | 22 +++++++++++++ .../components/alert_dropdown.tsx | 31 +++++-------------- 3 files changed, 38 insertions(+), 46 deletions(-) create mode 100644 x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx index 04642a01c15b4..ce0911666f0db 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx @@ -4,17 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useCallback, useMemo } from 'react'; +import React, { useState, useCallback } from 'react'; import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill'; import { AlertFlyout } from './alert_flyout'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { ManageAlertsContextMenuItem } from './manage_alerts_context_menu_item'; export const InventoryAlertDropdown = () => { const [popoverOpen, setPopoverOpen] = useState(false); const [flyoutVisible, setFlyoutVisible] = useState(false); - const kibana = useKibana(); const { inventoryPrefill } = useAlertPrefillContext(); const { nodeType, metric, filterQuery } = inventoryPrefill; @@ -27,26 +26,12 @@ export const InventoryAlertDropdown = () => { setPopoverOpen(true); }, [setPopoverOpen]); - const menuItems = useMemo(() => { - return [ - setFlyoutVisible(true)}> - - , - - - , - ]; - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [kibana.services]); + const menuItems = [ + setFlyoutVisible(true)}> + + , + , + ]; return ( <> diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx new file mode 100644 index 0000000000000..fc565aee37ff4 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx @@ -0,0 +1,22 @@ +/* + * 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 { EuiContextMenuItem } from '@elastic/eui'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useLinkProps } from '../../../hooks/use_link_props'; + +export const ManageAlertsContextMenuItem = () => { + const manageAlertsLinkProps = useLinkProps({ + app: 'management', + pathname: '/insightsAndAlerting/triggersActions/alerts', + }); + return ( + + + + ); +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx index 384a93e796dbe..dd61be0eee362 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx @@ -4,17 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useCallback, useMemo } from 'react'; +import React, { useState, useCallback } from 'react'; import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useAlertPrefillContext } from '../../use_alert_prefill'; import { AlertFlyout } from './alert_flyout'; +import { ManageAlertsContextMenuItem } from '../../inventory/components/manage_alerts_context_menu_item'; export const MetricsAlertDropdown = () => { const [popoverOpen, setPopoverOpen] = useState(false); const [flyoutVisible, setFlyoutVisible] = useState(false); - const kibana = useKibana(); const { metricThresholdPrefill } = useAlertPrefillContext(); const { groupBy, filterQuery, metrics } = metricThresholdPrefill; @@ -27,26 +26,12 @@ export const MetricsAlertDropdown = () => { setPopoverOpen(true); }, [setPopoverOpen]); - const menuItems = useMemo(() => { - return [ - setFlyoutVisible(true)}> - - , - - - , - ]; - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [kibana.services]); + const menuItems = [ + setFlyoutVisible(true)}> + + , + , + ]; return ( <> From 0a5427842b8f4810a9a2a44fbe625d1d50c3e070 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 30 Jul 2020 21:08:57 -0600 Subject: [PATCH 26/33] [SIEM] Fixes "include building block button" to operate (#73900) ## Summary Blocker fixes "include building block button" to operate when there is no data on the table. Before if you had nothing on the table then the button would not operate as it would not cause a re-render: ![button_not_working](https://user-images.githubusercontent.com/1151048/88980376-cde1de00-d280-11ea-98cf-b67ef9fe9f72.gif) After where the button now works: ![button_working](https://user-images.githubusercontent.com/1151048/88980385-d3d7bf00-d280-11ea-89e4-f806e62853ed.gif) This wasn't caught because most people have something already on the table which makes the rendering render and just work. Simple one line low level fix. ### Checklist Delete any items that are not applicable to this PR. - [ ] [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 No tests for this file at the moment and we need this as a fast backport to make the release cut off. --- .../alerts_utility_bar/index.test.tsx | 188 +++++++++++++++++- .../alerts_table/alerts_utility_bar/index.tsx | 5 +- 2 files changed, 188 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx index cbbe43cc03568..0ba9764cf24af 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx @@ -5,14 +5,15 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, mount } from 'enzyme'; -import { AlertsUtilityBar } from './index'; +import { AlertsUtilityBar, AlertsUtilityBarProps } from './index'; +import { TestProviders } from '../../../../common/mock/test_providers'; jest.mock('../../../../common/lib/kibana'); describe('AlertsUtilityBar', () => { - it('renders correctly', () => { + test('renders correctly', () => { const wrapper = shallow( { expect(wrapper.find('[dataTestSubj="alertActionPopover"]')).toBeTruthy(); }); + + describe('UtilityBarAdditionalFiltersContent', () => { + test('does not show the showBuildingBlockAlerts checked if the showBuildingBlockAlerts is false', () => { + const onShowBuildingBlockAlertsChanged = jest.fn(); + const wrapper = mount( + + + + ); + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // The check box should be false + expect( + wrapper + .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') + .first() + .prop('checked') + ).toEqual(false); + }); + + test('does show the showBuildingBlockAlerts checked if the showBuildingBlockAlerts is true', () => { + const onShowBuildingBlockAlertsChanged = jest.fn(); + const wrapper = mount( + + + + ); + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // The check box should be true + expect( + wrapper + .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') + .first() + .prop('checked') + ).toEqual(true); + }); + + test('calls the onShowBuildingBlockAlertsChanged when the check box is clicked', () => { + const onShowBuildingBlockAlertsChanged = jest.fn(); + const wrapper = mount( + + + + ); + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // check the box + wrapper + .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') + .first() + .simulate('change', { target: { checked: true } }); + + // Make sure our callback is called + expect(onShowBuildingBlockAlertsChanged).toHaveBeenCalled(); + }); + + test('can update showBuildingBlockAlerts from false to true', () => { + const Proxy = (props: AlertsUtilityBarProps) => ( + + + + ); + + const wrapper = mount( + + ); + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // The check box should false now since we initially set the showBuildingBlockAlerts to false + expect( + wrapper + .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') + .first() + .prop('checked') + ).toEqual(false); + + wrapper.setProps({ showBuildingBlockAlerts: true }); + wrapper.update(); + + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // The check box should be true now since we changed the showBuildingBlockAlerts from false to true + expect( + wrapper + .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') + .first() + .prop('checked') + ).toEqual(true); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx index bedc23790541c..bdad380f59ae9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx @@ -28,7 +28,7 @@ import { TimelineNonEcsData } from '../../../../graphql/types'; import { UpdateAlertsStatus } from '../types'; import { FILTER_CLOSED, FILTER_IN_PROGRESS, FILTER_OPEN } from '../alerts_filter_group'; -interface AlertsUtilityBarProps { +export interface AlertsUtilityBarProps { canUserCRUD: boolean; hasIndexWrite: boolean; areEventsLoading: boolean; @@ -223,5 +223,6 @@ export const AlertsUtilityBar = React.memo( prevProps.areEventsLoading === nextProps.areEventsLoading && prevProps.selectedEventIds === nextProps.selectedEventIds && prevProps.totalCount === nextProps.totalCount && - prevProps.showClearSelection === nextProps.showClearSelection + prevProps.showClearSelection === nextProps.showClearSelection && + prevProps.showBuildingBlockAlerts === nextProps.showBuildingBlockAlerts ); From d3f498af9aef9bda7f0ee51aa08b7faa728f317e Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Thu, 30 Jul 2020 22:37:54 -0500 Subject: [PATCH 27/33] [Metrics UI] Fix alert previews of ungrouped alerts (#73735) Co-authored-by: Elastic Machine --- .../alerting/metric_threshold/components/expression.tsx | 7 ++++++- .../metric_threshold/components/expression_chart.tsx | 2 +- .../hooks/use_metrics_explorer_chart_data.ts | 2 +- .../infra/public/alerting/metric_threshold/types.ts | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index cd1e93a2a0c96..f990578a471be 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -256,6 +256,11 @@ export const Expressions: React.FC = (props) => { [onFilterChange] ); + const groupByPreviewDisplayName = useMemo(() => { + if (Array.isArray(alertParams.groupBy)) return alertParams.groupBy.join(', '); + return alertParams.groupBy; + }, [alertParams.groupBy]); + return ( <> @@ -400,7 +405,7 @@ export const Expressions: React.FC = (props) => { showNoDataResults={alertParams.alertOnNoData} validate={validateMetricThreshold} fetch={alertsContext.http.fetch} - groupByDisplayName={alertParams.groupBy} + groupByDisplayName={groupByPreviewDisplayName} /> diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx index cdb6b341c7299..c90c534193fdc 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx @@ -45,7 +45,7 @@ interface Props { derivedIndexPattern: IIndexPattern; source: InfraSource | null; filterQuery?: string; - groupBy?: string; + groupBy?: string | string[]; } const tooltipProps = { diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts index 185895062cfe2..a3d09742e9a57 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts @@ -19,7 +19,7 @@ export const useMetricsExplorerChartData = ( derivedIndexPattern: IIndexPattern, source: InfraSource | null, filterQuery?: string, - groupBy?: string + groupBy?: string | string[] ) => { const { timeSize, timeUnit } = expression || { timeSize: 1, timeUnit: 'm' }; const options: MetricsExplorerOptions = useMemo( diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts index 58586c1dd8b98..b2317c558be44 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts @@ -53,7 +53,7 @@ export interface ExpressionChartData { export interface AlertParams { criteria: MetricExpression[]; - groupBy?: string; + groupBy?: string[]; filterQuery?: string; sourceId?: string; filterQueryText?: string; From 5fa162c69910d13ad771026235840056b1eaa6e0 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Thu, 30 Jul 2020 22:39:34 -0500 Subject: [PATCH 28/33] [Metrics UI] Fix all threshold alert conditions disappearing due to alert prefill (#73708) Co-authored-by: Elastic Machine --- .../alerting/metric_threshold/components/expression.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index f990578a471be..8bb8b3934b5fd 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -185,7 +185,7 @@ export const Expressions: React.FC = (props) => { const preFillAlertCriteria = useCallback(() => { const md = alertsContext.metadata; - if (md && md.currentOptions?.metrics) { + if (md?.currentOptions?.metrics?.length) { setAlertParams( 'criteria', md.currentOptions.metrics.map((metric) => ({ @@ -249,7 +249,7 @@ export const Expressions: React.FC = (props) => { if (!alertParams.sourceId) { setAlertParams('sourceId', source?.id || 'default'); } - }, [alertsContext.metadata, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps + }, [alertsContext.metadata, source]); // eslint-disable-line react-hooks/exhaustive-deps const handleFieldSearchChange = useCallback( (e: ChangeEvent) => onFilterChange(e.target.value), From 39cca0e55df05db3d165eb5c4756b4b37422972a Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Fri, 31 Jul 2020 06:52:36 +0200 Subject: [PATCH 29/33] [Discover] Improve saveSearch functional test handling (#73626) * Check for submit button to be disabled, before submitting the form to prevent occasional flakiness --- test/functional/page_objects/discover_page.ts | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 8f69bf629ce28..c558d9e2d8a31 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -17,11 +17,9 @@ * under the License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; export function DiscoverPageProvider({ getService, getPageObjects }: FtrProviderContext) { - const log = getService('log'); const retry = getService('retry'); const testSubjects = getService('testSubjects'); const find = getService('find'); @@ -51,9 +49,16 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider } public async saveSearch(searchName: string) { - log.debug('saveSearch'); await this.clickSaveSearchButton(); - await testSubjects.setValue('savedObjectTitle', searchName); + // preventing an occasional flakiness when the saved object wasn't set and the form can't be submitted + await retry.waitFor( + `saved search title is set to ${searchName} and save button is clickable`, + async () => { + const saveButton = await testSubjects.find('confirmSaveSavedObjectButton'); + await testSubjects.setValue('savedObjectTitle', searchName); + return (await saveButton.getAttribute('disabled')) !== 'true'; + } + ); await testSubjects.click('confirmSaveSavedObjectButton'); await header.waitUntilLoadingHasFinished(); // LeeDr - this additional checking for the saved search name was an attempt @@ -61,9 +66,8 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider // that the next action wouldn't have to retry. But it doesn't really solve // that issue. But it does typically take about 3 retries to // complete with the expected searchName. - await retry.try(async () => { - const name = await this.getCurrentQueryName(); - expect(name).to.be(searchName); + await retry.waitFor(`saved search was persisted with name ${searchName}`, async () => { + return (await this.getCurrentQueryName()) === searchName; }); } @@ -96,11 +100,11 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider // We need this try loop here because previous actions in Discover like // saving a search cause reloading of the page and the "Open" menu item goes stale. - await retry.try(async () => { + await retry.waitFor('saved search panel is opened', async () => { await this.clickLoadSavedSearchButton(); await header.waitUntilLoadingHasFinished(); isOpen = await testSubjects.exists('loadSearchForm'); - expect(isOpen).to.be(true); + return isOpen === true; }); } From 145f2eef57ca5b1bb1956a28fdb48db2dad345d8 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 31 Jul 2020 10:25:01 +0200 Subject: [PATCH 30/33] [ML] Migrate to React BrowserRouter and Kibana provided History. (#71941) - Migrate to React BrowserRouter and Kibana provided History including a fallback to redirect legacy hash based URLs. - Migrate breadcrumbs away from hash based URLs. - Make sure relative custom urls still work after migration. --- x-pack/plugins/ml/public/application/app.tsx | 10 +- .../components/anomalies_table/links_menu.js | 6 +- .../application/contexts/kibana/index.ts | 1 + .../contexts/kibana/use_navigate_to_path.ts | 34 +++++++ .../back_to_list_panel/back_to_list_panel.tsx | 10 +- .../components/action_clone/clone_button.tsx | 8 +- .../action_view/get_view_action.tsx | 10 +- .../components/action_view/view_button.tsx | 18 +--- .../components/analytics_list/use_actions.tsx | 2 +- .../source_selection/source_selection.tsx | 13 +-- .../datavisualizer_selector.tsx | 7 +- .../components/custom_url_editor/list.tsx | 4 +- .../edit_job_flyout/tabs/custom_urls.tsx | 7 +- .../common/job_creator/util/general.ts | 41 +++----- .../calendars/calendars_selection.tsx | 12 ++- .../single_metric_view/settings.tsx | 10 +- .../pages/components/summary_step/summary.tsx | 11 ++- .../new_job/pages/index_or_search/page.tsx | 11 ++- .../jobs/new_job/pages/job_type/page.tsx | 30 +++--- .../public/application/routing/breadcrumbs.ts | 49 ++++++++-- .../ml/public/application/routing/router.tsx | 97 ++++++++++++++----- .../routing/routes/access_denied.tsx | 4 +- .../analytics_job_creation.tsx | 34 ++++--- .../analytics_job_exploration.tsx | 38 ++++---- .../analytics_jobs_list.tsx | 30 +++--- .../routes/datavisualizer/datavisualizer.tsx | 15 +-- .../routes/datavisualizer/file_based.tsx | 30 +++--- .../routes/datavisualizer/index_based.tsx | 36 +++---- .../application/routing/routes/explorer.tsx | 30 +++--- .../application/routing/routes/index.ts | 2 +- .../application/routing/routes/jobs_list.tsx | 31 +++--- .../routes/new_job/index_or_search.tsx | 28 +++--- .../routing/routes/new_job/job_type.tsx | 34 ++++--- .../routing/routes/new_job/new_job.tsx | 20 +--- .../routing/routes/new_job/recognize.tsx | 43 ++++---- .../routing/routes/new_job/wizard.tsx | 64 ++++++------ .../application/routing/routes/overview.tsx | 35 +++---- .../routing/routes/settings/calendar_list.tsx | 32 +++--- .../routes/settings/calendar_new_edit.tsx | 56 ++++++----- .../routing/routes/settings/filter_list.tsx | 30 +++--- .../routes/settings/filter_list_new_edit.tsx | 57 ++++++----- .../routing/routes/settings/settings.tsx | 15 +-- .../routing/routes/timeseriesexplorer.tsx | 12 ++- .../application/util/custom_url_utils.test.ts | 46 +++++++++ .../application/util/custom_url_utils.ts | 30 ++++-- .../plugins/ml/public/url_generator.test.ts | 2 +- x-pack/plugins/ml/public/url_generator.ts | 2 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 49 files changed, 669 insertions(+), 480 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index cf645404860f5..cc3af9d7f4980 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -25,6 +25,7 @@ export type MlDependencies = Omit & MlStartDepende interface AppProps { coreStart: CoreStart; deps: MlDependencies; + appMountParams: AppMountParameters; } const localStorage = new Storage(window.localStorage); @@ -46,8 +47,9 @@ export interface MlServicesContext { export type MlGlobalServices = ReturnType; -const App: FC = ({ coreStart, deps }) => { +const App: FC = ({ coreStart, deps, appMountParams }) => { const pageDeps = { + history: appMountParams.history, indexPatterns: deps.data.indexPatterns, config: coreStart.uiSettings!, setBreadcrumbs: coreStart.chrome!.setBreadcrumbs, @@ -104,7 +106,11 @@ export const renderApp = ( appMountParams.onAppLeave((actions) => actions.default()); const mlLicense = setLicenseCache(deps.licensing, [ - () => ReactDOM.render(, appMountParams.element), + () => + ReactDOM.render( + , + appMountParams.element + ), ]); return () => { diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js index 4850d583a626c..f603264896cd3 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -62,6 +62,8 @@ class LinksMenuUI extends Component { const timestamp = record.timestamp; const configuredUrlValue = customUrl.url_value; const timeRangeInterval = parseInterval(customUrl.time_range); + const basePath = this.props.kibana.services.http.basePath.get(); + if (configuredUrlValue.includes('$earliest$')) { let earliestMoment = moment(timestamp); if (timeRangeInterval !== null) { @@ -117,7 +119,7 @@ class LinksMenuUI extends Component { // Replace any tokens in the configured url_value with values from the source record, // and then open link in a new tab/window. const urlPath = replaceStringTokens(customUrl.url_value, record, true); - openCustomUrlWindow(urlPath, customUrl); + openCustomUrlWindow(urlPath, customUrl, basePath); }) .catch((resp) => { console.log('openCustomUrl(): error loading categoryDefinition:', resp); @@ -136,7 +138,7 @@ class LinksMenuUI extends Component { // Replace any tokens in the configured url_value with values from the source record, // and then open link in a new tab/window. const urlPath = getUrlForRecord(customUrl, record); - openCustomUrlWindow(urlPath, customUrl); + openCustomUrlWindow(urlPath, customUrl, basePath); } }; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/index.ts b/x-pack/plugins/ml/public/application/contexts/kibana/index.ts index 0f071a42a5688..8a43ae12deb21 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/index.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/index.ts @@ -5,6 +5,7 @@ */ export { useMlKibana, StartServices, MlKibanaReactContextValue } from './kibana_context'; +export { useNavigateToPath, NavigateToPath } from './use_navigate_to_path'; export { useUiSettings } from './use_ui_settings_context'; export { useTimefilter } from './use_timefilter'; export { useNotifications } from './use_notifications_context'; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts b/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts new file mode 100644 index 0000000000000..f2db970bf5057 --- /dev/null +++ b/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; +import { PLUGIN_ID } from '../../../../common/constants/app'; + +import { useMlKibana } from './kibana_context'; + +export type NavigateToPath = ReturnType; + +export const useNavigateToPath = () => { + const { + services: { + application: { getUrlForApp, navigateToUrl }, + }, + } = useMlKibana(); + + const location = useLocation(); + + return useMemo( + () => (path: string | undefined, preserveSearch = false) => { + navigateToUrl( + getUrlForApp(PLUGIN_ID, { + path: `${path}${preserveSearch === true ? location.search : ''}`, + }) + ); + }, + [location] + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx index b6b335afa53f5..183cbe084f9b3 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx @@ -7,17 +7,13 @@ import React, { FC, Fragment } from 'react'; import { EuiCard, EuiHorizontalRule, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useNavigateToPath } from '../../../../../contexts/kibana'; export const BackToListPanel: FC = () => { - const { - services: { - application: { navigateToUrl }, - }, - } = useMlKibana(); + const navigateToPath = useNavigateToPath(); const redirectToAnalyticsManagementPage = async () => { - await navigateToUrl('#/data_frame_analytics?'); + await navigateToPath('/data_frame_analytics'); }; return ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx index 7a409e5238a57..010aa7b8513b5 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx @@ -13,7 +13,7 @@ import { DeepReadonly } from '../../../../../../../common/types/common'; import { DataFrameAnalyticsConfig, isOutlierAnalysis } from '../../../../common'; import { isClassificationAnalysis, isRegressionAnalysis } from '../../../../common/analytics'; import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana'; import { CreateAnalyticsFormProps, DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES, @@ -350,11 +350,11 @@ export function getCloneAction(createAnalyticsForm: CreateAnalyticsFormProps) { export const useNavigateToWizardWithClonedJob = () => { const { services: { - application: { navigateToUrl }, notifications: { toasts }, savedObjects, }, } = useMlKibana(); + const navigateToPath = useNavigateToPath(); const savedObjectsClient = savedObjects.client; @@ -395,8 +395,8 @@ export const useNavigateToWizardWithClonedJob = () => { } if (sourceIndexId) { - await navigateToUrl( - `ml#/data_frame_analytics/new_job?index=${encodeURIComponent(sourceIndexId)}&jobId=${ + await navigateToPath( + `/data_frame_analytics/new_job?index=${encodeURIComponent(sourceIndexId)}&jobId=${ item.config.id }` ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx index e31670ea42ceb..e123af204b515 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx @@ -12,11 +12,9 @@ import { DataFrameAnalyticsListRow } from '../analytics_list/common'; import { ViewButton } from './view_button'; -export const getViewAction = ( - isManagementTable: boolean = false -): EuiTableActionsColumnType['actions'][number] => ({ +export const getViewAction = (): EuiTableActionsColumnType< + DataFrameAnalyticsListRow +>['actions'][number] => ({ isPrimary: true, - render: (item: DataFrameAnalyticsListRow) => ( - - ), + render: (item: DataFrameAnalyticsListRow) => , }); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx index a0790cd802409..9472a3af852fc 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; import { getAnalysisType } from '../../../../common/analytics'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useNavigateToPath } from '../../../../../contexts/kibana'; import { getResultsUrl, DataFrameAnalyticsListRow } from '../analytics_list/common'; @@ -17,23 +17,15 @@ import { getViewLinkStatus } from './get_view_link_status'; interface ViewButtonProps { item: DataFrameAnalyticsListRow; - isManagementTable: boolean; } -export const ViewButton: FC = ({ item, isManagementTable }) => { - const { - services: { - application: { navigateToUrl, navigateToApp }, - }, - } = useMlKibana(); +export const ViewButton: FC = ({ item }) => { + const navigateToPath = useNavigateToPath(); const { disabled, tooltipContent } = getViewLinkStatus(item); const analysisType = getAnalysisType(item.config.analysis); - const url = getResultsUrl(item.id, analysisType); - const navigator = isManagementTable - ? () => navigateToApp('ml', { path: url }) - : () => navigateToUrl(url); + const onClickHandler = () => navigateToPath(getResultsUrl(item.id, analysisType)); const buttonText = i18n.translate('xpack.ml.dataframe.analyticsList.viewActionName', { defaultMessage: 'View', @@ -47,7 +39,7 @@ export const ViewButton: FC = ({ item, isManagementTable }) => flush="left" iconType="visTable" isDisabled={disabled} - onClick={navigator} + onClick={onClickHandler} size="xs" > {buttonText} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx index bc02c81bac0f0..373b9991d4d3c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx @@ -43,7 +43,7 @@ export const useActions = ( let modals: JSX.Element | null = null; const actions: EuiTableActionsColumnType['actions'] = [ - getViewAction(isManagementTable), + getViewAction(), ]; // isManagementTable will be the same for the lifecycle of the component diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx index b03a58a02309d..29d495062e309 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx @@ -17,7 +17,7 @@ import { } from '@elastic/eui'; import { SavedObjectFinderUi } from '../../../../../../../../../../src/plugins/saved_objects/public'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana'; const fixedPageSize: number = 8; @@ -27,16 +27,13 @@ interface Props { export const SourceSelection: FC = ({ onClose }) => { const { - services: { - application: { navigateToUrl }, - savedObjects, - uiSettings, - }, + services: { savedObjects, uiSettings }, } = useMlKibana(); + const navigateToPath = useNavigateToPath(); const onSearchSelected = async (id: string, type: string) => { - await navigateToUrl( - `ml#/data_frame_analytics/new_job?${ + await navigateToPath( + `/data_frame_analytics/new_job?${ type === 'index-pattern' ? 'index' : 'savedSearchId' }=${encodeURIComponent(id)}` ); diff --git a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx index fd86d9f48f46d..769b83c03110b 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { isFullLicense } from '../license'; -import { useTimefilter, useMlKibana } from '../contexts/kibana'; +import { useTimefilter, useMlKibana, useNavigateToPath } from '../contexts/kibana'; import { NavigationMenu } from '../components/navigation_menu'; import { getMaxBytesFormatted } from './file_based/components/utils'; @@ -54,6 +54,7 @@ export const DatavisualizerSelector: FC = () => { const { services: { licenseManagement }, } = useMlKibana(); + const navigateToPath = useNavigateToPath(); const startTrialVisible = licenseManagement !== undefined && @@ -124,7 +125,7 @@ export const DatavisualizerSelector: FC = () => { footer={ navigateToPath('/filedatavisualizer')} data-test-subj="mlDataVisualizerUploadFileButton" > { footer={ navigateToPath('/datavisualizer_index_select')} data-test-subj="mlDataVisualizerSelectIndexButton" > = ({ job, customUrls, setCustomUrls }) => { const { - services: { notifications }, + services: { http, notifications }, } = useMlKibana(); const [expandedUrlIndex, setExpandedUrlIndex] = useState(null); @@ -103,7 +103,7 @@ export const CustomUrlList: FC = ({ job, customUrls, setCust if (index < customUrls.length) { getTestUrl(job, customUrls[index]) .then((testUrl) => { - openCustomUrlWindow(testUrl, customUrls[index]); + openCustomUrlWindow(testUrl, customUrls[index], http.basePath.get()); }) .catch((resp) => { // eslint-disable-next-line no-console diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx index 7af27fc22e34c..468efcf013e9b 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx @@ -160,13 +160,16 @@ class CustomUrlsUI extends Component { }; onTestButtonClick = () => { - const { toasts } = this.props.kibana.services.notifications; + const { + http: { basePath }, + notifications: { toasts }, + } = this.props.kibana.services; const job = this.props.job; buildCustomUrlFromSettings(this.state.editorSettings as CustomUrlSettings) .then((customUrl) => { getTestUrl(job, customUrl) .then((testUrl) => { - openCustomUrlWindow(testUrl, customUrl); + openCustomUrlWindow(testUrl, customUrl, basePath.get()); }) .catch((resp) => { // eslint-disable-next-line no-console diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts index 92e65e580fc01..8ab45dc24aa17 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts @@ -5,8 +5,10 @@ */ import { i18n } from '@kbn/i18n'; + import { Job, Datafeed, Detector } from '../../../../../../../common/types/anomaly_detection_jobs'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; +import { NavigateToPath } from '../../../../../contexts/kibana'; import { ML_JOB_AGGREGATION, SPARSE_DATA_AGGREGATIONS, @@ -20,12 +22,7 @@ import { mlCategory, } from '../../../../../../../common/types/fields'; import { mlJobService } from '../../../../../services/job_service'; -import { - JobCreatorType, - isMultiMetricJobCreator, - isPopulationJobCreator, - isCategorizationJobCreator, -} from '../index'; +import { JobCreatorType } from '../index'; import { CREATED_BY_LABEL, JOB_TYPE } from '../../../../../../../common/constants/new_job'; const getFieldByIdFactory = (additionalFields: Field[]) => (id: string) => { @@ -247,43 +244,33 @@ function stashCombinedJob( mlJobService.tempJobCloningObjects.calendars = jobCreator.calendars; } -export function convertToMultiMetricJob(jobCreator: JobCreatorType) { +export function convertToMultiMetricJob( + jobCreator: JobCreatorType, + navigateToPath: NavigateToPath +) { jobCreator.createdBy = CREATED_BY_LABEL.MULTI_METRIC; jobCreator.modelPlot = false; stashCombinedJob(jobCreator, true, true); - window.location.href = window.location.href.replace( - JOB_TYPE.SINGLE_METRIC, - JOB_TYPE.MULTI_METRIC - ); + navigateToPath(`jobs/new_job/${JOB_TYPE.MULTI_METRIC}`, true); } -export function convertToAdvancedJob(jobCreator: JobCreatorType) { +export function convertToAdvancedJob(jobCreator: JobCreatorType, navigateToPath: NavigateToPath) { jobCreator.createdBy = null; stashCombinedJob(jobCreator, true, true); - let jobType = JOB_TYPE.SINGLE_METRIC; - if (isMultiMetricJobCreator(jobCreator)) { - jobType = JOB_TYPE.MULTI_METRIC; - } else if (isPopulationJobCreator(jobCreator)) { - jobType = JOB_TYPE.POPULATION; - } else if (isCategorizationJobCreator(jobCreator)) { - jobType = JOB_TYPE.CATEGORIZATION; - } - - window.location.href = window.location.href.replace(jobType, JOB_TYPE.ADVANCED); + navigateToPath(`jobs/new_job/${JOB_TYPE.ADVANCED}`, true); } -export function resetJob(jobCreator: JobCreatorType) { +export function resetJob(jobCreator: JobCreatorType, navigateToPath: NavigateToPath) { jobCreator.jobId = ''; stashCombinedJob(jobCreator, true, true); - - window.location.href = '#/jobs/new_job'; + navigateToPath('/jobs/new_job'); } -export function advancedStartDatafeed(jobCreator: JobCreatorType) { +export function advancedStartDatafeed(jobCreator: JobCreatorType, navigateToPath: NavigateToPath) { stashCombinedJob(jobCreator, false, false); - window.location.href = '#/jobs'; + navigateToPath('/jobs'); } export function aggFieldPairsCanBeCharted(afs: AggFieldPair[]) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx index 60b034b516939..7999ce46bc9ed 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx @@ -22,10 +22,18 @@ import { i18n } from '@kbn/i18n'; import { JobCreatorContext } from '../../../../../job_creator_context'; import { Description } from './description'; import { ml } from '../../../../../../../../../services/ml_api_service'; +import { PLUGIN_ID } from '../../../../../../../../../../../common/constants/app'; import { Calendar } from '../../../../../../../../../../../common/types/calendars'; +import { useMlKibana } from '../../../../../../../../../contexts/kibana'; import { GLOBAL_CALENDAR } from '../../../../../../../../../../../common/constants/calendars'; export const CalendarsSelection: FC = () => { + const { + services: { + application: { getUrlForApp }, + }, + } = useMlKibana(); + const { jobCreator, jobCreatorUpdate } = useContext(JobCreatorContext); const [selectedCalendars, setSelectedCalendars] = useState(jobCreator.calendars); const [selectedOptions, setSelectedOptions] = useState>>( @@ -64,7 +72,9 @@ export const CalendarsSelection: FC = () => { }, }; - const manageCalendarsHref = '#/settings/calendars_list'; + const manageCalendarsHref = getUrlForApp(PLUGIN_ID, { + path: '/settings/calendars_list', + }); return ( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx index 3bcac1cf6876c..e14e29cc965d3 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx @@ -8,21 +8,25 @@ import React, { Fragment, FC, useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; +import { useNavigateToPath } from '../../../../../../../contexts/kibana'; + +import { convertToMultiMetricJob } from '../../../../../common/job_creator/util/general'; + import { JobCreatorContext } from '../../../job_creator_context'; + import { BucketSpan } from '../bucket_span'; import { SparseDataSwitch } from '../sparse_data'; -import { convertToMultiMetricJob } from '../../../../../common/job_creator/util/general'; - interface Props { setIsValid: (proceed: boolean) => void; } export const SingleMetricSettings: FC = ({ setIsValid }) => { const { jobCreator } = useContext(JobCreatorContext); + const navigateToPath = useNavigateToPath(); const convertToMultiMetric = () => { - convertToMultiMetricJob(jobCreator); + convertToMultiMetricJob(jobCreator, navigateToPath); }; return ( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx index d8cd0f5e4f1f0..5ef59951c43cc 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx @@ -15,7 +15,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana'; import { PreviousButton } from '../wizard_nav'; import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; @@ -42,6 +42,9 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => const { services: { notifications }, } = useMlKibana(); + + const navigateToPath = useNavigateToPath(); + const { jobCreator, jobValidator, jobValidatorUpdated, resultsLoader } = useContext( JobCreatorContext ); @@ -87,7 +90,7 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => try { await jobCreator.createJob(); await jobCreator.createDatafeed(); - advancedStartDatafeed(jobCreator); + advancedStartDatafeed(jobCreator, navigateToPath); } catch (error) { // catch and display all job creation errors const { toasts } = notifications; @@ -112,11 +115,11 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => } function clickResetJob() { - resetJob(jobCreator); + resetJob(jobCreator, navigateToPath); } const convertToAdvanced = () => { - convertToAdvancedJob(jobCreator); + convertToAdvancedJob(jobCreator, navigateToPath); }; useEffect(() => { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx index 0f990a07aaf21..0caf97b0006d4 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx @@ -16,7 +16,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { SavedObjectFinderUi } from '../../../../../../../../../src/plugins/saved_objects/public'; -import { useMlKibana } from '../../../../contexts/kibana'; +import { useMlKibana, useNavigateToPath } from '../../../../contexts/kibana'; export interface PageProps { nextStepPath: string; @@ -25,11 +25,14 @@ export interface PageProps { export const Page: FC = ({ nextStepPath }) => { const RESULTS_PER_PAGE = 20; const { uiSettings, savedObjects } = useMlKibana().services; + const navigateToPath = useNavigateToPath(); const onObjectSelection = (id: string, type: string) => { - window.location.href = `${nextStepPath}?${ - type === 'index-pattern' ? 'index' : 'savedSearchId' - }=${encodeURIComponent(id)}`; + navigateToPath( + `${nextStepPath}?${type === 'index-pattern' ? 'index' : 'savedSearchId'}=${encodeURIComponent( + id + )}` + ); }; return ( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx index 3bfe0569e75be..be0135ec3f1e0 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx @@ -18,6 +18,7 @@ import { EuiLink, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useNavigateToPath } from '../../../../contexts/kibana'; import { useMlContext } from '../../../../contexts/ml'; import { isSavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { DataRecognizer } from '../../../../components/data_recognizer'; @@ -28,6 +29,8 @@ import { CategorizationIcon } from './categorization_job_icon'; export const Page: FC = () => { const mlContext = useMlContext(); + const navigateToPath = useNavigateToPath(); + const [recognizerResultsCount, setRecognizerResultsCount] = useState(0); const { currentSavedSearch, currentIndexPattern } = mlContext; @@ -68,25 +71,23 @@ export const Page: FC = () => { }, }; - const getUrl = (basePath: string) => { + const getUrlParams = () => { return !isSavedSearchSavedObject(currentSavedSearch) - ? `${basePath}?index=${currentIndexPattern.id}` - : `${basePath}?savedSearchId=${currentSavedSearch.id}`; + ? `?index=${currentIndexPattern.id}` + : `?savedSearchId=${currentSavedSearch.id}`; }; const addSelectionToRecentlyAccessed = () => { const title = !isSavedSearchSavedObject(currentSavedSearch) ? currentIndexPattern.title : (currentSavedSearch.attributes.title as string); - const url = getUrl(''); - addItemToRecentlyAccessed('jobs/new_job/datavisualizer', title, url); - - window.location.href = getUrl('#jobs/new_job/datavisualizer'); + addItemToRecentlyAccessed('jobs/new_job/datavisualizer', title, ''); + navigateToPath(`/jobs/new_job/datavisualizer${getUrlParams()}`); }; const jobTypes = [ { - href: getUrl('#jobs/new_job/single_metric'), + onClick: () => navigateToPath(`/jobs/new_job/single_metric${getUrlParams()}`), icon: { type: 'createSingleMetricJob', ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.singleMetricAriaLabel', { @@ -102,7 +103,7 @@ export const Page: FC = () => { id: 'mlJobTypeLinkSingleMetricJob', }, { - href: getUrl('#jobs/new_job/multi_metric'), + onClick: () => navigateToPath(`/jobs/new_job/multi_metric${getUrlParams()}`), icon: { type: 'createMultiMetricJob', ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.multiMetricAriaLabel', { @@ -119,7 +120,7 @@ export const Page: FC = () => { id: 'mlJobTypeLinkMultiMetricJob', }, { - href: getUrl('#jobs/new_job/population'), + onClick: () => navigateToPath(`/jobs/new_job/population${getUrlParams()}`), icon: { type: 'createPopulationJob', ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.populationAriaLabel', { @@ -136,7 +137,7 @@ export const Page: FC = () => { id: 'mlJobTypeLinkPopulationJob', }, { - href: getUrl('#jobs/new_job/advanced'), + onClick: () => navigateToPath(`/jobs/new_job/advanced${getUrlParams()}`), icon: { type: 'createAdvancedJob', ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.advancedAriaLabel', { @@ -153,7 +154,7 @@ export const Page: FC = () => { id: 'mlJobTypeLinkAdvancedJob', }, { - href: getUrl('#jobs/new_job/categorization'), + onClick: () => navigateToPath(`/jobs/new_job/categorization${getUrlParams()}`), icon: { type: CategorizationIcon, ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.categorizationAriaLabel', { @@ -247,11 +248,11 @@ export const Page: FC = () => { - {jobTypes.map(({ href, icon, title, description, id }) => ( + {jobTypes.map(({ onClick, icon, title, description, id }) => ( { /> } onClick={addSelectionToRecentlyAccessed} - href={getUrl('#jobs/new_job/datavisualizer')} /> diff --git a/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts b/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts index 82e233745f9e4..d0a4f999af758 100644 --- a/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts +++ b/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts @@ -4,47 +4,82 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiBreadcrumb } from '@elastic/eui'; + import { i18n } from '@kbn/i18n'; + import { ChromeBreadcrumb } from 'kibana/public'; +import { NavigateToPath } from '../contexts/kibana'; + export const ML_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.machineLearningBreadcrumbLabel', { defaultMessage: 'Machine Learning', }), - href: '#/', + href: '/', }); -export const SETTINGS: ChromeBreadcrumb = Object.freeze({ +export const SETTINGS_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.settingsBreadcrumbLabel', { defaultMessage: 'Settings', }), - href: '#/settings', + href: '/settings', }); export const ANOMALY_DETECTION_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.anomalyDetectionBreadcrumbLabel', { defaultMessage: 'Anomaly Detection', }), - href: '#/jobs', + href: '/jobs', }); export const DATA_FRAME_ANALYTICS_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.dataFrameAnalyticsLabel', { defaultMessage: 'Data Frame Analytics', }), - href: '#/data_frame_analytics', + href: '/data_frame_analytics', }); export const DATA_VISUALIZER_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.datavisualizerBreadcrumbLabel', { defaultMessage: 'Data Visualizer', }), - href: '#/datavisualizer', + href: '/datavisualizer', }); export const CREATE_JOB_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.createJobsBreadcrumbLabel', { defaultMessage: 'Create job', }), - href: '#/jobs/new_job', + href: '/jobs/new_job', }); + +const breadcrumbs = { + ML_BREADCRUMB, + SETTINGS_BREADCRUMB, + ANOMALY_DETECTION_BREADCRUMB, + DATA_FRAME_ANALYTICS_BREADCRUMB, + DATA_VISUALIZER_BREADCRUMB, + CREATE_JOB_BREADCRUMB, +}; +type Breadcrumb = keyof typeof breadcrumbs; + +export const breadcrumbOnClickFactory = ( + path: string | undefined, + navigateToPath: NavigateToPath +): EuiBreadcrumb['onClick'] => { + return (e) => { + e.preventDefault(); + navigateToPath(path); + }; +}; + +export const getBreadcrumbWithUrlForApp = ( + breadcrumbName: Breadcrumb, + navigateToPath: NavigateToPath +): EuiBreadcrumb => { + return { + ...breadcrumbs[breadcrumbName], + onClick: breadcrumbOnClickFactory(breadcrumbs[breadcrumbName].href, navigateToPath), + }; +}; diff --git a/x-pack/plugins/ml/public/application/routing/router.tsx b/x-pack/plugins/ml/public/application/routing/router.tsx index f1b8083f19ccf..56c9a19723fba 100644 --- a/x-pack/plugins/ml/public/application/routing/router.tsx +++ b/x-pack/plugins/ml/public/application/routing/router.tsx @@ -4,13 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; -import { HashRouter, Route, RouteProps } from 'react-router-dom'; +import React, { useEffect, FC } from 'react'; +import { useHistory, useLocation, Router, Route, RouteProps } from 'react-router-dom'; import { Location } from 'history'; -import { IUiSettingsClient, ChromeStart } from 'kibana/public'; +import { AppMountParameters, IUiSettingsClient, ChromeStart } from 'kibana/public'; import { ChromeBreadcrumb } from 'kibana/public'; import { IndexPatternsContract } from 'src/plugins/data/public'; + +import { useNavigateToPath } from '../contexts/kibana'; import { MlContext, MlContextValue } from '../contexts/ml'; import { UrlStateProvider } from '../util/url_state'; @@ -33,9 +35,10 @@ export interface PageProps { } interface PageDependencies { - setBreadcrumbs: ChromeStart['setBreadcrumbs']; - indexPatterns: IndexPatternsContract; config: IUiSettingsClient; + history: AppMountParameters['history']; + indexPatterns: IndexPatternsContract; + setBreadcrumbs: ChromeStart['setBreadcrumbs']; } export const PageLoader: FC<{ context: MlContextValue }> = ({ context, children }) => { @@ -44,28 +47,74 @@ export const PageLoader: FC<{ context: MlContextValue }> = ({ context, children ); }; -export const MlRouter: FC<{ pageDeps: PageDependencies }> = ({ pageDeps }) => { - const setBreadcrumbs = pageDeps.setBreadcrumbs; +/** + * This component provides compatibility with the previous hash based + * URL format used by HashRouter. Even if we migrate all internal URLs + * to one without hashes, we should keep this redirect in place to + * support legacy bookmarks and as a fallback for unmigrated URLs + * from other plugins. + */ +const LegacyHashUrlRedirect: FC = ({ children }) => { + const history = useHistory(); + const location = useLocation(); + + useEffect(() => { + if (location.hash.startsWith('#/')) { + history.push(location.hash.replace('#', '')); + } + }, [location.hash]); + + return <>{children}; +}; + +/** + * `MlRoutes` creates a React Router Route for every routeFactory + * and passes on the `navigateToPath` helper. + */ +const MlRoutes: FC<{ + pageDeps: PageDependencies; +}> = ({ pageDeps }) => { + const navigateToPath = useNavigateToPath(); return ( - + <> + {Object.entries(routes).map(([name, routeFactory]) => { + const route = routeFactory(navigateToPath); + + return ( + { + window.setTimeout(() => { + pageDeps.setBreadcrumbs(route.breadcrumbs); + }); + return route.render(props, pageDeps); + }} + /> + ); + })} + + ); +}; + +/** + * `MlRouter` is based on `BrowserRouter` and takes in `ScopedHistory` provided + * by Kibana. `LegacyHashUrlRedirect` provides compatibility with legacy hash based URLs. + * `UrlStateProvider` manages state stored in `_g/_a` URL parameters which can be + * use in components further down via `useUrlState()`. + */ +export const MlRouter: FC<{ + pageDeps: PageDependencies; +}> = ({ pageDeps }) => ( + +
- {Object.entries(routes).map(([name, route]) => ( - { - window.setTimeout(() => { - setBreadcrumbs(route.breadcrumbs); - }); - return route.render(props, pageDeps); - }} - /> - ))} +
-
- ); -}; + + +); diff --git a/x-pack/plugins/ml/public/application/routing/routes/access_denied.tsx b/x-pack/plugins/ml/public/application/routing/routes/access_denied.tsx index bd7fc434b36ac..42d9a59d15bfa 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/access_denied.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/access_denied.tsx @@ -19,11 +19,11 @@ const breadcrumbs = [ }, ]; -export const accessDeniedRoute: MlRoute = { +export const accessDeniedRouteFactory = (): MlRoute => ({ path: '/access-denied', render: (props, deps) => , breadcrumbs, -}; +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, {}); diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx index ebc7bd95fb0c3..8c45398098b2f 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx @@ -5,29 +5,31 @@ */ import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; import { parse } from 'query-string'; + +import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../data_frame_analytics/pages/analytics_creation'; -import { ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameManagementLabel', { - defaultMessage: 'Data Frame Analytics', - }), - href: '#/data_frame_analytics', - }, -]; - -export const analyticsJobsCreationRoute: MlRoute = { +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; + +export const analyticsJobsCreationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/data_frame_analytics/new_job', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameManagementLabel', { + defaultMessage: 'Data Frame Analytics', + }), + onClick: breadcrumbOnClickFactory('/data_frame_analytics', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { index, jobId, savedSearchId }: Record = parse(location.search, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx index 1ffea2c06faf4..47cc002ab4d83 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx @@ -4,33 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parse } from 'query-string'; import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; +import { parse } from 'query-string'; import { decode } from 'rison-node'; + +import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../data_frame_analytics/pages/analytics_exploration'; import { ANALYSIS_CONFIG_TYPE } from '../../../data_frame_analytics/common/analytics'; -import { ML_BREADCRUMB, DATA_FRAME_ANALYTICS_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - DATA_FRAME_ANALYTICS_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameExplorationLabel', { - defaultMessage: 'Exploration', - }), - href: '', - }, -]; - -export const analyticsJobExplorationRoute: MlRoute = { +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; + +export const analyticsJobExplorationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/data_frame_analytics/exploration', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameExplorationLabel', { + defaultMessage: 'Exploration', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx index 2623136d1e98f..b6ef9ea81b4ba 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx @@ -7,28 +7,28 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../data_frame_analytics/pages/analytics_management'; -import { ML_BREADCRUMB, DATA_FRAME_ANALYTICS_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - DATA_FRAME_ANALYTICS_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameListLabel', { - defaultMessage: 'Job Management', - }), - href: '', - }, -]; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const analyticsJobsListRoute: MlRoute = { +export const analyticsJobsListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/data_frame_analytics', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameListLabel', { + defaultMessage: 'Job Management', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx index fc2d517b2edb1..efe5c3cba04a5 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx @@ -11,21 +11,24 @@ import React, { FC } from 'react'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { DatavisualizerSelector } from '../../../datavisualizer'; import { checkBasicLicense } from '../../../license'; import { checkFindFileStructurePrivilegeResolver } from '../../../capabilities/check_capabilities'; -import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB]; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const selectorRoute: MlRoute = { +export const selectorRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/datavisualizer', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath), + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx index 1115a38870821..485af52c45a55 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx @@ -12,6 +12,8 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { FileDataVisualizerPage } from '../../../datavisualizer/file_based'; @@ -20,24 +22,22 @@ import { checkBasicLicense } from '../../../license'; import { checkFindFileStructurePrivilegeResolver } from '../../../capabilities/check_capabilities'; import { loadIndexPatterns } from '../../../util/index_utils'; -import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - DATA_VISUALIZER_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataVisualizer.fileBasedLabel', { - defaultMessage: 'File', - }), - href: '', - }, -]; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const fileBasedRoute: MlRoute = { +export const fileBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/filedatavisualizer', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.dataVisualizer.fileBasedLabel', { + defaultMessage: 'File', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver('', undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx index 1ec73fced82fe..358b8773e3460 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx @@ -4,9 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parse } from 'query-string'; import React, { FC } from 'react'; +import { parse } from 'query-string'; + import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { Page } from '../../../datavisualizer/index_based'; @@ -15,24 +19,22 @@ import { checkBasicLicense } from '../../../license'; import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; import { loadIndexPatterns } from '../../../util/index_utils'; import { checkMlNodesAvailable } from '../../../ml_nodes_check'; -import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - DATA_VISUALIZER_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.indexLabel', { - defaultMessage: 'Index', - }), - href: '', - }, -]; - -export const indexBasedRoute: MlRoute = { +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; + +export const indexBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/datavisualizer', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.indexLabel', { + defaultMessage: 'Index', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { index, savedSearchId }: Record = parse(location.search, { sort: false }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index 7d09797a0ff1b..a2030776773a9 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -9,6 +9,8 @@ import useObservable from 'react-use/lib/useObservable'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../contexts/kibana'; + import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; import { MlRoute, PageLoader, PageProps } from '../router'; @@ -27,26 +29,24 @@ import { useShowCharts } from '../../components/controls/checkbox_showcharts'; import { useTableInterval } from '../../components/controls/select_interval'; import { useTableSeverity } from '../../components/controls/select_severity'; import { useUrlState } from '../../util/url_state'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; import { useTimefilter } from '../../contexts/kibana'; import { isViewBySwimLaneData } from '../../explorer/swimlane_container'; -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.anomalyDetection.anomalyExplorerLabel', { - defaultMessage: 'Anomaly Explorer', - }), - href: '', - }, -]; - -export const explorerRoute: MlRoute = { +export const explorerRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/explorer', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.anomalyDetection.anomalyExplorerLabel', { + defaultMessage: 'Anomaly Explorer', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ deps }) => { const { context, results } = useResolver(undefined, undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/index.ts b/x-pack/plugins/ml/public/application/routing/routes/index.ts index 44111ae32cd30..fe7ecd129ebef 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/index.ts +++ b/x-pack/plugins/ml/public/application/routing/routes/index.ts @@ -10,6 +10,6 @@ export * from './new_job'; export * from './datavisualizer'; export * from './settings'; export * from './data_frame_analytics'; -export { timeSeriesExplorerRoute } from './timeseriesexplorer'; +export { timeSeriesExplorerRouteFactory } from './timeseriesexplorer'; export * from './explorer'; export * from './access_denied'; diff --git a/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx index c1d686d356dda..db58b6a537e06 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx @@ -7,6 +7,9 @@ import React, { useEffect, FC } from 'react'; import { useObservable } from 'react-use'; import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../contexts/kibana'; + import { DEFAULT_REFRESH_INTERVAL_MS } from '../../../../common/constants/jobs_list'; import { mlTimefilterRefresh$ } from '../../services/timefilter_refresh_service'; import { useUrlState } from '../../util/url_state'; @@ -15,24 +18,22 @@ import { useResolver } from '../use_resolver'; import { basicResolvers } from '../resolvers'; import { JobsPage } from '../../jobs/jobs_list'; import { useTimefilter } from '../../contexts/kibana'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.anomalyDetection.jobManagementLabel', { - defaultMessage: 'Job Management', - }), - href: '', - }, -]; - -export const jobListRoute: MlRoute = { +export const jobListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.anomalyDetection.jobManagementLabel', { + defaultMessage: 'Job Management', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, basicResolvers(deps)); diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx index b630b09b1a46d..d8605c4cc9115 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx @@ -5,12 +5,16 @@ */ import React, { FC } from 'react'; + import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page, preConfiguredJobRedirect } from '../../../jobs/new_job/pages/index_or_search'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; import { checkBasicLicense } from '../../../license'; import { loadIndexPatterns } from '../../../util/index_utils'; import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; @@ -26,9 +30,9 @@ interface IndexOrSearchPageProps extends PageProps { mode: MODE; } -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, +const getBreadcrumbs = (navigateToPath: NavigateToPath) => [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel', { defaultMessage: 'Create job', @@ -37,31 +41,31 @@ const breadcrumbs = [ }, ]; -export const indexOrSearchRoute: MlRoute = { +export const indexOrSearchRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/step/index_or_search', render: (props, deps) => ( ), - breadcrumbs, -}; + breadcrumbs: getBreadcrumbs(navigateToPath), +}); -export const dataVizIndexOrSearchRoute: MlRoute = { +export const dataVizIndexOrSearchRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/datavisualizer_index_select', render: (props, deps) => ( ), - breadcrumbs, -}; + breadcrumbs: getBreadcrumbs(navigateToPath), +}); const PageWrapper: FC = ({ nextStepPath, deps, mode }) => { const newJobResolvers = { diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx index f0a25d880a082..b8ab29d40fa1f 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx @@ -4,31 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parse } from 'query-string'; import React, { FC } from 'react'; +import { parse } from 'query-string'; + import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../jobs/new_job/pages/job_type'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectJobType', { - defaultMessage: 'Create job', - }), - href: '', - }, -]; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const jobTypeRoute: MlRoute = { +export const jobTypeRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/step/job_type', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectJobType', { + defaultMessage: 'Create job', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { index, savedSearchId }: Record = parse(location.search, { sort: false }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/new_job.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/new_job.tsx index b110434f6f0a8..b230da44c8d6d 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/new_job.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/new_job.tsx @@ -5,28 +5,16 @@ */ import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; import { Redirect } from 'react-router-dom'; import { MlRoute } from '../../router'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.jobWizardLabel', { - defaultMessage: 'Create job', - }), - href: '#/jobs/new_job', - }, -]; - -export const newJobRoute: MlRoute = { +export const newJobRouteFactory = (): MlRoute => ({ path: '/jobs/new_job', render: () => , - breadcrumbs, -}; + // no breadcrumbs since it's just a redirect + breadcrumbs: [], +}); const Page: FC = () => { return ; diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx index 2cd40cbcd95e6..6be58828ee1a5 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx @@ -6,42 +6,41 @@ import { parse } from 'query-string'; import React, { FC } from 'react'; + import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../jobs/new_job/recognize'; import { checkViewOrCreateJobs } from '../../../jobs/new_job/recognize/resolvers'; import { mlJobService } from '../../../services/job_service'; -import { - ANOMALY_DETECTION_BREADCRUMB, - CREATE_JOB_BREADCRUMB, - ML_BREADCRUMB, -} from '../../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - CREATE_JOB_BREADCRUMB, - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabelRecognize', { - defaultMessage: 'Recognized index', - }), - href: '', - }, -]; - -export const recognizeRoute: MlRoute = { +export const recognizeRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/recognize', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabelRecognize', { + defaultMessage: 'Recognized index', + }), + href: '', + }, + ], +}); -export const checkViewOrCreateRoute: MlRoute = { +export const checkViewOrCreateRouteFactory = (): MlRoute => ({ path: '/modules/check_view_or_create', render: (props, deps) => , + // no breadcrumbs since it's just a redirect breadcrumbs: [], -}; +}); const PageWrapper: FC = ({ location, deps }) => { const { id, index, savedSearchId }: Record = parse(location.search, { sort: false }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx index 14df9a1d44a85..35085fd557577 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx @@ -8,6 +8,8 @@ import { parse } from 'query-string'; import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { basicResolvers } from '../../resolvers'; import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -16,20 +18,20 @@ import { JOB_TYPE } from '../../../../../common/constants/new_job'; import { mlJobService } from '../../../services/job_service'; import { loadNewJobCapabilities } from '../../../services/new_job_capabilities_service'; import { checkCreateJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; -import { - ANOMALY_DETECTION_BREADCRUMB, - CREATE_JOB_BREADCRUMB, - ML_BREADCRUMB, -} from '../../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; interface WizardPageProps extends PageProps { jobType: JOB_TYPE; } -const baseBreadcrumbs = [ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, CREATE_JOB_BREADCRUMB]; +const getBaseBreadcrumbs = (navigateToPath: NavigateToPath) => [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath), +]; -const singleMetricBreadcrumbs = [ - ...baseBreadcrumbs, +const getSingleMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [ + ...getBaseBreadcrumbs(navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.singleMetricLabel', { defaultMessage: 'Single metric', @@ -38,8 +40,8 @@ const singleMetricBreadcrumbs = [ }, ]; -const multiMetricBreadcrumbs = [ - ...baseBreadcrumbs, +const getMultiMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [ + ...getBaseBreadcrumbs(navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.multiMetricLabel', { defaultMessage: 'Multi-metric', @@ -48,8 +50,8 @@ const multiMetricBreadcrumbs = [ }, ]; -const populationBreadcrumbs = [ - ...baseBreadcrumbs, +const getPopulationBreadcrumbs = (navigateToPath: NavigateToPath) => [ + ...getBaseBreadcrumbs(navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.populationLabel', { defaultMessage: 'Population', @@ -58,8 +60,8 @@ const populationBreadcrumbs = [ }, ]; -const advancedBreadcrumbs = [ - ...baseBreadcrumbs, +const getAdvancedBreadcrumbs = (navigateToPath: NavigateToPath) => [ + ...getBaseBreadcrumbs(navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel', { defaultMessage: 'Advanced configuration', @@ -68,8 +70,8 @@ const advancedBreadcrumbs = [ }, ]; -const categorizationBreadcrumbs = [ - ...baseBreadcrumbs, +const getCategorizationBreadcrumbs = (navigateToPath: NavigateToPath) => [ + ...getBaseBreadcrumbs(navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.categorizationLabel', { defaultMessage: 'Categorization', @@ -78,35 +80,35 @@ const categorizationBreadcrumbs = [ }, ]; -export const singleMetricRoute: MlRoute = { +export const singleMetricRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/single_metric', render: (props, deps) => , - breadcrumbs: singleMetricBreadcrumbs, -}; + breadcrumbs: getSingleMetricBreadcrumbs(navigateToPath), +}); -export const multiMetricRoute: MlRoute = { +export const multiMetricRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/multi_metric', render: (props, deps) => , - breadcrumbs: multiMetricBreadcrumbs, -}; + breadcrumbs: getMultiMetricBreadcrumbs(navigateToPath), +}); -export const populationRoute: MlRoute = { +export const populationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/population', render: (props, deps) => , - breadcrumbs: populationBreadcrumbs, -}; + breadcrumbs: getPopulationBreadcrumbs(navigateToPath), +}); -export const advancedRoute: MlRoute = { +export const advancedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/advanced', render: (props, deps) => , - breadcrumbs: advancedBreadcrumbs, -}; + breadcrumbs: getAdvancedBreadcrumbs(navigateToPath), +}); -export const categorizationRoute: MlRoute = { +export const categorizationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/categorization', render: (props, deps) => , - breadcrumbs: categorizationBreadcrumbs, -}; + breadcrumbs: getCategorizationBreadcrumbs(navigateToPath), +}); const PageWrapper: FC = ({ location, jobType, deps }) => { const { index, savedSearchId }: Record = parse(location.search, { sort: false }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx index 9b08bbf35c448..174e9804b9689 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx @@ -8,6 +8,9 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { Redirect } from 'react-router-dom'; + +import { NavigateToPath } from '../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../router'; import { useResolver } from '../use_resolver'; import { OverviewPage } from '../../overview'; @@ -17,23 +20,21 @@ import { checkGetJobsCapabilitiesResolver } from '../../capabilities/check_capab import { getMlNodeCount } from '../../ml_nodes_check'; import { loadMlServerInfo } from '../../services/ml_server_info'; import { useTimefilter } from '../../contexts/kibana'; -import { ML_BREADCRUMB } from '../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - { - text: i18n.translate('xpack.ml.overview.overviewLabel', { - defaultMessage: 'Overview', - }), - href: '#/overview', - }, -]; - -export const overviewRoute: MlRoute = { +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../breadcrumbs'; + +export const overviewRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/overview', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.overview.overviewLabel', { + defaultMessage: 'Overview', + }), + onClick: breadcrumbOnClickFactory('/overview', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { @@ -51,11 +52,11 @@ const PageWrapper: FC = ({ deps }) => { ); }; -export const appRootRoute: MlRoute = { +export const appRootRouteFactory = (): MlRoute => ({ path: '/', render: () => , breadcrumbs: [], -}; +}); const Page: FC = () => { return ; diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx index e015a3292acc4..f2ae57f1ec961 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx @@ -12,6 +12,8 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -23,24 +25,22 @@ import { } from '../../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { CalendarsList } from '../../../settings/calendars'; -import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagementLabel', { - defaultMessage: 'Calendar management', - }), - href: '#/settings/calendars_list', - }, -]; - -export const calendarListRoute: MlRoute = { +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; + +export const calendarListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/calendars_list', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagementLabel', { + defaultMessage: 'Calendar management', + }), + onClick: breadcrumbOnClickFactory('/settings/calendars_list', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx index ebd58120853a9..a5c30e1eaaacc 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx @@ -12,6 +12,8 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -23,7 +25,7 @@ import { } from '../../../capabilities/check_capabilities'; import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { NewCalendar } from '../../../settings/calendars'; -import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; enum MODE { NEW, @@ -34,39 +36,35 @@ interface NewCalendarPageProps extends PageProps { mode: MODE; } -const newBreadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.createLabel', { - defaultMessage: 'Create', - }), - href: '#/settings/calendars_list/new_calendar', - }, -]; - -const editBreadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.editLabel', { - defaultMessage: 'Edit', - }), - href: '#/settings/calendars_list/edit_calendar', - }, -]; - -export const newCalendarRoute: MlRoute = { +export const newCalendarRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/calendars_list/new_calendar', render: (props, deps) => , - breadcrumbs: newBreadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.createLabel', { + defaultMessage: 'Create', + }), + onClick: breadcrumbOnClickFactory('/settings/calendars_list/new_calendar', navigateToPath), + }, + ], +}); -export const editCalendarRoute: MlRoute = { +export const editCalendarRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/calendars_list/edit_calendar/:calendarId', render: (props, deps) => , - breadcrumbs: editBreadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.editLabel', { + defaultMessage: 'Edit', + }), + onClick: breadcrumbOnClickFactory('/settings/calendars_list/edit_calendar', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ location, mode, deps }) => { let calendarId: string | undefined; diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx index 25bded1a52db1..d734e18d72bab 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx @@ -12,6 +12,8 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -24,24 +26,22 @@ import { import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { FilterLists } from '../../../settings/filter_lists'; -import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.filterListsLabel', { - defaultMessage: 'Filter lists', - }), - href: '#/settings/filter_lists', - }, -]; +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const filterListRoute: MlRoute = { +export const filterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/filter_lists', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.filterListsLabel', { + defaultMessage: 'Filter lists', + }), + onClick: breadcrumbOnClickFactory('/settings/filter_lists', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx index 2f4ccecf2f1a2..c6f17bc7f6f68 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx @@ -12,6 +12,8 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -23,7 +25,8 @@ import { } from '../../../capabilities/check_capabilities'; import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { EditFilterList } from '../../../settings/filter_lists'; -import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; + +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; enum MODE { NEW, @@ -34,39 +37,35 @@ interface NewFilterPageProps extends PageProps { mode: MODE; } -const newBreadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.createLabel', { - defaultMessage: 'Create', - }), - href: '#/settings/filter_lists/new', - }, -]; - -const editBreadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.editLabel', { - defaultMessage: 'Edit', - }), - href: '#/settings/filter_lists/edit', - }, -]; - -export const newFilterListRoute: MlRoute = { +export const newFilterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/filter_lists/new_filter_list', render: (props, deps) => , - breadcrumbs: newBreadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.createLabel', { + defaultMessage: 'Create', + }), + onClick: breadcrumbOnClickFactory('/settings/filter_lists/new', navigateToPath), + }, + ], +}); -export const editFilterListRoute: MlRoute = { +export const editFilterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/filter_lists/edit_filter_list/:filterId', render: (props, deps) => , - breadcrumbs: editBreadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.editLabel', { + defaultMessage: 'Edit', + }), + onClick: breadcrumbOnClickFactory('/settings/filter_lists/edit', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ location, mode, deps }) => { let filterId: string | undefined; diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx index a80c173dbca34..3f4b269851469 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx @@ -11,6 +11,8 @@ import React, { FC } from 'react'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -22,15 +24,16 @@ import { } from '../../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { AnomalyDetectionSettingsContext, Settings } from '../../../settings'; -import { ML_BREADCRUMB, SETTINGS } from '../../breadcrumbs'; - -const breadcrumbs = [ML_BREADCRUMB, SETTINGS]; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const settingsRoute: MlRoute = { +export const settingsRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + ], +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index fdf29406893ad..6486db818e113 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -11,6 +11,8 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../contexts/kibana'; + import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; import { TimeSeriesExplorer } from '../../timeseriesexplorer'; @@ -34,15 +36,15 @@ import { MlRoute, PageLoader, PageProps } from '../router'; import { useRefresh } from '../use_refresh'; import { useResolver } from '../use_resolver'; import { basicResolvers } from '../resolvers'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; import { useTimefilter } from '../../contexts/kibana'; -export const timeSeriesExplorerRoute: MlRoute = { +export const timeSeriesExplorerRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/timeseriesexplorer', render: (props, deps) => , breadcrumbs: [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), { text: i18n.translate('xpack.ml.anomalyDetection.singleMetricViewerLabel', { defaultMessage: 'Single Metric Viewer', @@ -50,7 +52,7 @@ export const timeSeriesExplorerRoute: MlRoute = { href: '', }, ], -}; +}); const PageWrapper: FC = ({ deps }) => { const { context, results } = useResolver('', undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts b/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts index b5c01a1c26144..428060dd2c31b 100644 --- a/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts +++ b/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts @@ -9,6 +9,7 @@ import { getUrlForRecord, isValidLabel, isValidTimeRange, + openCustomUrlWindow, } from './custom_url_utils'; import { AnomalyRecordDoc } from '../../../common/types/anomalies'; import { @@ -474,4 +475,49 @@ describe('ML - custom URL utils', () => { expect(isValidTimeRange('AUTO')).toBe(false); }); }); + + describe('openCustomUrlWindow', () => { + const originalOpen = window.open; + + beforeEach(() => { + delete (window as any).open; + const mockOpen = jest.fn(); + window.open = mockOpen; + }); + + afterEach(() => { + window.open = originalOpen; + }); + + it('should add the base path to a relative non-kibana url', () => { + openCustomUrlWindow( + 'the-url', + { url_name: 'the-url-name', url_value: 'the-url-value' }, + 'the-base-path' + ); + expect(window.open).toHaveBeenCalledWith('the-base-path/the-url', '_blank'); + }); + + it('should add the base path and `app` prefix to a relative kibana url', () => { + openCustomUrlWindow( + 'discover#/the-url', + { url_name: 'the-url-name', url_value: 'discover#/the-url-value' }, + 'the-base-path' + ); + expect(window.open).toHaveBeenCalledWith('the-base-path/app/discover#/the-url', '_blank'); + }); + + it('should use an absolute url with protocol as is', () => { + openCustomUrlWindow( + 'http://example.com', + { url_name: 'the-url-name', url_value: 'http://example.com' }, + 'the-base-path' + ); + expect(window.open).toHaveBeenCalledWith( + 'http://example.com', + '_blank', + 'noopener,noreferrer' + ); + }); + }); }); diff --git a/x-pack/plugins/ml/public/application/util/custom_url_utils.ts b/x-pack/plugins/ml/public/application/util/custom_url_utils.ts index 20bb1c7f60597..9c843af36192e 100644 --- a/x-pack/plugins/ml/public/application/util/custom_url_utils.ts +++ b/x-pack/plugins/ml/public/application/util/custom_url_utils.ts @@ -76,15 +76,20 @@ export function getUrlForRecord( // Opens the specified URL in a new window. The behaviour (for example whether // it opens in a new tab or window) is determined from the original configuration // object which indicates whether it is opening a Kibana page running on the same server. -// fullUrl is the complete URL, including the base path, with any dollar delimited tokens -// from the urlConfig having been substituted with values from an anomaly record. -export function openCustomUrlWindow(fullUrl: string, urlConfig: UrlConfig) { +// `url` is the URL with any dollar delimited tokens from the urlConfig +// having been substituted with values from an anomaly record. +export function openCustomUrlWindow(url: string, urlConfig: UrlConfig, basePath: string) { // Run through a regex to test whether the url_value starts with a protocol scheme. if (/^(?:[a-z]+:)?\/\//i.test(urlConfig.url_value) === false) { - window.open(fullUrl, '_blank'); + // If `url` is a relative path, we need to prefix the base path. + if (url.charAt(0) !== '/') { + url = `${basePath}${isKibanaUrl(urlConfig) ? '/app/' : '/'}${url}`; + } + + window.open(url, '_blank'); } else { // Add noopener and noreferrr properties for external URLs. - const newWindow = window.open(fullUrl, '_blank', 'noopener,noreferrer'); + const newWindow = window.open(url, '_blank', 'noopener,noreferrer'); // Expect newWindow to be null, but just in case if not, reset the opener link. if (newWindow !== undefined && newWindow !== null) { @@ -94,13 +99,24 @@ export function openCustomUrlWindow(fullUrl: string, urlConfig: UrlConfig) { } // Returns whether the url_value of the supplied config is for -// a Kibana Discover or Dashboard page running on the same server as this ML plugin. +// a Kibana Discover, Dashboard or supported solution page running +// on the same server as this ML plugin. This is necessary so we can have +// backwards compatibility with custom URLs created before the move to +// BrowserRouter and URLs without hashes. If we add another solution to +// recognize modules or with custom UI in the custom URL builder we'd +// need to add the solution here. Manually created custom URLs for other +// solution pages need to be prefixed with `app/` in the custom URL builder. function isKibanaUrl(urlConfig: UrlConfig) { const urlValue = urlConfig.url_value; return ( + // HashRouter based plugins urlValue.startsWith('discover#/') || urlValue.startsWith('dashboards#/') || - urlValue.startsWith('apm#/') + urlValue.startsWith('apm#/') || + // BrowserRouter based plugins + urlValue.startsWith('security/') || + // Legacy links + urlValue.startsWith('siem#/') ); } diff --git a/x-pack/plugins/ml/public/url_generator.test.ts b/x-pack/plugins/ml/public/url_generator.test.ts index 45e2932b7781a..21dde12404957 100644 --- a/x-pack/plugins/ml/public/url_generator.test.ts +++ b/x-pack/plugins/ml/public/url_generator.test.ts @@ -19,7 +19,7 @@ describe('MlUrlGenerator', () => { mlExplorerSwimlane: { viewByFromPage: 2, viewByPerPage: 20 }, }); expect(url).toBe( - '/app/ml#/explorer?_g=(ml:(jobIds:!(test-job)))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFromPage:2,viewByPerPage:20))' + '/app/ml/explorer#?_g=(ml:(jobIds:!(test-job)))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFromPage:2,viewByPerPage:20))' ); }); diff --git a/x-pack/plugins/ml/public/url_generator.ts b/x-pack/plugins/ml/public/url_generator.ts index c2b57f6349d81..b7cf64159a827 100644 --- a/x-pack/plugins/ml/public/url_generator.ts +++ b/x-pack/plugins/ml/public/url_generator.ts @@ -83,7 +83,7 @@ export class MlUrlGenerator implements UrlGeneratorsDefinition('_g', queryState, { useHash: false }, url); url = setStateToKbnUrl('_a', appState, { useHash: false }, url); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ff8c3186d5cf8..c81aade2b063e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11669,7 +11669,6 @@ "xpack.ml.jobMessages.timeLabel": "時間", "xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel": "高度な構成", "xpack.ml.jobsBreadcrumbs.categorizationLabel": "カテゴリー分け", - "xpack.ml.jobsBreadcrumbs.jobWizardLabel": "ジョブを作成", "xpack.ml.jobsBreadcrumbs.multiMetricLabel": "マルチメトリック", "xpack.ml.jobsBreadcrumbs.populationLabel": "集団", "xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel": "ジョブを作成", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index bcf2f6707e7bf..aba5adf72c2f8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11672,7 +11672,6 @@ "xpack.ml.jobMessages.timeLabel": "时间", "xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel": "高级配置", "xpack.ml.jobsBreadcrumbs.categorizationLabel": "归类", - "xpack.ml.jobsBreadcrumbs.jobWizardLabel": "创建作业", "xpack.ml.jobsBreadcrumbs.multiMetricLabel": "多指标", "xpack.ml.jobsBreadcrumbs.populationLabel": "填充", "xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel": "创建作业", From 10f8beae8ecb64407df758d44601802cb768e35a Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Fri, 31 Jul 2020 11:52:00 +0200 Subject: [PATCH 31/33] [Discover] Context unskip date nanos functional tests (#73781) * Use _source value of timestamp for search_after since ES allows this now * Unskip functional tests * Remove unused convertIsoToNanosAsStr --- .../angular/context/api/utils/date_conversion.ts | 9 --------- .../context/api/utils/get_es_query_search_after.ts | 10 ++-------- test/functional/apps/context/_date_nanos.js | 3 +-- .../apps/context/_date_nanos_custom_timestamp.js | 5 +---- 4 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/plugins/discover/public/application/angular/context/api/utils/date_conversion.ts b/src/plugins/discover/public/application/angular/context/api/utils/date_conversion.ts index 64544a335c911..4369234a3ce9a 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/date_conversion.ts +++ b/src/plugins/discover/public/application/angular/context/api/utils/date_conversion.ts @@ -31,15 +31,6 @@ export function extractNanos(timeFieldValue: string = ''): string { return fractionSeconds.length !== 9 ? fractionSeconds.padEnd(9, '0') : fractionSeconds; } -/** - * extract the nanoseconds as string of a given ISO formatted timestamp - */ -export function convertIsoToNanosAsStr(isoValue: string): string { - const nanos = extractNanos(isoValue); - const millis = convertIsoToMillis(isoValue); - return `${millis}${nanos.substr(3, 6)}`; -} - /** * convert an iso formatted string to number of milliseconds since * 1970-01-01T00:00:00.000Z diff --git a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts index d4ee9e0e0f287..24ac19a7e3bc3 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts +++ b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import { convertIsoToNanosAsStr } from './date_conversion'; import { SurrDocType, EsHitRecordList, EsHitRecord } from '../context'; export type EsQuerySearchAfter = [string | number, string | number]; @@ -38,15 +37,10 @@ export function getEsQuerySearchAfter( // already surrounding docs -> first or last record is used const afterTimeRecIdx = type === 'successors' && documents.length ? documents.length - 1 : 0; const afterTimeDoc = documents[afterTimeRecIdx]; - const afterTimeValue = nanoSeconds - ? convertIsoToNanosAsStr(afterTimeDoc.fields[timeFieldName][0]) - : afterTimeDoc.sort[0]; + const afterTimeValue = nanoSeconds ? afterTimeDoc._source[timeFieldName] : afterTimeDoc.sort[0]; return [afterTimeValue, afterTimeDoc.sort[1]]; } // if data_nanos adapt timestamp value for sorting, since numeric value was rounded by browser // ES search_after also works when number is provided as string - return [ - nanoSeconds ? convertIsoToNanosAsStr(anchor.fields[timeFieldName][0]) : anchor.sort[0], - anchor.sort[1], - ]; + return [nanoSeconds ? anchor._source[timeFieldName] : anchor.sort[0], anchor.sort[1]]; } diff --git a/test/functional/apps/context/_date_nanos.js b/test/functional/apps/context/_date_nanos.js index 89769caaea253..cdf2d6c04be83 100644 --- a/test/functional/apps/context/_date_nanos.js +++ b/test/functional/apps/context/_date_nanos.js @@ -30,8 +30,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'context', 'timePicker', 'discover']); const esArchiver = getService('esArchiver'); - // FLAKY/FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/58815 - describe.skip('context view for date_nanos', () => { + describe('context view for date_nanos', () => { before(async function () { await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos']); await esArchiver.loadIfNeeded('date_nanos'); diff --git a/test/functional/apps/context/_date_nanos_custom_timestamp.js b/test/functional/apps/context/_date_nanos_custom_timestamp.js index 6329f6c431e6a..8fe08d13af0aa 100644 --- a/test/functional/apps/context/_date_nanos_custom_timestamp.js +++ b/test/functional/apps/context/_date_nanos_custom_timestamp.js @@ -30,10 +30,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'context', 'timePicker', 'discover']); const esArchiver = getService('esArchiver'); - // skipped due to a recent change in ES that caused search_after queries with data containing - // custom timestamp formats like in the testdata to fail - // https://github.com/elastic/kibana/issues/58815 - describe.skip('context view for date_nanos with custom timestamp', () => { + describe('context view for date_nanos with custom timestamp', () => { before(async function () { await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos_custom']); await esArchiver.loadIfNeeded('date_nanos_custom'); From dbb603f9793d6477fb73663d53216cbefa3b1d2e Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Fri, 31 Jul 2020 09:21:47 -0400 Subject: [PATCH 32/33] [Canvas][tech-debt] Ensure cursor is called until full results are received (#73347) * Ensure cursor is called until full results are receeived * Fix Typecheck * Convert dependencies to typescript * Fix typings Co-authored-by: Elastic Machine --- .../functions/server/esdocs.ts | 1 - .../functions/server/essql.ts | 1 - ...uild_bool_array.js => build_bool_array.ts} | 5 +- x-pack/plugins/canvas/server/lib/filters.js | 38 ------- x-pack/plugins/canvas/server/lib/filters.ts | 74 ++++++++++++ .../{get_es_filter.js => get_es_filter.ts} | 8 +- .../{normalize_type.js => normalize_type.ts} | 4 +- .../plugins/canvas/server/lib/query_es_sql.js | 59 ---------- .../canvas/server/lib/query_es_sql.test.ts | 106 ++++++++++++++++++ .../plugins/canvas/server/lib/query_es_sql.ts | 96 ++++++++++++++++ .../{sanitize_name.js => sanitize_name.ts} | 4 +- .../server/routes/es_fields/es_fields.ts | 1 - x-pack/plugins/canvas/types/filters.ts | 32 ++++++ x-pack/plugins/canvas/types/index.ts | 1 + 14 files changed, 320 insertions(+), 110 deletions(-) rename x-pack/plugins/canvas/server/lib/{build_bool_array.js => build_bool_array.ts} (66%) delete mode 100644 x-pack/plugins/canvas/server/lib/filters.js create mode 100644 x-pack/plugins/canvas/server/lib/filters.ts rename x-pack/plugins/canvas/server/lib/{get_es_filter.js => get_es_filter.ts} (75%) rename x-pack/plugins/canvas/server/lib/{normalize_type.js => normalize_type.ts} (89%) delete mode 100644 x-pack/plugins/canvas/server/lib/query_es_sql.js create mode 100644 x-pack/plugins/canvas/server/lib/query_es_sql.test.ts create mode 100644 x-pack/plugins/canvas/server/lib/query_es_sql.ts rename x-pack/plugins/canvas/server/lib/{sanitize_name.js => sanitize_name.ts} (85%) create mode 100644 x-pack/plugins/canvas/types/filters.ts diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts index a090f09a76ea2..23fbc912d739a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts @@ -7,7 +7,6 @@ import squel from 'squel'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions'; /* eslint-disable */ -// @ts-expect-error untyped local import { queryEsSQL } from '../../../server/lib/query_es_sql'; /* eslint-enable */ import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts index 5ac91bec849c2..2e053f9084296 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts @@ -6,7 +6,6 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; /* eslint-disable */ -// @ts-expect-error untyped local import { queryEsSQL } from '../../../server/lib/query_es_sql'; /* eslint-enable */ import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/server/lib/build_bool_array.js b/x-pack/plugins/canvas/server/lib/build_bool_array.ts similarity index 66% rename from x-pack/plugins/canvas/server/lib/build_bool_array.js rename to x-pack/plugins/canvas/server/lib/build_bool_array.ts index f1cab93ceebbb..bd418394cf375 100644 --- a/x-pack/plugins/canvas/server/lib/build_bool_array.js +++ b/x-pack/plugins/canvas/server/lib/build_bool_array.ts @@ -5,10 +5,11 @@ */ import { getESFilter } from './get_es_filter'; +import { ExpressionValueFilter } from '../../types'; -const compact = (arr) => (Array.isArray(arr) ? arr.filter((val) => Boolean(val)) : []); +const compact = (arr: T[]) => (Array.isArray(arr) ? arr.filter((val) => Boolean(val)) : []); -export function buildBoolArray(canvasQueryFilterArray) { +export function buildBoolArray(canvasQueryFilterArray: ExpressionValueFilter[]) { return compact( canvasQueryFilterArray.map((clause) => { try { diff --git a/x-pack/plugins/canvas/server/lib/filters.js b/x-pack/plugins/canvas/server/lib/filters.js deleted file mode 100644 index afa58c7ee30c2..0000000000000 --- a/x-pack/plugins/canvas/server/lib/filters.js +++ /dev/null @@ -1,38 +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. - */ - -/* - TODO: This could be pluggable -*/ - -export function time(filter) { - if (!filter.column) { - throw new Error('column is required for Elasticsearch range filters'); - } - return { - range: { - [filter.column]: { gte: filter.from, lte: filter.to }, - }, - }; -} - -export function luceneQueryString(filter) { - return { - query_string: { - query: filter.query || '*', - }, - }; -} - -export function exactly(filter) { - return { - term: { - [filter.column]: { - value: filter.value, - }, - }, - }; -} diff --git a/x-pack/plugins/canvas/server/lib/filters.ts b/x-pack/plugins/canvas/server/lib/filters.ts new file mode 100644 index 0000000000000..9997640154e2c --- /dev/null +++ b/x-pack/plugins/canvas/server/lib/filters.ts @@ -0,0 +1,74 @@ +/* + * 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 { + FilterType, + ExpressionValueFilter, + CanvasTimeFilter, + CanvasLuceneFilter, + CanvasExactlyFilter, +} from '../../types'; + +/* + TODO: This could be pluggable +*/ + +const isTimeFilter = ( + maybeTimeFilter: ExpressionValueFilter +): maybeTimeFilter is CanvasTimeFilter => { + return maybeTimeFilter.filterType === FilterType.time; +}; +const isLuceneFilter = ( + maybeLuceneFilter: ExpressionValueFilter +): maybeLuceneFilter is CanvasLuceneFilter => { + return maybeLuceneFilter.filterType === FilterType.luceneQueryString; +}; +const isExactlyFilter = ( + maybeExactlyFilter: ExpressionValueFilter +): maybeExactlyFilter is CanvasExactlyFilter => { + return maybeExactlyFilter.filterType === FilterType.exactly; +}; + +export function time(filter: ExpressionValueFilter) { + if (!isTimeFilter(filter) || !filter.column) { + throw new Error('column is required for Elasticsearch range filters'); + } + return { + range: { + [filter.column]: { gte: filter.from, lte: filter.to }, + }, + }; +} + +export function luceneQueryString(filter: ExpressionValueFilter) { + if (!isLuceneFilter(filter)) { + throw new Error('Filter is not a lucene filter'); + } + return { + query_string: { + query: filter.query || '*', + }, + }; +} + +export function exactly(filter: ExpressionValueFilter) { + if (!isExactlyFilter(filter)) { + throw new Error('Filter is not an exactly filter'); + } + return { + term: { + [filter.column]: { + value: filter.value, + }, + }, + }; +} + +export const filters: Record = { + exactly, + time, + luceneQueryString, +}; diff --git a/x-pack/plugins/canvas/server/lib/get_es_filter.js b/x-pack/plugins/canvas/server/lib/get_es_filter.ts similarity index 75% rename from x-pack/plugins/canvas/server/lib/get_es_filter.js rename to x-pack/plugins/canvas/server/lib/get_es_filter.ts index 7c025ed8dee9b..acc222ecc376f 100644 --- a/x-pack/plugins/canvas/server/lib/get_es_filter.js +++ b/x-pack/plugins/canvas/server/lib/get_es_filter.ts @@ -10,11 +10,11 @@ filter is the abstracted canvas filter. */ -/*eslint import/namespace: ['error', { allowComputed: true }]*/ -import * as filters from './filters'; +import { filters } from './filters'; +import { ExpressionValueFilter } from '../../types'; -export function getESFilter(filter) { - if (!filters[filter.filterType]) { +export function getESFilter(filter: ExpressionValueFilter) { + if (!filter.filterType || !filters[filter.filterType]) { throw new Error(`Unknown filter type: ${filter.filterType}`); } diff --git a/x-pack/plugins/canvas/server/lib/normalize_type.js b/x-pack/plugins/canvas/server/lib/normalize_type.ts similarity index 89% rename from x-pack/plugins/canvas/server/lib/normalize_type.js rename to x-pack/plugins/canvas/server/lib/normalize_type.ts index fda2fbe631646..b684325aacba9 100644 --- a/x-pack/plugins/canvas/server/lib/normalize_type.js +++ b/x-pack/plugins/canvas/server/lib/normalize_type.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export function normalizeType(type) { - const normalTypes = { +export function normalizeType(type: string) { + const normalTypes: Record = { string: ['string', 'text', 'keyword', '_type', '_id', '_index', 'geo_point'], number: [ 'float', diff --git a/x-pack/plugins/canvas/server/lib/query_es_sql.js b/x-pack/plugins/canvas/server/lib/query_es_sql.js deleted file mode 100644 index 442703b00ea3a..0000000000000 --- a/x-pack/plugins/canvas/server/lib/query_es_sql.js +++ /dev/null @@ -1,59 +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 { map, zipObject } from 'lodash'; -import { buildBoolArray } from './build_bool_array'; -import { sanitizeName } from './sanitize_name'; -import { normalizeType } from './normalize_type'; - -export const queryEsSQL = (elasticsearchClient, { count, query, filter, timezone }) => - elasticsearchClient('transport.request', { - path: '/_sql?format=json', - method: 'POST', - body: { - query, - time_zone: timezone, - fetch_size: count, - client_id: 'canvas', - filter: { - bool: { - must: [{ match_all: {} }, ...buildBoolArray(filter)], - }, - }, - }, - }) - .then((res) => { - const columns = res.columns.map(({ name, type }) => { - return { name: sanitizeName(name), type: normalizeType(type) }; - }); - const columnNames = map(columns, 'name'); - const rows = res.rows.map((row) => zipObject(columnNames, row)); - - if (!!res.cursor) { - elasticsearchClient('transport.request', { - path: '/_sql/close', - method: 'POST', - body: { - cursor: res.cursor, - }, - }).catch((e) => { - throw new Error(`Unexpected error from Elasticsearch: ${e.message}`); - }); - } - - return { - type: 'datatable', - columns, - rows, - }; - }) - .catch((e) => { - if (e.message.indexOf('parsing_exception') > -1) { - throw new Error( - `Couldn't parse Elasticsearch SQL query. You may need to add double quotes to names containing special characters. Check your query and try again. Error: ${e.message}` - ); - } - throw new Error(`Unexpected error from Elasticsearch: ${e.message}`); - }); diff --git a/x-pack/plugins/canvas/server/lib/query_es_sql.test.ts b/x-pack/plugins/canvas/server/lib/query_es_sql.test.ts new file mode 100644 index 0000000000000..c3c122d1e301a --- /dev/null +++ b/x-pack/plugins/canvas/server/lib/query_es_sql.test.ts @@ -0,0 +1,106 @@ +/* + * 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 { zipObject } from 'lodash'; +import { queryEsSQL } from './query_es_sql'; +// @ts-expect-error +import { buildBoolArray } from './build_bool_array'; + +const response = { + columns: [ + { name: 'One', type: 'keyword' }, + { name: 'Two', type: 'keyword' }, + ], + rows: [ + ['foo', 'bar'], + ['buz', 'baz'], + ], + cursor: 'cursor-value', +}; + +const baseArgs = { + count: 1, + query: 'query', + filter: [], + timezone: 'timezone', +}; + +const getApi = (resp = response) => { + const api = jest.fn(); + api.mockResolvedValue(resp); + return api; +}; + +describe('query_es_sql', () => { + it('should call the api with the given args', async () => { + const api = getApi(); + + queryEsSQL(api, baseArgs); + + expect(api).toHaveBeenCalled(); + const givenArgs = api.mock.calls[0][1]; + + expect(givenArgs.body.fetch_size).toBe(baseArgs.count); + expect(givenArgs.body.query).toBe(baseArgs.query); + expect(givenArgs.body.time_zone).toBe(baseArgs.timezone); + }); + + it('formats the response', async () => { + const api = getApi(); + + const result = await queryEsSQL(api, baseArgs); + + const expectedColumns = response.columns.map((c) => ({ name: c.name, type: 'string' })); + const columnNames = expectedColumns.map((c) => c.name); + const expectedRows = response.rows.map((r) => zipObject(columnNames, r)); + + expect(result.type).toBe('datatable'); + expect(result.columns).toEqual(expectedColumns); + expect(result.rows).toEqual(expectedRows); + }); + + it('fetches pages until it has the requested count', async () => { + const pageOne = { + columns: [ + { name: 'One', type: 'keyword' }, + { name: 'Two', type: 'keyword' }, + ], + rows: [['foo', 'bar']], + cursor: 'cursor-value', + }; + + const pageTwo = { + rows: [['buz', 'baz']], + }; + + const api = getApi(pageOne); + api.mockReturnValueOnce(pageOne).mockReturnValueOnce(pageTwo); + + const result = await queryEsSQL(api, { ...baseArgs, count: 2 }); + expect(result.rows).toHaveLength(2); + }); + + it('closes any cursors that remain open', async () => { + const api = getApi(); + + await queryEsSQL(api, baseArgs); + expect(api.mock.calls[1][1].body.cursor).toBe(response.cursor); + }); + + it('throws on errors', async () => { + const api = getApi(); + api.mockRejectedValueOnce(new Error('parsing_exception')); + api.mockRejectedValueOnce(new Error('generic es error')); + + expect(queryEsSQL(api, baseArgs)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Couldn't parse Elasticsearch SQL query. You may need to add double quotes to names containing special characters. Check your query and try again. Error: parsing_exception"` + ); + + expect(queryEsSQL(api, baseArgs)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unexpected error from Elasticsearch: generic es error"` + ); + }); +}); diff --git a/x-pack/plugins/canvas/server/lib/query_es_sql.ts b/x-pack/plugins/canvas/server/lib/query_es_sql.ts new file mode 100644 index 0000000000000..8639cfa31dca8 --- /dev/null +++ b/x-pack/plugins/canvas/server/lib/query_es_sql.ts @@ -0,0 +1,96 @@ +/* + * 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 { map, zipObject } from 'lodash'; +import { buildBoolArray } from './build_bool_array'; +import { sanitizeName } from './sanitize_name'; +import { normalizeType } from './normalize_type'; +import { LegacyAPICaller } from '../../../../../src/core/server'; +import { ExpressionValueFilter } from '../../types'; + +interface Args { + count: number; + query: string; + timezone?: string; + filter: ExpressionValueFilter[]; +} + +interface CursorResponse { + cursor?: string; + rows: string[][]; +} + +type QueryResponse = CursorResponse & { + columns: Array<{ + name: string; + type: string; + }>; + cursor?: string; + rows: string[][]; +}; + +export const queryEsSQL = async ( + elasticsearchClient: LegacyAPICaller, + { count, query, filter, timezone }: Args +) => { + try { + let response: QueryResponse = await elasticsearchClient('transport.request', { + path: '/_sql?format=json', + method: 'POST', + body: { + query, + time_zone: timezone, + fetch_size: count, + client_id: 'canvas', + filter: { + bool: { + must: [{ match_all: {} }, ...buildBoolArray(filter)], + }, + }, + }, + }); + + const columns = response.columns.map(({ name, type }) => { + return { name: sanitizeName(name), type: normalizeType(type) }; + }); + const columnNames = map(columns, 'name'); + let rows = response.rows.map((row) => zipObject(columnNames, row)); + + while (rows.length < count && response.cursor !== undefined) { + response = await elasticsearchClient('transport.request', { + path: '/_sql?format=json', + method: 'POST', + body: { + cursor: response.cursor, + }, + }); + + rows = [...rows, ...response.rows.map((row) => zipObject(columnNames, row))]; + } + + if (response.cursor !== undefined) { + elasticsearchClient('transport.request', { + path: '/_sql/close', + method: 'POST', + body: { + cursor: response.cursor, + }, + }); + } + + return { + type: 'datatable', + columns, + rows, + }; + } catch (e) { + if (e.message.indexOf('parsing_exception') > -1) { + throw new Error( + `Couldn't parse Elasticsearch SQL query. You may need to add double quotes to names containing special characters. Check your query and try again. Error: ${e.message}` + ); + } + throw new Error(`Unexpected error from Elasticsearch: ${e.message}`); + } +}; diff --git a/x-pack/plugins/canvas/server/lib/sanitize_name.js b/x-pack/plugins/canvas/server/lib/sanitize_name.ts similarity index 85% rename from x-pack/plugins/canvas/server/lib/sanitize_name.js rename to x-pack/plugins/canvas/server/lib/sanitize_name.ts index 4c787c816a331..781ab20509b36 100644 --- a/x-pack/plugins/canvas/server/lib/sanitize_name.js +++ b/x-pack/plugins/canvas/server/lib/sanitize_name.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export function sanitizeName(name) { +export function sanitizeName(name: string) { // invalid characters const invalid = ['(', ')']; const pattern = invalid.map((v) => escapeRegExp(v)).join('|'); @@ -12,6 +12,6 @@ export function sanitizeName(name) { return name.replace(regex, '_'); } -function escapeRegExp(string) { +function escapeRegExp(string: string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } diff --git a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts index 7a9830124e305..000b7f6029952 100644 --- a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts +++ b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts @@ -8,7 +8,6 @@ import { mapValues, keys } from 'lodash'; import { schema } from '@kbn/config-schema'; import { API_ROUTE } from '../../../common/lib'; import { catchErrorHandler } from '../catch_error_handler'; -// @ts-expect-error unconverted lib import { normalizeType } from '../../lib/normalize_type'; import { RouteInitializerDeps } from '..'; diff --git a/x-pack/plugins/canvas/types/filters.ts b/x-pack/plugins/canvas/types/filters.ts new file mode 100644 index 0000000000000..356ebbbb76ac0 --- /dev/null +++ b/x-pack/plugins/canvas/types/filters.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 { ExpressionValueFilter } from '.'; + +export enum FilterType { + luceneQueryString = 'luceneQueryString', + time = 'time', + exactly = 'exactly', +} + +export type CanvasTimeFilter = ExpressionValueFilter & { + filterType: typeof FilterType.time; + to: string; + from: string; +}; + +export type CanvasLuceneFilter = ExpressionValueFilter & { + filterType: typeof FilterType.luceneQueryString; + query: string; +}; + +export type CanvasExactlyFilter = ExpressionValueFilter & { + filterType: typeof FilterType.exactly; + value: string; + column: string; +}; + +export type CanvasFilter = CanvasTimeFilter | CanvasExactlyFilter | CanvasLuceneFilter; diff --git a/x-pack/plugins/canvas/types/index.ts b/x-pack/plugins/canvas/types/index.ts index 0799627ce9b5a..f39c2d4367f9e 100644 --- a/x-pack/plugins/canvas/types/index.ts +++ b/x-pack/plugins/canvas/types/index.ts @@ -8,6 +8,7 @@ export * from '../../../../src/plugins/expressions/common'; export * from './assets'; export * from './canvas'; export * from './elements'; +export * from './filters'; export * from './functions'; export * from './renderers'; export * from './shortcuts'; From c66ea65ec1ca28172b93f4f5db8ae24e7023940e Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 31 Jul 2020 15:22:04 +0200 Subject: [PATCH 33/33] [APM] Use apmEventClient for querying APM event indices (#73449) Co-authored-by: Elastic Machine --- x-pack/plugins/apm/common/processor_event.ts | 14 + x-pack/plugins/apm/common/projections.ts | 16 ++ .../apm/common/projections/services.ts | 64 ----- .../app/ErrorGroupOverview/index.tsx | 4 +- .../components/app/RumDashboard/index.tsx | 4 +- .../components/app/ServiceMetrics/index.tsx | 4 +- .../app/ServiceNodeOverview/index.tsx | 4 +- .../components/app/ServiceOverview/index.tsx | 4 +- .../components/app/TraceOverview/index.tsx | 4 +- .../app/TransactionDetails/index.tsx | 4 +- .../app/TransactionOverview/index.tsx | 4 +- .../components/shared/KueryBar/index.tsx | 2 +- .../shared/LocalUIFilters/index.tsx | 4 +- .../shared/charts/MetricsChart/index.tsx | 2 +- .../context/UrlParamsContext/helpers.ts | 7 +- .../public/context/UrlParamsContext/types.ts | 4 +- .../public/hooks/useDynamicIndexPattern.ts | 4 +- .../apm/public/hooks/useLocalUIFilters.ts | 4 +- .../plugins/apm/public/utils/testHelpers.tsx | 5 +- .../get_all_environments.test.ts.snap | 42 +-- .../lib/environments/get_all_environments.ts | 25 +- .../errors/__snapshots__/queries.test.ts.snap | 33 ++- .../__snapshots__/queries.test.ts.snap | 22 +- .../__snapshots__/get_buckets.test.ts.snap | 11 +- .../__tests__/get_buckets.test.ts | 8 +- .../lib/errors/distribution/get_buckets.ts | 11 +- .../apm/server/lib/errors/get_error_group.ts | 12 +- .../apm/server/lib/errors/get_error_groups.ts | 25 +- .../call_client_with_debug.ts | 72 +++++ .../add_filter_to_exclude_legacy_data.ts | 31 +++ .../create_apm_event_client/index.ts | 91 +++++++ .../unpack_processor_events.ts | 61 +++++ .../create_internal_es_client/index.ts | 66 +++++ .../apm/server/lib/helpers/es_client.test.ts | 48 ---- .../apm/server/lib/helpers/es_client.ts | 228 ---------------- .../server/lib/helpers/setup_request.test.ts | 250 +++++++++--------- .../apm/server/lib/helpers/setup_request.ts | 33 +-- .../get_dynamic_index_pattern.ts | 19 +- .../__snapshots__/queries.test.ts.snap | 165 ++++++------ .../java/gc/fetch_and_transform_gc_metrics.ts | 8 +- .../metrics/fetch_and_transform_metrics.ts | 12 +- .../get_service_count.ts | 34 +-- .../get_transaction_coordinates.ts | 14 +- .../lib/observability_overview/has_data.ts | 32 +-- .../__snapshots__/queries.test.ts.snap | 44 ++- .../lib/rum_client/get_client_metrics.ts | 8 +- .../rum_client/get_page_load_distribution.ts | 12 +- .../lib/rum_client/get_page_view_trends.ts | 8 +- .../lib/rum_client/get_pl_dist_breakdown.ts | 18 +- .../server/lib/rum_client/get_rum_services.ts | 8 +- .../lib/rum_client/get_visitor_breakdown.ts | 8 +- .../fetch_service_paths_from_trace_ids.ts | 22 +- .../server/lib/service_map/get_service_map.ts | 8 +- .../get_service_map_service_node_info.test.ts | 4 +- .../get_service_map_service_node_info.ts | 42 +-- .../lib/service_map/get_trace_sample_ids.ts | 17 +- .../__snapshots__/queries.test.ts.snap | 33 ++- .../apm/server/lib/service_nodes/index.ts | 8 +- .../__snapshots__/queries.test.ts.snap | 153 ++++------- .../get_derived_service_annotations.ts | 25 +- .../lib/services/get_service_agent_name.ts | 21 +- .../lib/services/get_service_node_metadata.ts | 8 +- .../services/get_service_transaction_types.ts | 11 +- .../get_services/get_legacy_data_status.ts | 19 +- .../get_services/get_services_items.ts | 4 +- .../get_services/get_services_items_stats.ts | 132 +++------ .../get_services/has_historical_agent_data.ts | 39 +-- .../__snapshots__/queries.test.ts.snap | 27 +- .../create_or_update_configuration.ts | 2 +- .../get_agent_name_by_service.ts | 29 +- .../agent_configuration/get_service_names.ts | 31 +-- .../get_transaction.test.ts.snap | 25 +- .../create_or_update_custom_link.ts | 2 +- .../settings/custom_link/get_transaction.ts | 15 +- .../traces/__snapshots__/queries.test.ts.snap | 11 +- .../apm/server/lib/traces/get_trace_items.ts | 32 +-- .../__snapshots__/queries.test.ts.snap | 77 +++--- .../server/lib/transaction_groups/fetcher.ts | 9 +- .../lib/transaction_groups/get_error_rate.ts | 10 +- .../get_transaction_group_stats.ts | 11 +- .../__snapshots__/queries.test.ts.snap | 77 +++--- .../avg_duration_by_browser/fetcher.test.ts | 2 +- .../avg_duration_by_browser/fetcher.ts | 10 +- .../avg_duration_by_country/index.ts | 11 +- .../lib/transactions/breakdown/index.test.ts | 2 +- .../lib/transactions/breakdown/index.ts | 11 +- .../__snapshots__/fetcher.test.ts.snap | 11 +- .../get_timeseries_data/fetcher.test.ts | 14 +- .../charts/get_timeseries_data/fetcher.ts | 11 +- .../distribution/get_buckets/fetcher.ts | 12 +- .../distribution/get_distribution_max.ts | 11 +- .../lib/transactions/get_transaction/index.ts | 14 +- .../get_transaction_by_trace/index.ts | 16 +- .../__snapshots__/queries.test.ts.snap | 42 +-- .../server/lib/ui_filters/get_environments.ts | 23 +- .../__snapshots__/queries.test.ts.snap | 21 +- .../get_local_filter_query.ts | 4 +- .../lib/ui_filters/local_ui_filters/index.ts | 6 +- .../local_ui_filters/queries.test.ts | 2 +- .../{common => server}/projections/errors.ts | 15 +- .../{common => server}/projections/metrics.ts | 17 +- .../projections/rum_overview.ts | 13 +- .../projections/service_nodes.ts | 3 +- .../apm/server/projections/services.ts | 47 ++++ .../projections/transaction_groups.ts | 7 +- .../projections/transactions.ts | 15 +- .../{common => server}/projections/typings.ts | 16 +- .../util/merge_projection/index.test.ts | 21 +- .../util/merge_projection/index.ts | 8 +- .../plugins/apm/server/routes/ui_filters.ts | 16 +- 110 files changed, 1346 insertions(+), 1576 deletions(-) create mode 100644 x-pack/plugins/apm/common/projections.ts delete mode 100644 x-pack/plugins/apm/common/projections/services.ts create mode 100644 x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts create mode 100644 x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts create mode 100644 x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts create mode 100644 x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts create mode 100644 x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts delete mode 100644 x-pack/plugins/apm/server/lib/helpers/es_client.test.ts delete mode 100644 x-pack/plugins/apm/server/lib/helpers/es_client.ts rename x-pack/plugins/apm/{common => server}/projections/errors.ts (70%) rename x-pack/plugins/apm/{common => server}/projections/metrics.ts (72%) rename x-pack/plugins/apm/{common => server}/projections/rum_overview.ts (71%) rename x-pack/plugins/apm/{common => server}/projections/service_nodes.ts (88%) create mode 100644 x-pack/plugins/apm/server/projections/services.ts rename x-pack/plugins/apm/{common => server}/projections/transaction_groups.ts (86%) rename x-pack/plugins/apm/{common => server}/projections/transactions.ts (76%) rename x-pack/plugins/apm/{common => server}/projections/typings.ts (56%) rename x-pack/plugins/apm/{common => server}/projections/util/merge_projection/index.test.ts (73%) rename x-pack/plugins/apm/{common => server}/projections/util/merge_projection/index.ts (82%) diff --git a/x-pack/plugins/apm/common/processor_event.ts b/x-pack/plugins/apm/common/processor_event.ts index 3e8b0ba0e8b5e..cd8bcaa1de237 100644 --- a/x-pack/plugins/apm/common/processor_event.ts +++ b/x-pack/plugins/apm/common/processor_event.ts @@ -8,4 +8,18 @@ export enum ProcessorEvent { transaction = 'transaction', error = 'error', metric = 'metric', + span = 'span', + onboarding = 'onboarding', + sourcemap = 'sourcemap', } +/** + * Processor events that are searchable in the UI via the query bar. + * + * Some client-sideroutes will define 1 or more processor events that + * will be used to fetch the dynamic index pattern for the query bar. + */ + +export type UIProcessorEvent = + | ProcessorEvent.transaction + | ProcessorEvent.error + | ProcessorEvent.metric; diff --git a/x-pack/plugins/apm/common/projections.ts b/x-pack/plugins/apm/common/projections.ts new file mode 100644 index 0000000000000..a5fd9d3951cc9 --- /dev/null +++ b/x-pack/plugins/apm/common/projections.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. + */ + +export enum Projection { + services = 'services', + transactionGroups = 'transactionGroups', + traces = 'traces', + transactions = 'transactions', + metrics = 'metrics', + errorGroups = 'errorGroups', + serviceNodes = 'serviceNodes', + rumOverview = 'rumOverview', +} diff --git a/x-pack/plugins/apm/common/projections/services.ts b/x-pack/plugins/apm/common/projections/services.ts deleted file mode 100644 index 809caeeaf6088..0000000000000 --- a/x-pack/plugins/apm/common/projections/services.ts +++ /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 { - Setup, - SetupUIFilters, - SetupTimeRange, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../server/lib/helpers/setup_request'; -import { SERVICE_NAME, PROCESSOR_EVENT } from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../utils/range_filter'; - -export function getServicesProjection({ - setup, - noEvents, -}: { - setup: Setup & SetupTimeRange & SetupUIFilters; - noEvents?: boolean; -}) { - const { start, end, uiFiltersES, indices } = setup; - - return { - ...(noEvents - ? {} - : { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], - }), - body: { - size: 0, - query: { - bool: { - filter: [ - ...(noEvents - ? [] - : [ - { - terms: { - [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'], - }, - }, - ]), - { range: rangeFilter(start, end) }, - ...uiFiltersES, - ], - }, - }, - aggs: { - services: { - terms: { - field: SERVICE_NAME, - }, - }, - }, - }, - }; -} diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx index fe2303d645ec9..92ea044720531 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx @@ -14,7 +14,7 @@ import { import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import { useTrackPageview } from '../../../../../observability/public'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { useFetcher } from '../../../hooks/useFetcher'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { callApmApi } from '../../../services/rest/createCallApmApi'; @@ -79,7 +79,7 @@ function ErrorGroupOverview() { params: { serviceName, }, - projection: PROJECTION.ERROR_GROUPS, + projection: Projection.errorGroups, }; return config; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx index 9b88202b2e5ef..8d1959ec14d15 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx @@ -13,7 +13,7 @@ import { } from '@elastic/eui'; import { useTrackPageview } from '../../../../../observability/public'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { RumDashboard } from './RumDashboard'; import { ServiceNameFilter } from '../../shared/LocalUIFilters/ServiceNameFilter'; import { useUrlParams } from '../../../hooks/useUrlParams'; @@ -28,7 +28,7 @@ export function RumOverview() { const localUIFiltersConfig = useMemo(() => { const config: React.ComponentProps = { filterNames: ['transactionUrl', 'location', 'device', 'os', 'browser'], - projection: PROJECTION.RUM_OVERVIEW, + projection: Projection.rumOverview, }; return config; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx index 9af6a8d988c11..9b01f9ebb7e99 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx @@ -16,7 +16,7 @@ import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts'; import { MetricsChart } from '../../shared/charts/MetricsChart'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; interface ServiceMetricsProps { @@ -36,7 +36,7 @@ export function ServiceMetrics({ agentName }: ServiceMetricsProps) { serviceName, serviceNodeName, }, - projection: PROJECTION.METRICS, + projection: Projection.metrics, showCount: false, }), [serviceName, serviceNodeName] diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx index 5537a73d228e8..3cde48aa483cb 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx @@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../common/i18n'; import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { ManagedTable, ITableColumn } from '../../shared/ManagedTable'; @@ -46,7 +46,7 @@ function ServiceNodeOverview() { params: { serviceName, }, - projection: PROJECTION.SERVICE_NODES, + projection: Projection.serviceNodes, }), [serviceName] ); diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx index 7d05ae90afb87..7146e471a7f82 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx @@ -15,7 +15,7 @@ import { NoServicesMessage } from './NoServicesMessage'; import { ServiceList } from './ServiceList'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../observability/public'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; @@ -88,7 +88,7 @@ export function ServiceOverview() { const localFiltersConfig: React.ComponentProps = useMemo( () => ({ filterNames: ['host', 'agentName'], - projection: PROJECTION.SERVICES, + projection: Projection.services, }), [] ); diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx index cdebb3aac129b..06b4459fb56eb 100644 --- a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx @@ -11,7 +11,7 @@ import { TraceList } from './TraceList'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../observability/public'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; type TracesAPIResponse = APIReturnType<'/api/apm/traces'>; @@ -48,7 +48,7 @@ export function TraceOverview() { const localUIFiltersConfig = useMemo(() => { const config: React.ComponentProps = { filterNames: ['transactionResult', 'host', 'containerId', 'podName'], - projection: PROJECTION.TRACES, + projection: Projection.traces, }; return config; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx index c4d5be5874215..0dc2f607b1ef2 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -27,7 +27,7 @@ import { FETCH_STATUS } from '../../../hooks/useFetcher'; import { TransactionBreakdown } from '../../shared/TransactionBreakdown'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; import { useTrackPageview } from '../../../../../observability/public'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { HeightRetainer } from '../../shared/HeightRetainer'; import { ErroneousTransactionsRateChart } from '../../shared/charts/ErroneousTransactionsRateChart'; @@ -52,7 +52,7 @@ export function TransactionDetails() { const localUIFiltersConfig = useMemo(() => { const config: React.ComponentProps = { filterNames: ['transactionResult', 'serviceVersion'], - projection: PROJECTION.TRANSACTIONS, + projection: Projection.transactions, params: { transactionName, transactionType, diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx index 98702fe3686ff..d9bd3e59d281f 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -35,7 +35,7 @@ import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; import { useTrackPageview } from '../../../../../observability/public'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter'; @@ -103,7 +103,7 @@ export function TransactionOverview() { serviceName, transactionType, }, - projection: PROJECTION.TRANSACTION_GROUPS, + projection: Projection.transactionGroups, }), [serviceName, transactionType] ); diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx index 502f5f0034b5f..6c605886e6e00 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -9,7 +9,7 @@ import { uniqueId, startsWith } from 'lodash'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { fromQuery, toQuery } from '../Links/url_helpers'; -// @ts-ignore +// @ts-expect-error import { Typeahead } from './Typeahead'; import { getBoolFilter } from './get_bool_filter'; import { useLocation } from '../../../hooks/useLocation'; diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx index fedf96b4cc4ea..ba700e68b59bc 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx @@ -17,10 +17,10 @@ import styled from 'styled-components'; import { LocalUIFilterName } from '../../../../server/lib/ui_filters/local_ui_filters/config'; import { Filter } from './Filter'; import { useLocalUIFilters } from '../../../hooks/useLocalUIFilters'; -import { PROJECTION } from '../../../../common/projections/typings'; +import { Projection } from '../../../../common/projections'; interface Props { - projection: PROJECTION; + projection: Projection; filterNames: LocalUIFilterName[]; params?: Record; showCount?: boolean; diff --git a/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx index 61632700b81d8..5b167e8160ffa 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx @@ -7,7 +7,7 @@ import { EuiTitle } from '@elastic/eui'; import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { GenericMetricsChart } from '../../../../../server/lib/metrics/transform_metrics_chart'; -// @ts-ignore +// @ts-expect-error import CustomPlot from '../CustomPlot'; import { asDecimal, diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts index d9781400f2272..65514ff71d02b 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts @@ -7,10 +7,13 @@ import { compact, pickBy } from 'lodash'; import datemath from '@elastic/datemath'; import { IUrlParams } from './types'; -import { ProcessorEvent } from '../../../common/processor_event'; +import { + ProcessorEvent, + UIProcessorEvent, +} from '../../../common/processor_event'; interface PathParams { - processorEvent?: ProcessorEvent; + processorEvent?: UIProcessorEvent; serviceName?: string; errorGroupId?: string; serviceNodeName?: string; diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts index 78fe662b88d75..7b50a705afa33 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts @@ -6,7 +6,7 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { LocalUIFilterName } from '../../../server/lib/ui_filters/local_ui_filters/config'; -import { ProcessorEvent } from '../../../common/processor_event'; +import { UIProcessorEvent } from '../../../common/processor_event'; export type IUrlParams = { detailTab?: string; @@ -32,6 +32,6 @@ export type IUrlParams = { pageSize?: number; serviceNodeName?: string; searchTerm?: string; - processorEvent?: ProcessorEvent; + processorEvent?: UIProcessorEvent; traceIdLink?: string; } & Partial>; diff --git a/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts b/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts index 64f333d72f0f5..0b4978acdfcb1 100644 --- a/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts +++ b/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts @@ -5,10 +5,10 @@ */ import { useFetcher } from './useFetcher'; -import { ProcessorEvent } from '../../common/processor_event'; +import { UIProcessorEvent } from '../../common/processor_event'; export function useDynamicIndexPattern( - processorEvent: ProcessorEvent | undefined + processorEvent: UIProcessorEvent | undefined ) { const { data, status } = useFetcher( (callApmApi) => { diff --git a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts index 3354e676cf323..45ede7e7f2607 100644 --- a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts +++ b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts @@ -17,7 +17,7 @@ import { import { history } from '../utils/history'; import { toQuery, fromQuery } from '../components/shared/Links/url_helpers'; import { removeUndefinedProps } from '../context/UrlParamsContext/helpers'; -import { PROJECTION } from '../../common/projections/typings'; +import { Projection } from '../../common/projections'; import { pickKeys } from '../../common/utils/pick_keys'; import { useCallApi } from './useCallApi'; @@ -35,7 +35,7 @@ export function useLocalUIFilters({ filterNames, params, }: { - projection: PROJECTION; + projection: Projection; filterNames: LocalUIFilterName[]; params?: Record; }) { diff --git a/x-pack/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx index 418312743c324..e750102de2baa 100644 --- a/x-pack/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx @@ -99,10 +99,9 @@ export function expectTextsInDocument(output: any, texts: string[]) { } interface MockSetup { - dynamicIndexPattern: any; start: number; end: number; - client: any; + apmEventClient: any; internalClient: any; config: APMConfig; uiFiltersES: ESFilter[]; @@ -148,7 +147,7 @@ export async function inspectSearchParams( const mockSetup = { start: 1528113600000, end: 1528977600000, - client: { search: spy } as any, + apmEventClient: { search: spy } as any, internalClient: { search: spy } as any, config: new Proxy({}, { get: () => 'myIndex' }) as APMConfig, uiFiltersES: [{ term: { 'my.custom.ui.filter': 'foo-bar' } }], diff --git a/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap index b943102b39de8..da2309afa07cf 100644 --- a/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap @@ -2,6 +2,13 @@ exports[`getAllEnvironments fetches all environments 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "error", + "metric", + ], + }, "body": Object { "aggs": Object { "environments": Object { @@ -15,15 +22,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "term": Object { "service.name": "test", @@ -34,16 +32,18 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; exports[`getAllEnvironments fetches all environments with includeMissing 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "error", + "metric", + ], + }, "body": Object { "aggs": Object { "environments": Object { @@ -57,15 +57,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "term": Object { "service.name": "test", @@ -76,10 +67,5 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; diff --git a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts index 9b17033a1f2a5..423b87cb78c3c 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { Setup } from '../helpers/setup_request'; import { - PROCESSOR_EVENT, SERVICE_NAME, SERVICE_ENVIRONMENT, } from '../../../common/elasticsearch_fieldnames'; @@ -21,7 +21,7 @@ export async function getAllEnvironments({ setup: Setup; includeMissing?: boolean; }) { - const { client, indices } = setup; + const { apmEventClient } = setup; // omit filter for service.name if "All" option is selected const serviceNameFilter = serviceName @@ -29,21 +29,18 @@ export async function getAllEnvironments({ : []; const params = { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, body: { size: 0, query: { bool: { - filter: [ - { - terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] }, - }, - ...serviceNameFilter, - ], + filter: [...serviceNameFilter], }, }, aggs: { @@ -58,7 +55,7 @@ export async function getAllEnvironments({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const environments = resp.aggregations?.environments.buckets.map( (bucket) => bucket.key as string diff --git a/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap index 982ad558dc91d..63b6c9cde4d0d 100644 --- a/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`error queries fetches a single error group 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "query": Object { "bool": Object { @@ -11,11 +16,6 @@ Object { "service.name": "serviceName", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "term": Object { "error.grouping_key": "groupId", @@ -57,12 +57,16 @@ Object { }, ], }, - "index": "myIndex", } `; exports[`error queries fetches multiple error groups 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "error_groups": Object { @@ -104,11 +108,6 @@ Object { "service.name": "serviceName", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -128,12 +127,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`error queries fetches multiple error groups when sortField = latestOccurrenceAt 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "error_groups": Object { @@ -180,11 +183,6 @@ Object { "service.name": "serviceName", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -204,6 +202,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap index b71b2d697126a..ea142ca2acc00 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/errors/distribution/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`error distribution queries fetches an error distribution 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "distribution": Object { @@ -19,11 +24,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "term": Object { "service.name": "serviceName", @@ -48,12 +48,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`error distribution queries fetches an error distribution with a group id 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "distribution": Object { @@ -71,11 +75,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "term": Object { "service.name": "serviceName", @@ -105,6 +104,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap index d336d71424750..085bedf774c46 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap @@ -4,6 +4,11 @@ exports[`timeseriesFetcher should make the correct query 1`] = ` Array [ Array [ Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "distribution": Object { @@ -21,11 +26,6 @@ Array [ "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "term": Object { "service.name": "myServiceName", @@ -50,7 +50,6 @@ Array [ }, "size": 0, }, - "index": "apm-*", }, ], ] diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts index 5f23a9329a583..e0df4d7744610 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PROCESSOR_EVENT } from '../../../../../common/elasticsearch_fieldnames'; import { getBuckets } from '../get_buckets'; import { APMConfig } from '../../../..'; +import { ProcessorEvent } from '../../../../../common/processor_event'; describe('timeseriesFetcher', () => { let clientSpy: jest.Mock; @@ -29,7 +29,7 @@ describe('timeseriesFetcher', () => { setup: { start: 1528113600000, end: 1528977600000, - client: { + apmEventClient: { search: clientSpy, } as any, internalClient: { @@ -66,8 +66,6 @@ describe('timeseriesFetcher', () => { it('should limit query results to error documents', () => { const query = clientSpy.mock.calls[0][0]; - expect(query.body.query.bool.filter).toEqual( - expect.arrayContaining([{ term: { [PROCESSOR_EVENT]: 'error' } }]) - ); + expect(query.apm.events).toEqual([ProcessorEvent.error]); }); }); diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts index db36ad1ede91c..de6df15354e79 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { ESFilter } from '../../../../typings/elasticsearch'; import { ERROR_GROUP_ID, - PROCESSOR_EVENT, SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; import { rangeFilter } from '../../../../common/utils/range_filter'; @@ -28,9 +28,8 @@ export async function getBuckets({ bucketSize: number; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const filter: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: 'error' } }, { term: { [SERVICE_NAME]: serviceName } }, { range: rangeFilter(start, end) }, ...uiFiltersES, @@ -41,7 +40,9 @@ export async function getBuckets({ } const params = { - index: indices['apm_oss.errorIndices'], + apm: { + events: [ProcessorEvent.error], + }, body: { size: 0, query: { @@ -65,7 +66,7 @@ export async function getBuckets({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const buckets = (resp.aggregations?.distribution.buckets || []).map( (bucket) => ({ diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts b/x-pack/plugins/apm/server/lib/errors/get_error_group.ts index 3d20f84ccfbc2..b23c955b57183 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_group.ts @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { ERROR_GROUP_ID, - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_SAMPLED, } from '../../../common/elasticsearch_fieldnames'; import { PromiseReturnType } from '../../../typings/common'; -import { APMError } from '../../../typings/es_schemas/ui/apm_error'; import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, @@ -32,17 +31,18 @@ export async function getErrorGroup({ groupId: string; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const params = { - index: indices['apm_oss.errorIndices'], + apm: { + events: [ProcessorEvent.error as const], + }, body: { size: 1, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'error' } }, { term: { [ERROR_GROUP_ID]: groupId } }, { range: rangeFilter(start, end) }, ...uiFiltersES, @@ -57,7 +57,7 @@ export async function getErrorGroup({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const error = resp.hits.hits[0]?._source; const transactionId = error?.transaction?.id; const traceId = error?.trace?.id; diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts index ad216de271f37..ab1c2149be343 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts @@ -13,14 +13,13 @@ import { ERROR_LOG_MESSAGE, } from '../../../common/elasticsearch_fieldnames'; import { PromiseReturnType } from '../../../typings/common'; -import { APMError } from '../../../typings/es_schemas/ui/apm_error'; import { Setup, SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; -import { getErrorGroupsProjection } from '../../../common/projections/errors'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getErrorGroupsProjection } from '../../projections/errors'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { SortOptions } from '../../../typings/elasticsearch/aggregations'; export type ErrorGroupListAPIResponse = PromiseReturnType< @@ -38,7 +37,7 @@ export async function getErrorGroups({ sortDirection?: 'asc' | 'desc'; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { client } = setup; + const { apmEventClient } = setup; // sort buckets by last occurrence of error const sortByLatestOccurrence = sortField === 'latestOccurrenceAt'; @@ -92,23 +91,7 @@ export async function getErrorGroups({ }, }); - interface SampleError { - '@timestamp': APMError['@timestamp']; - error: { - log?: { - message: string; - }; - exception?: Array<{ - handled?: boolean; - message?: string; - type?: string; - }>; - culprit: APMError['error']['culprit']; - grouping_key: APMError['error']['grouping_key']; - }; - } - - const resp = await client.search(params); + const resp = await apmEventClient.search(params); // aggregations can be undefined when no matching indices are found. // this is an exception rather than the rule so the ES type does not account for this. diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts new file mode 100644 index 0000000000000..c475640595227 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_client_with_debug.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable no-console */ + +import chalk from 'chalk'; +import { + LegacyAPICaller, + KibanaRequest, +} from '../../../../../../../src/core/server'; + +function formatObj(obj: Record) { + return JSON.stringify(obj, null, 2); +} + +export async function callClientWithDebug({ + apiCaller, + operationName, + params, + debug, + request, +}: { + apiCaller: LegacyAPICaller; + operationName: string; + params: Record; + debug: boolean; + request: KibanaRequest; +}) { + const startTime = process.hrtime(); + + let res: any; + let esError = null; + try { + res = apiCaller(operationName, params); + } catch (e) { + // catch error and throw after outputting debug info + esError = e; + } + + if (debug) { + const highlightColor = esError ? 'bgRed' : 'inverse'; + const diff = process.hrtime(startTime); + const duration = `${Math.round(diff[0] * 1000 + diff[1] / 1e6)}ms`; + const routeInfo = `${request.route.method.toUpperCase()} ${ + request.route.path + }`; + + console.log( + chalk.bold[highlightColor](`=== Debug: ${routeInfo} (${duration}) ===`) + ); + + if (operationName === 'search') { + console.log(`GET ${params.index}/_${operationName}`); + console.log(formatObj(params.body)); + } else { + console.log(chalk.bold('ES operation:'), operationName); + + console.log(chalk.bold('ES query:')); + console.log(formatObj(params)); + } + console.log(`\n`); + } + + if (esError) { + throw esError; + } + + return res; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts new file mode 100644 index 0000000000000..494cd6cbf0eec --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts @@ -0,0 +1,31 @@ +/* + * 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 { cloneDeep } from 'lodash'; +import { OBSERVER_VERSION_MAJOR } from '../../../../../common/elasticsearch_fieldnames'; +import { + ESSearchRequest, + ESFilter, +} from '../../../../../typings/elasticsearch'; + +/* + Adds a range query to the ES request to exclude legacy data +*/ + +export function addFilterToExcludeLegacyData( + params: ESSearchRequest & { + body: { query: { bool: { filter: ESFilter[] } } }; + } +) { + const nextParams = cloneDeep(params); + + // add filter for omitting pre-7.x data + nextParams.body.query.bool.filter.push({ + range: { [OBSERVER_VERSION_MAJOR]: { gte: 7 } }, + }); + + return nextParams; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts new file mode 100644 index 0000000000000..2bfd3c94ed34c --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ValuesType } from 'utility-types'; +import { APMBaseDoc } from '../../../../../typings/es_schemas/raw/apm_base_doc'; +import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; +import { KibanaRequest } from '../../../../../../../../src/core/server'; +import { ProcessorEvent } from '../../../../../common/processor_event'; +import { + ESSearchRequest, + ESSearchResponse, +} from '../../../../../typings/elasticsearch'; +import { ApmIndicesConfig } from '../../../settings/apm_indices/get_apm_indices'; +import { APMRequestHandlerContext } from '../../../../routes/typings'; +import { addFilterToExcludeLegacyData } from './add_filter_to_exclude_legacy_data'; +import { callClientWithDebug } from '../call_client_with_debug'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { Span } from '../../../../../typings/es_schemas/ui/span'; +import { unpackProcessorEvents } from './unpack_processor_events'; + +export type APMEventESSearchRequest = Omit & { + apm: { + events: ProcessorEvent[]; + }; +}; + +type TypeOfProcessorEvent = { + [ProcessorEvent.error]: APMError; + [ProcessorEvent.transaction]: Transaction; + [ProcessorEvent.span]: Span; + [ProcessorEvent.metric]: APMBaseDoc; + [ProcessorEvent.onboarding]: unknown; + [ProcessorEvent.sourcemap]: unknown; +}[T]; + +type ESSearchRequestOf = Omit< + TParams, + 'apm' +> & { index: string[] | string }; + +type TypedSearchResponse< + TParams extends APMEventESSearchRequest +> = ESSearchResponse< + TypeOfProcessorEvent>, + ESSearchRequestOf +>; + +export type APMEventClient = ReturnType; + +export function createApmEventClient({ + context, + request, + indices, + options: { includeFrozen } = { includeFrozen: false }, +}: { + context: APMRequestHandlerContext; + request: KibanaRequest; + indices: ApmIndicesConfig; + options: { + includeFrozen: boolean; + }; +}) { + const client = context.core.elasticsearch.legacy.client; + + return { + search( + params: TParams, + { includeLegacyData } = { includeLegacyData: false } + ): Promise> { + const withProcessorEventFilter = unpackProcessorEvents(params, indices); + + const withPossibleLegacyDataFilter = !includeLegacyData + ? addFilterToExcludeLegacyData(withProcessorEventFilter) + : withProcessorEventFilter; + + return callClientWithDebug({ + apiCaller: client.callAsCurrentUser, + operationName: 'search', + params: { + ...withPossibleLegacyDataFilter, + ignore_throttled: !includeFrozen, + }, + request, + debug: context.params.query._debug, + }); + }, + }; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts new file mode 100644 index 0000000000000..d35403ad35d94 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.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 { uniq, defaultsDeep, cloneDeep } from 'lodash'; +import { PROCESSOR_EVENT } from '../../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../../common/processor_event'; +import { + ESSearchRequest, + ESFilter, +} from '../../../../../typings/elasticsearch'; +import { APMEventESSearchRequest } from '.'; +import { + ApmIndicesConfig, + ApmIndicesName, +} from '../../../settings/apm_indices/get_apm_indices'; + +export const processorEventIndexMap: Record = { + [ProcessorEvent.transaction]: 'apm_oss.transactionIndices', + [ProcessorEvent.span]: 'apm_oss.spanIndices', + [ProcessorEvent.metric]: 'apm_oss.metricsIndices', + [ProcessorEvent.error]: 'apm_oss.errorIndices', + [ProcessorEvent.sourcemap]: 'apm_oss.sourcemapIndices', + [ProcessorEvent.onboarding]: 'apm_oss.onboardingIndices', +}; + +export function unpackProcessorEvents( + request: APMEventESSearchRequest, + indices: ApmIndicesConfig +) { + const { apm, ...params } = request; + + const index = uniq( + apm.events.map((event) => indices[processorEventIndexMap[event]]) + ); + + const withFilterForProcessorEvent: ESSearchRequest & { + body: { query: { bool: { filter: ESFilter[] } } }; + } = defaultsDeep(cloneDeep(params), { + body: { + query: { + bool: { + filter: [], + }, + }, + }, + }); + + withFilterForProcessorEvent.body.query.bool.filter.push({ + terms: { + [PROCESSOR_EVENT]: apm.events, + }, + }); + + return { + index, + ...withFilterForProcessorEvent, + }; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts new file mode 100644 index 0000000000000..072391606d574 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + IndexDocumentParams, + IndicesCreateParams, + DeleteDocumentResponse, + DeleteDocumentParams, +} from 'elasticsearch'; +import { KibanaRequest } from 'src/core/server'; +import { APMRequestHandlerContext } from '../../../../routes/typings'; +import { + ESSearchResponse, + ESSearchRequest, +} from '../../../../../typings/elasticsearch'; +import { callClientWithDebug } from '../call_client_with_debug'; + +// `type` was deprecated in 7.0 +export type APMIndexDocumentParams = Omit, 'type'>; + +export type APMInternalClient = ReturnType; + +export function createInternalESClient({ + context, + request, +}: { + context: APMRequestHandlerContext; + request: KibanaRequest; +}) { + const { callAsInternalUser } = context.core.elasticsearch.legacy.client; + + const callEs = (operationName: string, params: Record) => { + return callClientWithDebug({ + apiCaller: callAsInternalUser, + operationName, + params, + request, + debug: context.params.query._debug, + }); + }; + + return { + search: async < + TDocument = unknown, + TSearchRequest extends ESSearchRequest = ESSearchRequest + >( + params: TSearchRequest + ): Promise> => { + return callEs('search', params); + }, + index: (params: APMIndexDocumentParams) => { + return callEs('index', params); + }, + delete: ( + params: Omit + ): Promise => { + return callEs('delete', params); + }, + indicesCreate: (params: IndicesCreateParams) => { + return callEs('indices.create', params); + }, + }; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/es_client.test.ts b/x-pack/plugins/apm/server/lib/helpers/es_client.test.ts deleted file mode 100644 index 61c9d751bf533..0000000000000 --- a/x-pack/plugins/apm/server/lib/helpers/es_client.test.ts +++ /dev/null @@ -1,48 +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 { isApmIndex } from './es_client'; - -describe('isApmIndex', () => { - const apmIndices = [ - 'apm-*-metric-*', - 'apm-*-onboarding-*', - 'apm-*-span-*', - 'apm-*-transaction-*', - 'apm-*-error-*', - ]; - describe('when indexParam is a string', () => { - it('should return true if it matches any of the items in apmIndices', () => { - const indexParam = 'apm-*-transaction-*'; - expect(isApmIndex(apmIndices, indexParam)).toBe(true); - }); - - it('should return false if it does not match any of the items in `apmIndices`', () => { - const indexParam = '.ml-anomalies-*'; - expect(isApmIndex(apmIndices, indexParam)).toBe(false); - }); - }); - - describe('when indexParam is an array', () => { - it('should return true if all values in `indexParam` matches values in `apmIndices`', () => { - const indexParam = ['apm-*-transaction-*', 'apm-*-span-*']; - expect(isApmIndex(apmIndices, indexParam)).toBe(true); - }); - - it("should return false if some of the values don't match with `apmIndices`", () => { - const indexParam = ['apm-*-transaction-*', '.ml-anomalies-*']; - expect(isApmIndex(apmIndices, indexParam)).toBe(false); - }); - }); - - describe('when indexParam is neither a string or an array', () => { - it('should return false', () => { - [true, false, undefined].forEach((indexParam) => { - expect(isApmIndex(apmIndices, indexParam)).toBe(false); - }); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/plugins/apm/server/lib/helpers/es_client.ts deleted file mode 100644 index 2d730933e2473..0000000000000 --- a/x-pack/plugins/apm/server/lib/helpers/es_client.ts +++ /dev/null @@ -1,228 +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. - */ - -/* eslint-disable no-console */ -import { - IndexDocumentParams, - SearchParams, - IndicesCreateParams, - DeleteDocumentResponse, - DeleteDocumentParams, -} from 'elasticsearch'; -import { cloneDeep, isString, merge } from 'lodash'; -import { KibanaRequest } from 'src/core/server'; -import chalk from 'chalk'; -import { - ESSearchRequest, - ESSearchResponse, -} from '../../../typings/elasticsearch'; -import { OBSERVER_VERSION_MAJOR } from '../../../common/elasticsearch_fieldnames'; -import { pickKeys } from '../../../common/utils/pick_keys'; -import { APMRequestHandlerContext } from '../../routes/typings'; -import { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; - -// `type` was deprecated in 7.0 -export type APMIndexDocumentParams = Omit, 'type'>; - -export interface IndexPrivileges { - has_all_requested: boolean; - index: Record; -} - -interface IndexPrivilegesParams { - index: Array<{ - names: string[] | string; - privileges: string[]; - }>; -} - -export function isApmIndex( - apmIndices: string[], - indexParam: SearchParams['index'] -) { - if (isString(indexParam)) { - return apmIndices.includes(indexParam); - } else if (Array.isArray(indexParam)) { - // return false if at least one of the indices is not an APM index - return indexParam.every((index) => apmIndices.includes(index)); - } - return false; -} - -function addFilterForLegacyData( - apmIndices: string[], - params: ESSearchRequest, - { includeLegacyData = false } = {} -): SearchParams { - // search across all data (including data) - if (includeLegacyData || !isApmIndex(apmIndices, params.index)) { - return params; - } - - const nextParams = merge( - { - body: { - query: { - bool: { - filter: [], - }, - }, - }, - }, - cloneDeep(params) - ); - - // add filter for omitting pre-7.x data - nextParams.body.query.bool.filter.push({ - range: { [OBSERVER_VERSION_MAJOR]: { gte: 7 } }, - }); - - return nextParams; -} - -// add additional params for search (aka: read) requests -function getParamsForSearchRequest({ - context, - params, - indices, - includeFrozen, - includeLegacyData, -}: { - context: APMRequestHandlerContext; - params: ESSearchRequest; - indices: ApmIndicesConfig; - includeFrozen: boolean; - includeLegacyData?: boolean; -}) { - // Get indices for legacy data filter (only those which apply) - const apmIndices = Object.values( - pickKeys( - indices, - 'apm_oss.sourcemapIndices', - 'apm_oss.errorIndices', - 'apm_oss.onboardingIndices', - 'apm_oss.spanIndices', - 'apm_oss.transactionIndices', - 'apm_oss.metricsIndices' - ) - ); - return { - ...addFilterForLegacyData(apmIndices, params, { includeLegacyData }), // filter out pre-7.0 data - ignore_throttled: !includeFrozen, // whether to query frozen indices or not - }; -} - -interface APMOptions { - includeLegacyData: boolean; -} - -interface ClientCreateOptions { - clientAsInternalUser?: boolean; - indices: ApmIndicesConfig; - includeFrozen: boolean; -} - -export type ESClient = ReturnType; - -function formatObj(obj: Record) { - return JSON.stringify(obj, null, 2); -} - -export function getESClient( - context: APMRequestHandlerContext, - request: KibanaRequest, - { clientAsInternalUser = false, indices, includeFrozen }: ClientCreateOptions -) { - const { - callAsCurrentUser, - callAsInternalUser, - } = context.core.elasticsearch.legacy.client; - - async function callEs(operationName: string, params: Record) { - const startTime = process.hrtime(); - - let res: any; - let esError = null; - try { - res = clientAsInternalUser - ? await callAsInternalUser(operationName, params) - : await callAsCurrentUser(operationName, params); - } catch (e) { - // catch error and throw after outputting debug info - esError = e; - } - - if (context.params.query._debug) { - const highlightColor = esError ? 'bgRed' : 'inverse'; - const diff = process.hrtime(startTime); - const duration = `${Math.round(diff[0] * 1000 + diff[1] / 1e6)}ms`; - const routeInfo = `${request.route.method.toUpperCase()} ${ - request.route.path - }`; - - console.log( - chalk.bold[highlightColor](`=== Debug: ${routeInfo} (${duration}) ===`) - ); - - if (operationName === 'search') { - console.log(`GET ${params.index}/_${operationName}`); - console.log(formatObj(params.body)); - } else { - console.log(chalk.bold('ES operation:'), operationName); - - console.log(chalk.bold('ES query:')); - console.log(formatObj(params)); - } - console.log(`\n`); - } - - if (esError) { - throw esError; - } - - return res; - } - - return { - search: async < - TDocument = unknown, - TSearchRequest extends ESSearchRequest = {} - >( - params: TSearchRequest, - apmOptions?: APMOptions - ): Promise> => { - const nextParams = await getParamsForSearchRequest({ - context, - params, - indices, - includeFrozen, - ...apmOptions, - }); - - return callEs('search', nextParams); - }, - index: (params: APMIndexDocumentParams) => { - return callEs('index', params); - }, - delete: ( - params: Omit - ): Promise => { - return callEs('delete', params); - }, - indicesCreate: (params: IndicesCreateParams) => { - return callEs('indices.create', params); - }, - hasPrivileges: ( - params: IndexPrivilegesParams - ): Promise => { - return callEs('transport.request', { - method: 'POST', - path: '/_security/user/_has_privileges', - body: params, - }); - }, - }; -} diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts index 5a4bc62b87486..d8dbd8273f476 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts @@ -7,6 +7,8 @@ import { setupRequest } from './setup_request'; import { APMConfig } from '../..'; import { APMRequestHandlerContext } from '../../routes/typings'; import { KibanaRequest } from '../../../../../../src/core/server'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames'; jest.mock('../settings/apm_indices/get_apm_indices', () => ({ getApmIndices: async () => ({ @@ -93,163 +95,175 @@ function getMockRequest() { } describe('setupRequest', () => { - it('should call callWithRequest with default args', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search({ index: 'apm-*', body: { foo: 'bar' } } as any); - expect( - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser - ).toHaveBeenCalledWith('search', { - index: 'apm-*', - body: { - foo: 'bar', - query: { - bool: { - filter: [{ range: { 'observer.version_major': { gte: 7 } } }], - }, - }, - }, - ignore_throttled: true, - }); - }); - - it('should call callWithInternalUser with default args', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { internalClient } = await setupRequest(mockContext, mockRequest); - await internalClient.search({ - index: 'apm-*', - body: { foo: 'bar' }, - } as any); - expect( - mockContext.core.elasticsearch.legacy.client.callAsInternalUser - ).toHaveBeenCalledWith('search', { - index: 'apm-*', - body: { - foo: 'bar', - query: { - bool: { - filter: [{ range: { 'observer.version_major': { gte: 7 } } }], - }, - }, - }, - ignore_throttled: true, - }); - }); - - describe('observer.version_major filter', () => { - describe('if index is apm-*', () => { - it('should merge `observer.version_major` filter with existing boolean filters', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search({ - index: 'apm-*', - body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, - }); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.body).toEqual({ + describe('with default args', () => { + it('calls callWithRequest', async () => { + const { mockContext, mockRequest } = getMockRequest(); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); + await apmEventClient.search({ + apm: { events: [ProcessorEvent.transaction] }, + body: { foo: 'bar' }, + }); + expect( + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser + ).toHaveBeenCalledWith('search', { + index: ['apm-*'], + body: { + foo: 'bar', query: { bool: { filter: [ - { term: 'someTerm' }, + { terms: { 'processor.event': ['transaction'] } }, { range: { 'observer.version_major': { gte: 7 } } }, ], }, }, - }); + }, + ignore_throttled: true, }); + }); - it('should add `observer.version_major` filter if none exists', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search({ index: 'apm-*' }); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.body).toEqual({ - query: { - bool: { - filter: [{ range: { 'observer.version_major': { gte: 7 } } }], - }, - }, - }); + it('calls callWithInternalUser', async () => { + const { mockContext, mockRequest } = getMockRequest(); + const { internalClient } = await setupRequest(mockContext, mockRequest); + await internalClient.search({ + index: ['apm-*'], + body: { foo: 'bar' }, + } as any); + expect( + mockContext.core.elasticsearch.legacy.client.callAsInternalUser + ).toHaveBeenCalledWith('search', { + index: ['apm-*'], + body: { + foo: 'bar', + }, }); + }); + }); - it('should not add `observer.version_major` filter if `includeLegacyData=true`', async () => { - const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search( - { - index: 'apm-*', - body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, + describe('with a bool filter', () => { + it('adds a range filter for `observer.version_major` to the existing filter', async () => { + const { mockContext, mockRequest } = getMockRequest(); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); + await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, + body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, + }); + const params = + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock + .calls[0][1]; + expect(params.body).toEqual({ + query: { + bool: { + filter: [ + { term: 'someTerm' }, + { terms: { [PROCESSOR_EVENT]: ['transaction'] } }, + { range: { 'observer.version_major': { gte: 7 } } }, + ], }, - { - includeLegacyData: true, - } - ); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.body).toEqual({ - query: { bool: { filter: [{ term: 'someTerm' }] } }, - }); + }, }); }); - it('if index is not an APM index, it should not add `observer.version_major` filter', async () => { + it('does not add a range filter for `observer.version_major` if includeLegacyData=true', async () => { const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); - await client.search({ - index: '.ml-*', - body: { - query: { bool: { filter: [{ term: 'someTerm' }] } }, + const { apmEventClient } = await setupRequest(mockContext, mockRequest); + await apmEventClient.search( + { + apm: { + events: [ProcessorEvent.error], + }, + body: { query: { bool: { filter: [{ term: 'someTerm' }] } } }, }, - }); + { + includeLegacyData: true, + } + ); const params = mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock .calls[0][1]; expect(params.body).toEqual({ query: { bool: { - filter: [{ term: 'someTerm' }], + filter: [ + { term: 'someTerm' }, + { + terms: { + [PROCESSOR_EVENT]: ['error'], + }, + }, + ], }, }, }); }); }); +}); - describe('ignore_throttled', () => { - it('should set `ignore_throttled=true` if `includeFrozen=false`', async () => { - const { mockContext, mockRequest } = getMockRequest(); +describe('without a bool filter', () => { + it('adds a range filter for `observer.version_major`', async () => { + const { mockContext, mockRequest } = getMockRequest(); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); + await apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, + }); + const params = + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock + .calls[0][1]; + expect(params.body).toEqual({ + query: { + bool: { + filter: [ + { terms: { [PROCESSOR_EVENT]: ['error'] } }, + { range: { 'observer.version_major': { gte: 7 } } }, + ], + }, + }, + }); + }); +}); - // mock includeFrozen to return false - mockContext.core.uiSettings.client.get.mockResolvedValue(false); +describe('with includeFrozen=false', () => { + it('sets `ignore_throttled=true`', async () => { + const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); + // mock includeFrozen to return false + mockContext.core.uiSettings.client.get.mockResolvedValue(false); - await client.search({}); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.ignore_throttled).toBe(true); + await apmEventClient.search({ + apm: { + events: [], + }, }); - it('should set `ignore_throttled=false` if `includeFrozen=true`', async () => { - const { mockContext, mockRequest } = getMockRequest(); + const params = + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock + .calls[0][1]; + expect(params.ignore_throttled).toBe(true); + }); +}); - // mock includeFrozen to return true - mockContext.core.uiSettings.client.get.mockResolvedValue(true); +describe('with includeFrozen=true', () => { + it('sets `ignore_throttled=false`', async () => { + const { mockContext, mockRequest } = getMockRequest(); - const { client } = await setupRequest(mockContext, mockRequest); + // mock includeFrozen to return true + mockContext.core.uiSettings.client.get.mockResolvedValue(true); - await client.search({}); + const { apmEventClient } = await setupRequest(mockContext, mockRequest); - const params = - mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock - .calls[0][1]; - expect(params.ignore_throttled).toBe(false); + await apmEventClient.search({ + apm: { events: [] }, }); + + const params = + mockContext.core.elasticsearch.legacy.client.callAsCurrentUser.mock + .calls[0][1]; + expect(params.ignore_throttled).toBe(false); }); }); diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index 6f381d4945ab4..ddad2eb2d22dc 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -13,11 +13,17 @@ import { ApmIndicesConfig, } from '../settings/apm_indices/get_apm_indices'; import { ESFilter } from '../../../typings/elasticsearch'; -import { ESClient } from './es_client'; import { getUiFiltersES } from './convert_ui_filters/get_ui_filters_es'; import { APMRequestHandlerContext } from '../../routes/typings'; -import { getESClient } from './es_client'; import { ProcessorEvent } from '../../../common/processor_event'; +import { + APMEventClient, + createApmEventClient, +} from './create_es_client/create_apm_event_client'; +import { + APMInternalClient, + createInternalESClient, +} from './create_es_client/create_internal_es_client'; function decodeUiFilters(uiFiltersEncoded?: string) { if (!uiFiltersEncoded) { @@ -30,8 +36,8 @@ function decodeUiFilters(uiFiltersEncoded?: string) { // https://github.com/microsoft/TypeScript/issues/34933 export interface Setup { - client: ESClient; - internalClient: ESClient; + apmEventClient: APMEventClient; + internalClient: APMInternalClient; ml?: ReturnType; config: APMConfig; indices: ApmIndicesConfig; @@ -78,22 +84,19 @@ export async function setupRequest( context.core.uiSettings.client.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN), ]); - const createClientOptions = { - indices, - includeFrozen, - }; - const uiFiltersES = decodeUiFilters(query.uiFilters); const coreSetupRequest = { indices, - client: getESClient(context, request, { - clientAsInternalUser: false, - ...createClientOptions, + apmEventClient: createApmEventClient({ + context, + request, + indices, + options: { includeFrozen }, }), - internalClient: getESClient(context, request, { - clientAsInternalUser: true, - ...createClientOptions, + internalClient: createInternalESClient({ + context, + request, }), ml: getMlSetup(context, request), config, diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index ee03e77de3580..cb30c6c064848 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -11,7 +11,10 @@ import { IIndexPattern, } from '../../../../../../src/plugins/data/server'; import { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; -import { ProcessorEvent } from '../../../common/processor_event'; +import { + ProcessorEvent, + UIProcessorEvent, +} from '../../../common/processor_event'; import { APMRequestHandlerContext } from '../../routes/typings'; const cache = new LRU({ @@ -27,7 +30,7 @@ export const getDynamicIndexPattern = async ({ }: { context: APMRequestHandlerContext; indices: ApmIndicesConfig; - processorEvent?: ProcessorEvent; + processorEvent?: UIProcessorEvent; }) => { const patternIndices = getPatternIndices(indices, processorEvent); const indexPatternTitle = patternIndices.join(','); @@ -75,17 +78,17 @@ export const getDynamicIndexPattern = async ({ function getPatternIndices( indices: ApmIndicesConfig, - processorEvent?: ProcessorEvent + processorEvent?: UIProcessorEvent ) { const indexNames = processorEvent ? [processorEvent] - : ['transaction' as const, 'metric' as const, 'error' as const]; + : [ProcessorEvent.transaction, ProcessorEvent.metric, ProcessorEvent.error]; const indicesMap = { - transaction: indices['apm_oss.transactionIndices'], - metric: indices['apm_oss.metricsIndices'], - error: indices['apm_oss.errorIndices'], + [ProcessorEvent.transaction]: indices['apm_oss.transactionIndices'], + [ProcessorEvent.metric]: indices['apm_oss.metricsIndices'], + [ProcessorEvent.error]: indices['apm_oss.errorIndices'], }; - return indexNames.map((name) => indicesMap[name]); + return indexNames.map((name) => indicesMap[name as UIProcessorEvent]); } diff --git a/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap index d8119ac96a536..b88c90a213c67 100644 --- a/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`metrics queries with a service node name fetches cpu chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "processCPUAverage": Object { @@ -66,11 +71,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -95,12 +95,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with a service node name fetches heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "heapMemoryCommitted": Object { @@ -155,11 +159,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -189,12 +188,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with a service node name fetches memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "memoryUsedAvg": Object { @@ -251,11 +254,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -290,12 +288,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with a service node name fetches non heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "nonHeapMemoryCommitted": Object { @@ -350,11 +352,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -384,12 +381,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with a service node name fetches thread count chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "threadCount": Object { @@ -434,11 +435,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -468,12 +464,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches cpu chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "processCPUAverage": Object { @@ -538,11 +538,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -573,12 +568,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "heapMemoryCommitted": Object { @@ -633,11 +632,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -673,12 +667,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "memoryUsedAvg": Object { @@ -735,11 +733,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -780,12 +773,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches non heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "nonHeapMemoryCommitted": Object { @@ -840,11 +837,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -880,12 +872,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries with service_node_name_missing fetches thread count chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "threadCount": Object { @@ -930,11 +926,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -970,12 +961,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches cpu chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "processCPUAverage": Object { @@ -1040,11 +1035,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1064,12 +1054,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "heapMemoryCommitted": Object { @@ -1124,11 +1118,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1153,12 +1142,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "memoryUsedAvg": Object { @@ -1215,11 +1208,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1249,12 +1237,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches non heap memory chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "nonHeapMemoryCommitted": Object { @@ -1309,11 +1301,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1338,12 +1325,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`metrics queries without a service node name fetches thread count chart data 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "threadCount": Object { @@ -1388,11 +1379,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -1417,6 +1403,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts index 3ed6e4a944b51..e5c573ba1ec02 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts @@ -18,8 +18,8 @@ import { } from '../../../../helpers/setup_request'; import { getMetricsDateHistogramParams } from '../../../../helpers/metrics'; import { ChartBase } from '../../../types'; -import { getMetricsProjection } from '../../../../../../common/projections/metrics'; -import { mergeProjection } from '../../../../../../common/projections/util/merge_projection'; +import { getMetricsProjection } from '../../../../../projections/metrics'; +import { mergeProjection } from '../../../../../projections/util/merge_projection'; import { AGENT_NAME, LABEL_NAME, @@ -42,7 +42,7 @@ export async function fetchAndTransformGcMetrics({ chartBase: ChartBase; fieldName: typeof METRIC_JAVA_GC_COUNT | typeof METRIC_JAVA_GC_TIME; }) { - const { start, end, client } = setup; + const { start, end, apmEventClient } = setup; const { bucketSize } = getBucketSize(start, end, 'auto'); @@ -105,7 +105,7 @@ export async function fetchAndTransformGcMetrics({ }, }); - const response = await client.search(params); + const response = await apmEventClient.search(params); const { aggregations } = response; diff --git a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts index 895920a9b6c7d..f6e201b395c37 100644 --- a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts @@ -5,7 +5,6 @@ */ import { Unionize, Overwrite } from 'utility-types'; -import { ESSearchRequest } from '../../../typings/elasticsearch'; import { Setup, SetupTimeRange, @@ -14,9 +13,10 @@ import { import { getMetricsDateHistogramParams } from '../helpers/metrics'; import { ChartBase } from './types'; import { transformDataToMetricsChart } from './transform_metrics_chart'; -import { getMetricsProjection } from '../../../common/projections/metrics'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getMetricsProjection } from '../../projections/metrics'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { AggregationOptionsByType } from '../../../typings/elasticsearch/aggregations'; +import { APMEventESSearchRequest } from '../helpers/create_es_client/create_apm_event_client'; type MetricsAggregationMap = Unionize<{ min: AggregationOptionsByType['min']; @@ -28,7 +28,7 @@ type MetricsAggregationMap = Unionize<{ type MetricAggs = Record; export type GenericMetricsRequest = Overwrite< - ESSearchRequest, + APMEventESSearchRequest, { body: { aggs: { @@ -65,7 +65,7 @@ export async function fetchAndTransformMetrics({ aggs: T; additionalFilters?: Filter[]; }) { - const { start, end, client } = setup; + const { start, end, apmEventClient } = setup; const projection = getMetricsProjection({ setup, @@ -91,7 +91,7 @@ export async function fetchAndTransformMetrics({ }, }); - const response = await client.search(params); + const response = await apmEventClient.search(params); return transformDataToMetricsChart(response, chartBase); } diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts index 4c4d058c7139d..8a1f3cb0e0149 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts @@ -6,10 +6,7 @@ import { ProcessorEvent } from '../../../common/processor_event'; import { rangeFilter } from '../../../common/utils/range_filter'; -import { - SERVICE_NAME, - PROCESSOR_EVENT, -} from '../../../common/elasticsearch_fieldnames'; +import { SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; export async function getServiceCount({ @@ -17,36 +14,27 @@ export async function getServiceCount({ }: { setup: Setup & SetupTimeRange; }) { - const { client, indices, start, end } = setup; + const { apmEventClient, start, end } = setup; const params = { - index: [ - indices['apm_oss.transactionIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.metricsIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, body: { size: 0, query: { bool: { - filter: [ - { range: rangeFilter(start, end) }, - { - terms: { - [PROCESSOR_EVENT]: [ - ProcessorEvent.error, - ProcessorEvent.transaction, - ProcessorEvent.metric, - ], - }, - }, - ], + filter: [{ range: rangeFilter(start, end) }], }, }, aggs: { serviceCount: { cardinality: { field: SERVICE_NAME } } }, }, }; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); return aggregations?.serviceCount.value || 0; } diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts index 0d1a4274c16dc..116b37a395299 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts @@ -10,7 +10,6 @@ */ import { rangeFilter } from '../../../common/utils/range_filter'; import { Coordinates } from '../../../../observability/public'; -import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { ProcessorEvent } from '../../../common/processor_event'; @@ -21,18 +20,17 @@ export async function getTransactionCoordinates({ setup: Setup & SetupTimeRange; bucketSize: string; }): Promise { - const { client, indices, start, end } = setup; + const { apmEventClient, start, end } = setup; - const { aggregations } = await client.search({ - index: indices['apm_oss.transactionIndices'], + const { aggregations } = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { - filter: [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - { range: rangeFilter(start, end) }, - ], + filter: [{ range: rangeFilter(start, end) }], }, }, aggs: { diff --git a/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts b/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts index fc7445ab4a225..66d82b9f88355 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts @@ -3,41 +3,27 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; import { Setup } from '../helpers/setup_request'; export async function hasData({ setup }: { setup: Setup }) { - const { client, indices } = setup; + const { apmEventClient } = setup; try { const params = { - index: [ - indices['apm_oss.transactionIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.metricsIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, terminateAfter: 1, body: { size: 0, - query: { - bool: { - filter: [ - { - terms: { - [PROCESSOR_EVENT]: [ - ProcessorEvent.error, - ProcessorEvent.metric, - ProcessorEvent.transaction, - ], - }, - }, - ], - }, - }, }, }; - const response = await client.search(params); + const response = await apmEventClient.search(params); return response.hits.total.value > 0; } catch (e) { return false; diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap index 602eb88ba8940..c5264373ea495 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`rum client dashboard queries fetches client metrics 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "backEnd": Object { @@ -34,11 +39,6 @@ Object { }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "page-load", @@ -59,12 +59,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`rum client dashboard queries fetches page load distribution 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "durPercentiles": Object { @@ -101,11 +105,6 @@ Object { }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "page-load", @@ -126,12 +125,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`rum client dashboard queries fetches page view trends 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "pageViews": Object { @@ -154,11 +157,6 @@ Object { }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "page-load", @@ -179,12 +177,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`rum client dashboard queries fetches rum services 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -206,11 +208,6 @@ Object { }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "page-load", @@ -231,6 +228,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts index 8b3f733fc402a..194c136e2b3d0 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -45,9 +45,9 @@ export async function getClientMetrics({ }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); const { backEnd, domInteractive, pageViews } = response.aggregations!; // Divide by 1000 to convert ms into seconds diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts index e847a87264759..2a0c709ea9235 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -57,12 +57,12 @@ export async function getPageLoadDistribution({ }, }); - const { client } = setup; + const { apmEventClient } = setup; const { aggregations, hits: { total }, - } = await client.search(params); + } = await apmEventClient.search(params); if (total.value === 0) { return null; @@ -130,9 +130,9 @@ const getPercentilesDistribution = async ( }, }); - const { client } = setup; + const { apmEventClient } = setup; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const pageDist = aggregations?.loadDistribution.values ?? []; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts index 30b2677d3c217..23169ddaca534 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -56,9 +56,9 @@ export async function getPageViewTrends({ }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); const result = response.aggregations?.pageViews.buckets ?? []; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts index ea9d701e64c3d..ffb06e649b9be 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -16,6 +17,7 @@ import { USER_AGENT_DEVICE, USER_AGENT_NAME, USER_AGENT_OS, + TRANSACTION_DURATION, } from '../../../common/elasticsearch_fieldnames'; import { MICRO_TO_SEC, microToSec } from './get_page_load_distribution'; @@ -53,11 +55,11 @@ export const getPageLoadDistBreakdown = async ( }); const params = mergeProjection(projection, { + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, - query: { - bool: projection.body.query.bool, - }, aggs: { breakdowns: { terms: { @@ -67,7 +69,7 @@ export const getPageLoadDistBreakdown = async ( aggs: { page_dist: { percentile_ranks: { - field: 'transaction.duration.us', + field: TRANSACTION_DURATION, values: stepValues, keyed: false, hdr: { @@ -81,9 +83,9 @@ export const getPageLoadDistBreakdown = async ( }, }); - const { client } = setup; + const { apmEventClient } = setup; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const pageDistBreakdowns = aggregations?.breakdowns.buckets; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts b/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts index 5957a25239307..9bfa109f00faf 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; import { Setup, SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; export async function getRumServices({ setup, @@ -38,9 +38,9 @@ export async function getRumServices({ }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); const result = response.aggregations?.services.buckets ?? []; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts index a14affb6eeec5..3681923b484b0 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange, @@ -55,9 +55,9 @@ export async function getVisitorBreakdown({ }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); const { browsers, os, devices } = response.aggregations!; return { diff --git a/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts b/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts index 08c8aba5f0207..14047f4bacea9 100644 --- a/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts @@ -3,10 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { - PROCESSOR_EVENT, - TRACE_ID, -} from '../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { TRACE_ID } from '../../../common/elasticsearch_fieldnames'; import { ConnectionNode, ExternalConnectionNode, @@ -18,23 +16,17 @@ export async function fetchServicePathsFromTraceIds( setup: Setup, traceIds: string[] ) { - const { indices, client } = setup; + const { apmEventClient } = setup; const serviceMapParams = { - index: [ - indices['apm_oss.spanIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ProcessorEvent.span, ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter: [ - { - terms: { - [PROCESSOR_EVENT]: ['span', 'transaction'], - }, - }, { terms: { [TRACE_ID]: traceIds, @@ -212,7 +204,7 @@ export async function fetchServicePathsFromTraceIds( }, }; - const serviceMapFromTraceIdsScriptResponse = await client.search( + const serviceMapFromTraceIdsScriptResponse = await apmEventClient.search( serviceMapParams ); diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index cd125f944f8a5..b162c3b61d928 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -10,8 +10,8 @@ import { SERVICE_ENVIRONMENT, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; -import { getServicesProjection } from '../../../common/projections/services'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getServicesProjection } from '../../projections/services'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { PromiseReturnType } from '../../../typings/common'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { transformServiceMapResponses } from './transform_service_map_responses'; @@ -118,9 +118,9 @@ async function getServicesData(options: IEnvOptions) { }, }); - const { client } = setup; + const { apmEventClient } = setup; - const response = await client.search(params); + const response = await apmEventClient.search(params); return ( response.aggregations?.services.buckets.map((bucket) => { diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts index 1e0d001340edf..d1c99d778c8f0 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts @@ -12,7 +12,7 @@ describe('getServiceMapServiceNodeInfo', () => { describe('with no results', () => { it('returns null data', async () => { const setup = ({ - client: { + apmEventClient: { search: () => Promise.resolve({ hits: { total: { value: 0 } }, @@ -49,7 +49,7 @@ describe('getServiceMapServiceNodeInfo', () => { }); const setup = ({ - client: { + apmEventClient: { search: () => Promise.resolve({ hits: { total: { value: 1 } }, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index 0f7136d6d74a4..330d38739a063 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -6,13 +6,12 @@ import { UIFilters } from '../../../typings/ui_filters'; import { + SERVICE_NAME, + TRANSACTION_DURATION, TRANSACTION_TYPE, METRIC_SYSTEM_CPU_PERCENT, METRIC_SYSTEM_FREE_MEMORY, METRIC_SYSTEM_TOTAL_MEMORY, - PROCESSOR_EVENT, - SERVICE_NAME, - TRANSACTION_DURATION, } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; import { rangeFilter } from '../../../common/utils/range_filter'; @@ -109,17 +108,18 @@ async function getTransactionStats({ avgTransactionDuration: number | null; avgRequestsPerMinute: number | null; }> { - const { indices, client } = setup; + const { apmEventClient } = setup; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter: [ ...filter, - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { terms: { [TRANSACTION_TYPE]: [ @@ -135,8 +135,9 @@ async function getTransactionStats({ aggs: { duration: { avg: { field: TRANSACTION_DURATION } } }, }, }; - const response = await client.search(params); + const response = await apmEventClient.search(params); const docCount = response.hits.total.value; + return { avgTransactionDuration: response.aggregations?.duration.value ?? null, avgRequestsPerMinute: docCount > 0 ? docCount / minutes : null, @@ -147,18 +148,17 @@ async function getCpuStats({ setup, filter, }: TaskParameters): Promise<{ avgCpuUsage: number | null }> { - const { indices, client } = setup; + const { apmEventClient } = setup; - const response = await client.search({ - index: indices['apm_oss.metricsIndices'], + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.metric], + }, body: { size: 0, query: { bool: { - filter: filter.concat([ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.metric } }, - { exists: { field: METRIC_SYSTEM_CPU_PERCENT } }, - ]), + filter: [...filter, { exists: { field: METRIC_SYSTEM_CPU_PERCENT } }], }, }, aggs: { avgCpuUsage: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } } }, @@ -172,17 +172,19 @@ async function getMemoryStats({ setup, filter, }: TaskParameters): Promise<{ avgMemoryUsage: number | null }> { - const { client, indices } = setup; - const response = await client.search({ - index: indices['apm_oss.metricsIndices'], + const { apmEventClient } = setup; + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.metric], + }, body: { query: { bool: { - filter: filter.concat([ - { term: { [PROCESSOR_EVENT]: 'metric' } }, + filter: [ + ...filter, { exists: { field: METRIC_SYSTEM_FREE_MEMORY } }, { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, - ]), + ], }, }, aggs: { avgMemoryUsage: { avg: { script: percentMemoryUsedScript } } }, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts index 11c3a00f32980..d6d681f24ab85 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { uniq, take, sortBy } from 'lodash'; +import { ProcessorEvent } from '../../../common/processor_event'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { rangeFilter } from '../../../common/utils/range_filter'; import { ESFilter } from '../../../typings/elasticsearch'; import { - PROCESSOR_EVENT, SERVICE_NAME, SERVICE_ENVIRONMENT, TRACE_ID, @@ -26,18 +26,13 @@ export async function getTraceSampleIds({ environment?: string; setup: Setup & SetupTimeRange; }) { - const { start, end, client, indices, config } = setup; + const { start, end, apmEventClient, config } = setup; const rangeQuery = { range: rangeFilter(start, end) }; const query = { bool: { filter: [ - { - term: { - [PROCESSOR_EVENT]: 'span', - }, - }, { exists: { field: SPAN_DESTINATION_SERVICE_RESOURCE, @@ -67,7 +62,9 @@ export async function getTraceSampleIds({ const samplerShardSize = traceIdBucketSize * 10; const params = { - index: [indices['apm_oss.spanIndices']], + apm: { + events: [ProcessorEvent.span], + }, body: { size: 0, query, @@ -126,9 +123,7 @@ export async function getTraceSampleIds({ }, }; - const tracesSampleResponse = await client.search( - params - ); + const tracesSampleResponse = await apmEventClient.search(params); // make sure at least one trace per composite/connection bucket // is queried diff --git a/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap index 3935ecda42db9..87aca0d056909 100644 --- a/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`service node queries fetches metadata for a service node 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "containerId": Object { @@ -30,11 +35,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -59,12 +59,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`service node queries fetches metadata for unidentified service nodes 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "containerId": Object { @@ -93,11 +97,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -128,12 +127,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`service node queries fetches services nodes 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "nodes": Object { @@ -174,11 +177,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -197,6 +195,5 @@ Object { }, }, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/service_nodes/index.ts b/x-pack/plugins/apm/server/lib/service_nodes/index.ts index de66c242815a4..a83aba192dba9 100644 --- a/x-pack/plugins/apm/server/lib/service_nodes/index.ts +++ b/x-pack/plugins/apm/server/lib/service_nodes/index.ts @@ -9,8 +9,8 @@ import { SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; -import { getServiceNodesProjection } from '../../../common/projections/service_nodes'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getServiceNodesProjection } from '../../projections/service_nodes'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes'; import { METRIC_PROCESS_CPU_PERCENT, @@ -26,7 +26,7 @@ const getServiceNodes = async ({ setup: Setup & SetupTimeRange & SetupUIFilters; serviceName: string; }) => { - const { client } = setup; + const { apmEventClient } = setup; const projection = getServiceNodesProjection({ setup, serviceName }); @@ -66,7 +66,7 @@ const getServiceNodes = async ({ }, }); - const response = await client.search(params); + const response = await apmEventClient.search(params); if (!response.aggregations) { return []; diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index 0fc1f89a3723b..ca86c1d93fa6e 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -2,48 +2,32 @@ exports[`services queries fetches the agent status 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + "metric", + "sourcemap", + "transaction", + ], + }, "body": Object { - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "error", - "metric", - "sourcemap", - "transaction", - ], - }, - }, - ], - }, - }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - "myIndex", - ], "terminateAfter": 1, } `; exports[`services queries fetches the legacy data status 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - ], - }, - }, Object { "range": Object { "observer.version_major": Object { @@ -56,13 +40,19 @@ Object { }, "size": 0, }, - "index": "myIndex", "terminateAfter": 1, } `; exports[`services queries fetches the service agent name 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + "transaction", + "metric", + ], + }, "body": Object { "aggs": Object { "agents": Object { @@ -80,15 +70,6 @@ Object { "service.name": "foo", }, }, - Object { - "terms": Object { - "processor.event": Array [ - "error", - "transaction", - "metric", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -103,11 +84,6 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], "terminateAfter": 1, } `; @@ -115,6 +91,11 @@ Object { exports[`services queries fetches the service items 1`] = ` Array [ Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -148,20 +129,20 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, ], }, }, "size": 0, }, - "index": "myIndex", - "size": 0, }, Object { + "apm": Object { + "events": Array [ + "metric", + "error", + "transaction", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -198,27 +179,18 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "terms": Object { - "processor.event": Array [ - "metric", - "error", - "transaction", - ], - }, - }, ], }, }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -245,19 +217,18 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, ], }, }, "size": 0, }, - "index": "myIndex", }, Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -284,19 +255,20 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, ], }, }, "size": 0, }, - "index": "myIndex", }, Object { + "apm": Object { + "events": Array [ + "metric", + "transaction", + "error", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -330,31 +302,22 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, ], }, }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], }, ] `; exports[`services queries fetches the service transaction types 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "types": Object { @@ -372,13 +335,6 @@ Object { "service.name": "foo", }, }, - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -393,6 +349,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts index 6a8aaf8dca8a6..ad3f47d443b87 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import { isNumber } from 'lodash'; +import { ProcessorEvent } from '../../../../common/processor_event'; import { Annotation, AnnotationType } from '../../../../common/annotations'; import { SetupTimeRange, Setup } from '../../helpers/setup_request'; import { ESFilter } from '../../../../typings/elasticsearch'; import { rangeFilter } from '../../../../common/utils/range_filter'; import { - PROCESSOR_EVENT, SERVICE_NAME, SERVICE_VERSION, } from '../../../../common/elasticsearch_fieldnames'; @@ -24,23 +24,24 @@ export async function getDerivedServiceAnnotations({ environment?: string; setup: Setup & SetupTimeRange; }) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; const filter: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [SERVICE_NAME]: serviceName } }, ...getEnvironmentUiFilterES(environment), ]; const versions = ( - await client.search({ - index: indices['apm_oss.transactionIndices'], + await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { - filter: filter.concat({ range: rangeFilter(start, end) }), + filter: [...filter, { range: rangeFilter(start, end) }], }, }, aggs: { @@ -59,17 +60,15 @@ export async function getDerivedServiceAnnotations({ } const annotations = await Promise.all( versions.map(async (version) => { - const response = await client.search({ - index: indices['apm_oss.transactionIndices'], + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { - filter: filter.concat({ - term: { - [SERVICE_VERSION]: version, - }, - }), + filter: [...filter, { term: { [SERVICE_VERSION]: version } }], }, }, aggs: { diff --git a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts b/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts index 8d75d746c7fca..a95c27df0e502 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { - PROCESSOR_EVENT, AGENT_NAME, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; @@ -15,24 +15,23 @@ export async function getServiceAgentName( serviceName: string, setup: Setup & SetupTimeRange ) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; const params = { terminateAfter: 1, - index: [ - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - indices['apm_oss.metricsIndices'], - ], + apm: { + events: [ + ProcessorEvent.error, + ProcessorEvent.transaction, + ProcessorEvent.metric, + ], + }, body: { size: 0, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { - terms: { [PROCESSOR_EVENT]: ['error', 'transaction', 'metric'] }, - }, { range: rangeFilter(start, end) }, ], }, @@ -45,7 +44,7 @@ export async function getServiceAgentName( }, }; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const agentName = aggregations?.agents.buckets[0]?.key as string | undefined; return { agentName }; } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts b/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts index c2d9fa6c1df39..fca472b0ce8c2 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts @@ -14,8 +14,8 @@ import { CONTAINER_ID, } from '../../../common/elasticsearch_fieldnames'; import { NOT_AVAILABLE_LABEL } from '../../../common/i18n'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; -import { getServiceNodesProjection } from '../../../common/projections/service_nodes'; +import { mergeProjection } from '../../projections/util/merge_projection'; +import { getServiceNodesProjection } from '../../projections/service_nodes'; export async function getServiceNodeMetadata({ serviceName, @@ -26,7 +26,7 @@ export async function getServiceNodeMetadata({ serviceNodeName: string; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { client } = setup; + const { apmEventClient } = setup; const query = mergeProjection( getServiceNodesProjection({ @@ -55,7 +55,7 @@ export async function getServiceNodeMetadata({ } ); - const response = await client.search(query); + const response = await apmEventClient.search(query); return { host: response.aggregations?.host.buckets[0]?.key || NOT_AVAILABLE_LABEL, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts index d88be4055dc21..6c6e03ab0b46f 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; @@ -15,17 +15,18 @@ export async function getServiceTransactionTypes( serviceName: string, setup: Setup & SetupTimeRange ) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { terms: { [PROCESSOR_EVENT]: ['transaction'] } }, { range: rangeFilter(start, end) }, ], }, @@ -38,7 +39,7 @@ export async function getServiceTransactionTypes( }, }; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const transactionTypes = aggregations?.types.buckets.map((bucket) => bucket.key as string) || []; return { transactionTypes }; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts index dde726c51393f..1be95967cb47a 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts @@ -4,33 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - OBSERVER_VERSION_MAJOR, - PROCESSOR_EVENT, -} from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { OBSERVER_VERSION_MAJOR } from '../../../../common/elasticsearch_fieldnames'; import { Setup } from '../../helpers/setup_request'; // returns true if 6.x data is found export async function getLegacyDataStatus(setup: Setup) { - const { client, indices } = setup; + const { apmEventClient } = setup; const params = { terminateAfter: 1, - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { - filter: [ - { terms: { [PROCESSOR_EVENT]: ['transaction'] } }, - { range: { [OBSERVER_VERSION_MAJOR]: { lt: 7 } } }, - ], + filter: [{ range: { [OBSERVER_VERSION_MAJOR]: { lt: 7 } } }], }, }, }, }; - const resp = await client.search(params, { includeLegacyData: true }); + const resp = await apmEventClient.search(params, { includeLegacyData: true }); const hasLegacyData = resp.hits.total.value > 0; return hasLegacyData; } diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index 14772e77fe1c2..d888b43b63fac 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -10,7 +10,7 @@ import { SetupTimeRange, SetupUIFilters, } from '../../helpers/setup_request'; -import { getServicesProjection } from '../../../../common/projections/services'; +import { getServicesProjection } from '../../../projections/services'; import { getTransactionDurationAverages, getAgentNames, @@ -25,7 +25,7 @@ export type ServicesItemsProjection = ReturnType; export async function getServicesItems(setup: ServicesItemsSetup) { const params = { - projection: getServicesProjection({ setup, noEvents: true }), + projection: getServicesProjection({ setup }), setup, }; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts index de699028f5675..ddce3b667a603 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts @@ -5,12 +5,11 @@ */ import { - PROCESSOR_EVENT, TRANSACTION_DURATION, AGENT_NAME, SERVICE_ENVIRONMENT, } from '../../../../common/elasticsearch_fieldnames'; -import { mergeProjection } from '../../../../common/projections/util/merge_projection'; +import { mergeProjection } from '../../../projections/util/merge_projection'; import { ProcessorEvent } from '../../../../common/processor_event'; import { ServicesItemsSetup, @@ -31,22 +30,15 @@ export const getTransactionDurationAverages = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; + const { apmEventClient } = setup; - const response = await client.search( + const response = await apmEventClient.search( mergeProjection(projection, { - size: 0, - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { - query: { - bool: { - filter: projection.body.query.bool.filter.concat({ - term: { - [PROCESSOR_EVENT]: ProcessorEvent.transaction, - }, - }), - }, - }, + size: 0, aggs: { services: { terms: { @@ -82,32 +74,18 @@ export const getAgentNames = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; - const response = await client.search( + const { apmEventClient } = setup; + const response = await apmEventClient.search( mergeProjection(projection, { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.metric, + ProcessorEvent.error, + ProcessorEvent.transaction, + ], + }, body: { size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - { - terms: { - [PROCESSOR_EVENT]: [ - ProcessorEvent.metric, - ProcessorEvent.error, - ProcessorEvent.transaction, - ], - }, - }, - ], - }, - }, aggs: { services: { terms: { @@ -136,11 +114,7 @@ export const getAgentNames = async ({ return aggregations.services.buckets.map((bucket) => ({ serviceName: bucket.key as string, - agentName: (bucket.agent_name.hits.hits[0]?._source as { - agent: { - name: string; - }; - }).agent.name, + agentName: bucket.agent_name.hits.hits[0]?._source.agent.name, })); }; @@ -148,24 +122,14 @@ export const getTransactionRates = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; - const response = await client.search( + const { apmEventClient } = setup; + const response = await apmEventClient.search( mergeProjection(projection, { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - { - term: { - [PROCESSOR_EVENT]: ProcessorEvent.transaction, - }, - }, - ], - }, - }, aggs: { services: { terms: { @@ -199,24 +163,14 @@ export const getErrorRates = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; - const response = await client.search( + const { apmEventClient } = setup; + const response = await apmEventClient.search( mergeProjection(projection, { - index: indices['apm_oss.errorIndices'], + apm: { + events: [ProcessorEvent.error], + }, body: { size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - { - term: { - [PROCESSOR_EVENT]: ProcessorEvent.error, - }, - }, - ], - }, - }, aggs: { services: { terms: { @@ -250,32 +204,18 @@ export const getEnvironments = async ({ setup, projection, }: AggregationParams) => { - const { client, indices } = setup; - const response = await client.search( + const { apmEventClient } = setup; + const response = await apmEventClient.search( mergeProjection(projection, { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.metric, + ProcessorEvent.transaction, + ProcessorEvent.error, + ], + }, body: { size: 0, - query: { - bool: { - filter: [ - ...projection.body.query.bool.filter, - { - terms: { - [PROCESSOR_EVENT]: [ - ProcessorEvent.transaction, - ProcessorEvent.error, - ProcessorEvent.metric, - ], - }, - }, - ], - }, - }, aggs: { services: { terms: { diff --git a/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts b/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts index 42f53fc93fa60..eed9f2588152d 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/has_historical_agent_data.ts @@ -4,43 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PROCESSOR_EVENT } from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup } from '../../helpers/setup_request'; // Note: this logic is duplicated in tutorials/apm/envs/on_prem export async function hasHistoricalAgentData(setup: Setup) { - const { client, indices } = setup; + const { apmEventClient } = setup; const params = { terminateAfter: 1, - index: [ - indices['apm_oss.errorIndices'], - indices['apm_oss.metricsIndices'], - indices['apm_oss.sourcemapIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.error, + ProcessorEvent.metric, + ProcessorEvent.sourcemap, + ProcessorEvent.transaction, + ], + }, body: { size: 0, - query: { - bool: { - filter: [ - { - terms: { - [PROCESSOR_EVENT]: [ - 'error', - 'metric', - 'sourcemap', - 'transaction', - ], - }, - }, - ], - }, - }, }, }; - const resp = await client.search(params); - const hasHistorialAgentData = resp.hits.total.value > 0; - return hasHistorialAgentData; + const resp = await apmEventClient.search(params); + return resp.hits.total.value > 0; } diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap index 24a1840bc0ab8..2b465a0f87475 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap @@ -115,6 +115,13 @@ Object { exports[`agent configuration queries getServiceNames fetches service names 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "error", + "metric", + ], + }, "body": Object { "aggs": Object { "services": Object { @@ -124,28 +131,8 @@ Object { }, }, }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, - ], - }, - }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts index 4d61e1e9ae284..86aeb95e165a0 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts @@ -10,7 +10,7 @@ import { AgentConfiguration, AgentConfigurationIntake, } from '../../../../common/agent_configuration/configuration_types'; -import { APMIndexDocumentParams } from '../../helpers/es_client'; +import { APMIndexDocumentParams } from '../../helpers/create_es_client/create_internal_es_client'; export async function createOrUpdateConfiguration({ configurationId, diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts index 39674ee57abf6..9f0e65d492a8f 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup } from '../../helpers/setup_request'; -import { - PROCESSOR_EVENT, - SERVICE_NAME, -} from '../../../../common/elasticsearch_fieldnames'; +import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; import { AGENT_NAME } from '../../../../common/elasticsearch_fieldnames'; export async function getAgentNameByService({ @@ -18,25 +16,22 @@ export async function getAgentNameByService({ serviceName: string; setup: Setup; }) { - const { client, indices } = setup; + const { apmEventClient } = setup; const params = { terminateAfter: 1, - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, body: { size: 0, query: { bool: { - filter: [ - { - terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] }, - }, - { term: { [SERVICE_NAME]: serviceName } }, - ], + filter: [{ term: { [SERVICE_NAME]: serviceName } }], }, }, aggs: { @@ -47,7 +42,7 @@ export async function getAgentNameByService({ }, }; - const { aggregations } = await client.search(params); + const { aggregations } = await apmEventClient.search(params); const agentName = aggregations?.agent_names.buckets[0]?.key; return agentName as string | undefined; } diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts index 068bb30ddcf79..8b6c1d82beab0 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts @@ -4,37 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup } from '../../helpers/setup_request'; import { PromiseReturnType } from '../../../../../observability/typings/common'; -import { - PROCESSOR_EVENT, - SERVICE_NAME, -} from '../../../../common/elasticsearch_fieldnames'; +import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; import { ALL_OPTION_VALUE } from '../../../../common/agent_configuration/all_option'; export type AgentConfigurationServicesAPIResponse = PromiseReturnType< typeof getServiceNames >; export async function getServiceNames({ setup }: { setup: Setup }) { - const { client, indices } = setup; + const { apmEventClient } = setup; const params = { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, body: { size: 0, - query: { - bool: { - filter: [ - { - terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] }, - }, - ], - }, - }, aggs: { services: { terms: { @@ -46,7 +37,7 @@ export async function getServiceNames({ setup }: { setup: Setup }) { }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const serviceNames = resp.aggregations?.services.buckets .map((bucket) => bucket.key as string) diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/__snapshots__/get_transaction.test.ts.snap b/x-pack/plugins/apm/server/lib/settings/custom_link/__snapshots__/get_transaction.test.ts.snap index a91641b592526..0649c8c38d29a 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/__snapshots__/get_transaction.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/__snapshots__/get_transaction.test.ts.snap @@ -2,15 +2,15 @@ exports[`custom link get transaction fetches with all filter 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "terms": Object { "service.name": Array [ @@ -43,7 +43,6 @@ Object { }, }, }, - "index": "myIndex", "size": 1, "terminateAfter": 1, } @@ -51,20 +50,18 @@ Object { exports[`custom link get transaction fetches without filter 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "query": Object { "bool": Object { - "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, - ], + "filter": Array [], }, }, }, - "index": "myIndex", "size": 1, "terminateAfter": 1, } diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts index 16a694c04c485..48b115619283c 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.ts @@ -8,9 +8,9 @@ import { CustomLink, CustomLinkES, } from '../../../../common/custom_link/custom_link_types'; -import { APMIndexDocumentParams } from '../../helpers/es_client'; import { Setup } from '../../helpers/setup_request'; import { toESFormat } from './helper'; +import { APMIndexDocumentParams } from '../../helpers/create_es_client/create_internal_es_client'; export async function createOrUpdateCustomLink({ customLinkId, diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts index e3becc040580f..9bf489e768a4b 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import * as t from 'io-ts'; -import { PROCESSOR_EVENT } from '../../../../common/elasticsearch_fieldnames'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { Setup } from '../../helpers/setup_request'; import { ProcessorEvent } from '../../../../common/processor_event'; import { filterOptionsRt } from './custom_link_types'; @@ -18,7 +16,7 @@ export async function getTransaction({ setup: Setup; filters?: t.TypeOf; }) { - const { client, indices } = setup; + const { apmEventClient } = setup; const esFilters = Object.entries(filters) // loops through the filters splitting the value by comma and removing white spaces @@ -32,19 +30,18 @@ export async function getTransaction({ const params = { terminateAfter: 1, - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, size: 1, body: { query: { bool: { - filter: [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - ...esFilters, - ], + filter: esFilters, }, }, }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); return resp.hits.hits[0]?._source; } diff --git a/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap index 0a9f9d38b2be7..3c521839b587e 100644 --- a/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap @@ -2,6 +2,11 @@ exports[`trace queries fetches a trace 1`] = ` Object { + "apm": Object { + "events": Array [ + "error", + ], + }, "body": Object { "aggs": Object { "by_transaction_id": Object { @@ -20,11 +25,6 @@ Object { "trace.id": "foo", }, }, - Object { - "term": Object { - "processor.event": "error", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -48,6 +48,5 @@ Object { }, "size": "myIndex", }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts index f9374558dfeeb..17f9743ae9f00 100644 --- a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts +++ b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { - PROCESSOR_EVENT, TRACE_ID, PARENT_ID, TRANSACTION_DURATION, @@ -13,8 +13,6 @@ import { TRANSACTION_ID, ERROR_LOG_LEVEL, } from '../../../common/elasticsearch_fieldnames'; -import { Span } from '../../../typings/es_schemas/ui/span'; -import { Transaction } from '../../../typings/es_schemas/ui/transaction'; import { APMError } from '../../../typings/es_schemas/ui/apm_error'; import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; @@ -28,19 +26,20 @@ export async function getTraceItems( traceId: string, setup: Setup & SetupTimeRange ) { - const { start, end, client, config, indices } = setup; + const { start, end, apmEventClient, config } = setup; const maxTraceItems = config['xpack.apm.ui.maxTraceItems']; const excludedLogLevels = ['debug', 'info', 'warning']; - const errorResponsePromise = client.search({ - index: indices['apm_oss.errorIndices'], + const errorResponsePromise = apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, body: { size: maxTraceItems, query: { bool: { filter: [ { term: { [TRACE_ID]: traceId } }, - { term: { [PROCESSOR_EVENT]: 'error' } }, { range: rangeFilter(start, end) }, ], must_not: { terms: { [ERROR_LOG_LEVEL]: excludedLogLevels } }, @@ -59,18 +58,16 @@ export async function getTraceItems( }, }); - const traceResponsePromise = client.search({ - index: [ - indices['apm_oss.spanIndices'], - indices['apm_oss.transactionIndices'], - ], + const traceResponsePromise = apmEventClient.search({ + apm: { + events: [ProcessorEvent.span, ProcessorEvent.transaction], + }, body: { size: maxTraceItems, query: { bool: { filter: [ { term: { [TRACE_ID]: traceId } }, - { terms: { [PROCESSOR_EVENT]: ['span', 'transaction'] } }, { range: rangeFilter(start, end) }, ], should: { @@ -91,22 +88,17 @@ export async function getTraceItems( // explicit intermediary types to avoid TS "excessively deep" error PromiseValueType, PromiseValueType - // @ts-ignore ] = await Promise.all([errorResponsePromise, traceResponsePromise]); const exceedsMax = traceResponse.hits.total.value > maxTraceItems; - const items = (traceResponse.hits.hits as Array<{ - _source: Transaction | Span; - }>).map((hit) => hit._source); + const items = traceResponse.hits.hits.map((hit) => hit._source); const errorFrequencies: { errorsPerTransaction: ErrorsPerTransaction; errorDocs: APMError[]; } = { - errorDocs: errorResponse.hits.hits.map( - ({ _source }) => _source as APMError - ), + errorDocs: errorResponse.hits.hits.map(({ _source }) => _source), errorsPerTransaction: errorResponse.aggregations?.by_transaction_id.buckets.reduce( (acc, current) => { diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap index deca46f4ebd0c..0ea7bcf7ce8ab 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap @@ -3,6 +3,11 @@ exports[`transaction group queries fetches top traces 1`] = ` Array [ Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -46,11 +51,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "my.custom.ui.filter": "foo-bar", @@ -84,10 +84,14 @@ Array [ }, ], }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -131,11 +135,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "my.custom.ui.filter": "foo-bar", @@ -152,10 +151,14 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -199,11 +202,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "my.custom.ui.filter": "foo-bar", @@ -220,7 +218,6 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, ] @@ -229,6 +226,11 @@ Array [ exports[`transaction group queries fetches top transactions 1`] = ` Array [ Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -257,11 +259,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "bar", @@ -298,10 +295,14 @@ Array [ }, ], }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -330,11 +331,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "bar", @@ -354,10 +350,14 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -386,11 +386,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "bar", @@ -410,10 +405,14 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "transaction_groups": Object { @@ -448,11 +447,6 @@ Array [ }, }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "bar", @@ -472,7 +466,6 @@ Array [ }, }, }, - "index": "myIndex", "size": 0, }, ] diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index 73bf1d01924e7..b06d1a8af3bc5 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -7,13 +7,12 @@ import { take, sortBy } from 'lodash'; import { Unionize } from 'utility-types'; import moment from 'moment'; import { joinByKey } from '../../../common/utils/join_by_key'; -import { ESSearchRequest } from '../../../typings/elasticsearch'; import { SERVICE_NAME, TRANSACTION_NAME, } from '../../../common/elasticsearch_fieldnames'; -import { getTransactionGroupsProjection } from '../../../common/projections/transaction_groups'; -import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { getTransactionGroupsProjection } from '../../projections/transaction_groups'; +import { mergeProjection } from '../../projections/util/merge_projection'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { AggregationOptionsByType } from '../../../typings/elasticsearch/aggregations'; import { Transaction } from '../../../typings/es_schemas/ui/transaction'; @@ -45,7 +44,9 @@ export type Options = TopTransactionOptions | TopTraceOptions; export type ESResponse = PromiseReturnType; -export type TransactionGroupRequestBase = ESSearchRequest & { +export type TransactionGroupRequestBase = ReturnType< + typeof getTransactionGroupsProjection +> & { body: { aggs: { transaction_groups: Unionize< diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index 6a1ee8daad7c7..8fb2ceb30db85 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -5,7 +5,6 @@ */ import { mean } from 'lodash'; import { - PROCESSOR_EVENT, HTTP_RESPONSE_STATUS_CODE, TRANSACTION_NAME, TRANSACTION_TYPE, @@ -31,7 +30,7 @@ export async function getErrorRate({ transactionName?: string; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const transactionNamefilter = transactionName ? [{ term: { [TRANSACTION_NAME]: transactionName } }] @@ -42,7 +41,6 @@ export async function getErrorRate({ const filter = [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { range: rangeFilter(start, end) }, { exists: { field: HTTP_RESPONSE_STATUS_CODE } }, ...transactionNamefilter, @@ -51,7 +49,9 @@ export async function getErrorRate({ ]; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter } }, @@ -68,7 +68,7 @@ export async function getErrorRate({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const noHits = resp.hits.total.value === 0; diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts index 59fb370113ec2..7d45f39e08a83 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts @@ -5,7 +5,6 @@ */ import { merge } from 'lodash'; import { arrayUnionToCallable } from '../../../common/utils/array_union_to_callable'; -import { Transaction } from '../../../typings/es_schemas/ui/transaction'; import { TRANSACTION_SAMPLED, TRANSACTION_DURATION, @@ -52,7 +51,7 @@ export async function getSamples({ request, setup }: MetricParams) { { '@timestamp': { order: 'desc' as const } }, ]; - const response = await setup.client.search({ + const response = await setup.apmEventClient.search({ ...params, body: { ...params.body, @@ -73,7 +72,7 @@ export async function getSamples({ request, setup }: MetricParams) { return { key: bucket.key as BucketKey, count: bucket.doc_count, - sample: bucket.sample.hits.hits[0]._source as Transaction, + sample: bucket.sample.hits.hits[0]._source, }; }); } @@ -87,7 +86,7 @@ export async function getAverages({ request, setup }: MetricParams) { }, }); - const response = await setup.client.search(params); + const response = await setup.apmEventClient.search(params); return arrayUnionToCallable( response.aggregations?.transaction_groups.buckets ?? [] @@ -108,7 +107,7 @@ export async function getSums({ request, setup }: MetricParams) { }, }); - const response = await setup.client.search(params); + const response = await setup.apmEventClient.search(params); return arrayUnionToCallable( response.aggregations?.transaction_groups.buckets ?? [] @@ -131,7 +130,7 @@ export async function getPercentiles({ request, setup }: MetricParams) { }, }); - const response = await setup.client.search(params); + const response = await setup.apmEventClient.search(params); return arrayUnionToCallable( response.aggregations?.transaction_groups.buckets ?? [] diff --git a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap index cc5900919f829..9bc4b1d69d9ac 100644 --- a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap @@ -2,15 +2,15 @@ exports[`transaction queries fetches a transaction 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.id": "foo", @@ -35,12 +35,16 @@ Object { }, "size": 1, }, - "index": "myIndex", } `; exports[`transaction queries fetches breakdown data for transactions 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "by_date": Object { @@ -146,11 +150,6 @@ Object { "transaction.type": "bar", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -170,12 +169,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches breakdown data for transactions for a transaction name 1`] = ` Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, "body": Object { "aggs": Object { "by_date": Object { @@ -281,11 +284,6 @@ Object { "transaction.type": "bar", }, }, - Object { - "term": Object { - "processor.event": "metric", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -310,12 +308,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches transaction charts 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "overall_avg_duration": Object { @@ -376,11 +378,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "service.name": "foo", @@ -405,12 +402,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches transaction charts for a transaction type 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "overall_avg_duration": Object { @@ -471,11 +472,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "service.name": "foo", @@ -505,12 +501,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches transaction charts for a transaction type and transaction name 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "overall_avg_duration": Object { @@ -571,11 +571,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "service.name": "foo", @@ -610,12 +605,16 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; exports[`transaction queries fetches transaction distribution 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "stats": Object { @@ -632,11 +631,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "transaction.type": "baz", @@ -666,6 +660,5 @@ Object { }, "size": 0, }, - "index": "myIndex", } `; diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts index d8175a34ceb9f..278819ea20a83 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts @@ -15,7 +15,7 @@ describe('fetcher', () => { it('performs a search', async () => { const search = jest.fn(); const setup = ({ - client: { search }, + apmEventClient: { search }, indices: {}, uiFiltersES: [], } as unknown) as Setup & SetupTimeRange & SetupUIFilters; diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts index b4d98ec41fc2d..f68082dfaa1e1 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts @@ -7,7 +7,6 @@ import { ESFilter } from '../../../../typings/elasticsearch'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_TYPE, USER_AGENT_NAME, @@ -23,7 +22,7 @@ import { ProcessorEvent } from '../../../../common/processor_event'; export type ESResponse = PromiseReturnType; export function fetcher(options: Options) { - const { end, client, indices, start, uiFiltersES } = options.setup; + const { end, apmEventClient, start, uiFiltersES } = options.setup; const { serviceName, transactionName } = options; const { intervalString } = getBucketSize(start, end, 'auto'); @@ -32,7 +31,6 @@ export function fetcher(options: Options) { : []; const filter: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } }, { range: rangeFilter(start, end) }, @@ -41,7 +39,9 @@ export function fetcher(options: Options) { ]; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter } }, @@ -80,5 +80,5 @@ export function fetcher(options: Options) { }, }; - return client.search(params); + return apmEventClient.search(params); } diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts index ea6213f64ee36..9bb42d2fa7aad 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { CLIENT_GEO_COUNTRY_ISO_CODE, - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_DURATION, TRANSACTION_TYPE, @@ -29,12 +29,14 @@ export async function getTransactionAvgDurationByCountry({ serviceName: string; transactionName?: string; }) { - const { uiFiltersES, client, start, end, indices } = setup; + const { uiFiltersES, apmEventClient, start, end } = setup; const transactionNameFilter = transactionName ? [{ term: { [TRANSACTION_NAME]: transactionName } }] : []; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { @@ -42,7 +44,6 @@ export async function getTransactionAvgDurationByCountry({ filter: [ { term: { [SERVICE_NAME]: serviceName } }, ...transactionNameFilter, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } }, { exists: { field: CLIENT_GEO_COUNTRY_ISO_CODE } }, { range: rangeFilter(start, end) }, @@ -66,7 +67,7 @@ export async function getTransactionAvgDurationByCountry({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); if (!resp.aggregations) { return []; diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts index 85d4eab448c72..3c1618ed7715f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts @@ -26,7 +26,7 @@ function getMockSetup(esResponse: any) { return { start: 0, end: 500000, - client: { search: clientSpy } as any, + apmEventClient: { search: clientSpy } as any, internalClient: { search: clientSpy } as any, config: new Proxy( {}, diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts index 3c48c14c2a471..7248399d1f93f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts @@ -5,6 +5,7 @@ */ import { flatten, orderBy, last } from 'lodash'; +import { ProcessorEvent } from '../../../../common/processor_event'; import { SERVICE_NAME, SPAN_SUBTYPE, @@ -13,7 +14,6 @@ import { TRANSACTION_TYPE, TRANSACTION_NAME, TRANSACTION_BREAKDOWN_COUNT, - PROCESSOR_EVENT, } from '../../../../common/elasticsearch_fieldnames'; import { Setup, @@ -36,7 +36,7 @@ export async function getTransactionBreakdown({ transactionName?: string; transactionType: string; }) { - const { uiFiltersES, client, start, end, indices } = setup; + const { uiFiltersES, apmEventClient, start, end } = setup; const subAggs = { sum_all_self_times: { @@ -82,7 +82,6 @@ export async function getTransactionBreakdown({ const filters = [ { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: transactionType } }, - { term: { [PROCESSOR_EVENT]: 'metric' } }, { range: rangeFilter(start, end) }, ...uiFiltersES, ]; @@ -92,7 +91,9 @@ export async function getTransactionBreakdown({ } const params = { - index: indices['apm_oss.metricsIndices'], + apm: { + events: [ProcessorEvent.metric], + }, body: { size: 0, query: { @@ -110,7 +111,7 @@ export async function getTransactionBreakdown({ }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const formatBucket = ( aggs: diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap index 25ebb15fd73e8..7bc60a7fc7f1a 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap @@ -4,6 +4,11 @@ exports[`timeseriesFetcher should call client with correct query 1`] = ` Array [ Array [ Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, "body": Object { "aggs": Object { "overall_avg_duration": Object { @@ -64,11 +69,6 @@ Array [ "query": Object { "bool": Object { "filter": Array [ - Object { - "term": Object { - "processor.event": "transaction", - }, - }, Object { "term": Object { "service.name": "myServiceName", @@ -98,7 +98,6 @@ Array [ }, "size": 0, }, - "index": "myIndex", }, ], ] diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts index fb357040f5781..09e1287f032f5 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PROCESSOR_EVENT } from '../../../../../common/elasticsearch_fieldnames'; import { ESResponse, timeseriesFetcher } from './fetcher'; import { APMConfig } from '../../../../../server'; +import { ProcessorEvent } from '../../../../../common/processor_event'; describe('timeseriesFetcher', () => { let res: ESResponse; @@ -21,7 +21,7 @@ describe('timeseriesFetcher', () => { setup: { start: 1528113600000, end: 1528977600000, - client: { search: clientSpy } as any, + apmEventClient: { search: clientSpy } as any, internalClient: { search: clientSpy } as any, config: new Proxy( {}, @@ -54,15 +54,7 @@ describe('timeseriesFetcher', () => { it('should restrict results to only transaction documents', () => { const query = clientSpy.mock.calls[0][0]; - expect(query.body.query.bool.filter).toEqual( - expect.arrayContaining([ - { - term: { - [PROCESSOR_EVENT]: 'transaction', - }, - } as any, - ]) - ); + expect(query.apm.events).toEqual([ProcessorEvent.transaction]); }); it('should return correct response', () => { diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts index 8e19af926ce02..1498c22e327d6 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../../common/processor_event'; import { ESFilter } from '../../../../../typings/elasticsearch'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_DURATION, TRANSACTION_NAME, @@ -34,11 +34,10 @@ export function timeseriesFetcher({ transactionName: string | undefined; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const { intervalString } = getBucketSize(start, end, 'auto'); const filter: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [SERVICE_NAME]: serviceName } }, { range: rangeFilter(start, end) }, ...uiFiltersES, @@ -54,7 +53,9 @@ export function timeseriesFetcher({ } const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, body: { size: 0, query: { bool: { filter } }, @@ -95,5 +96,5 @@ export function timeseriesFetcher({ }, }; - return client.search(params); + return apmEventClient.search(params); } diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts index 3f8bf635712be..bfe72bf7c00f9 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { ProcessorEvent } from '../../../../../common/processor_event'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRACE_ID, TRANSACTION_DURATION, @@ -32,17 +31,18 @@ export async function bucketFetcher( bucketSize: number, setup: Setup & SetupTimeRange & SetupUIFilters ) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, body: { size: 0, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [TRANSACTION_TYPE]: transactionType } }, { term: { [TRANSACTION_NAME]: transactionName } }, { range: rangeFilter(start, end) }, @@ -85,7 +85,7 @@ export async function bucketFetcher( }, }; - const response = await client.search(params); + const response = await apmEventClient.search(params); return response; } diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts index 8289113fddae9..139dac3df1171 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../../common/processor_event'; import { - PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_DURATION, TRANSACTION_NAME, @@ -23,17 +23,18 @@ export async function getDistributionMax( transactionType: string, setup: Setup & SetupTimeRange & SetupUIFilters ) { - const { start, end, uiFiltersES, client, indices } = setup; + const { start, end, uiFiltersES, apmEventClient } = setup; const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 0, query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [TRANSACTION_TYPE]: transactionType } }, { term: { [TRANSACTION_NAME]: transactionName } }, { @@ -59,6 +60,6 @@ export async function getDistributionMax( }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); return resp.aggregations ? resp.aggregations.stats.max : null; } diff --git a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts index a7de93a3bf650..9aa1a8f4de87f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts @@ -5,11 +5,9 @@ */ import { - PROCESSOR_EVENT, TRACE_ID, TRANSACTION_ID, } from '../../../../common/elasticsearch_fieldnames'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { rangeFilter } from '../../../../common/utils/range_filter'; import { Setup, @@ -27,16 +25,17 @@ export async function getTransaction({ traceId: string; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; - const params = { - index: indices['apm_oss.transactionIndices'], + const resp = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, body: { size: 1, query: { bool: { filter: [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { term: { [TRANSACTION_ID]: transactionId } }, { term: { [TRACE_ID]: traceId } }, { range: rangeFilter(start, end) }, @@ -44,8 +43,7 @@ export async function getTransaction({ }, }, }, - }; + }); - const resp = await client.search(params); return resp.hits.hits[0]?._source; } diff --git a/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts index ad4f58d85d188..8ba61c6c726a4 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts @@ -5,11 +5,9 @@ */ import { - PROCESSOR_EVENT, TRACE_ID, PARENT_ID, } from '../../../../common/elasticsearch_fieldnames'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { Setup } from '../../helpers/setup_request'; import { ProcessorEvent } from '../../../../common/processor_event'; @@ -17,9 +15,12 @@ export async function getRootTransactionByTraceId( traceId: string, setup: Setup ) { - const { client, indices } = setup; + const { apmEventClient } = setup; + const params = { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, body: { size: 1, query: { @@ -35,16 +36,13 @@ export async function getRootTransactionByTraceId( }, }, ], - filter: [ - { term: { [TRACE_ID]: traceId } }, - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - ], + filter: [{ term: { [TRACE_ID]: traceId } }], }, }, }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); return { transaction: resp.hits.hits[0]?._source, }; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap index 30e75f46ad5e7..d94b766aee6a8 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap @@ -2,6 +2,13 @@ exports[`ui filter queries fetches environments 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "metric", + "error", + ], + }, "body": Object { "aggs": Object { "environments": Object { @@ -14,15 +21,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -42,16 +40,18 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; exports[`ui filter queries fetches environments without a service name 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "metric", + "error", + ], + }, "body": Object { "aggs": Object { "environments": Object { @@ -64,15 +64,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -87,10 +78,5 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts index 3fca30634be6a..98f00bf8e6555 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ProcessorEvent } from '../../../common/processor_event'; import { - PROCESSOR_EVENT, SERVICE_ENVIRONMENT, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; @@ -18,12 +18,9 @@ export async function getEnvironments( setup: Setup & SetupTimeRange, serviceName?: string ) { - const { start, end, client, indices } = setup; + const { start, end, apmEventClient } = setup; - const filter: ESFilter[] = [ - { terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] } }, - { range: rangeFilter(start, end) }, - ]; + const filter: ESFilter[] = [{ range: rangeFilter(start, end) }]; if (serviceName) { filter.push({ @@ -32,11 +29,13 @@ export async function getEnvironments( } const params = { - index: [ - indices['apm_oss.metricsIndices'], - indices['apm_oss.errorIndices'], - indices['apm_oss.transactionIndices'], - ], + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.metric, + ProcessorEvent.error, + ], + }, body: { size: 0, query: { @@ -55,7 +54,7 @@ export async function getEnvironments( }, }; - const resp = await client.search(params); + const resp = await apmEventClient.search(params); const aggs = resp.aggregations; const environmentsBuckets = aggs?.environments.buckets || []; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap index e6b6a9a52adfe..5f38432719280 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap @@ -2,6 +2,13 @@ exports[`local ui filter queries fetches local ui filter aggregations 1`] = ` Object { + "apm": Object { + "events": Array [ + "transaction", + "metric", + "error", + ], + }, "body": Object { "aggs": Object { "by_terms": Object { @@ -28,15 +35,6 @@ Object { "query": Object { "bool": Object { "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, Object { "range": Object { "@timestamp": Object { @@ -56,10 +54,5 @@ Object { }, "size": 0, }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], } `; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts index e892284fd87cd..cfbd79d37c041 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts @@ -5,8 +5,8 @@ */ import { omit } from 'lodash'; -import { mergeProjection } from '../../../../common/projections/util/merge_projection'; -import { Projection } from '../../../../common/projections/typings'; +import { mergeProjection } from '../../../projections/util/merge_projection'; +import { Projection } from '../../../projections/typings'; import { UIFilters } from '../../../../typings/ui_filters'; import { getUiFiltersES } from '../../helpers/convert_ui_filters/get_ui_filters_es'; import { localUIFilters, LocalUIFilterName } from './config'; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts index 3833b93c8d1f7..12c02679d0859 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts @@ -5,7 +5,7 @@ */ import { cloneDeep, orderBy } from 'lodash'; import { UIFilters } from '../../../../typings/ui_filters'; -import { Projection } from '../../../../common/projections/typings'; +import { Projection } from '../../../projections/typings'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { getLocalFilterQuery } from './get_local_filter_query'; import { Setup } from '../../helpers/setup_request'; @@ -26,7 +26,7 @@ export async function getLocalUIFilters({ uiFilters: UIFilters; localFilterNames: LocalUIFilterName[]; }) { - const { client } = setup; + const { apmEventClient } = setup; const projectionWithoutAggs = cloneDeep(projection); @@ -40,7 +40,7 @@ export async function getLocalUIFilters({ localUIFilterName: name, }); - const response = await client.search(query); + const response = await apmEventClient.search(query); const filter = localUIFilters[name]; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts index ac61910968850..92ee67de49314 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts @@ -9,7 +9,7 @@ import { SearchParamsMock, inspectSearchParams, } from '../../../../public/utils/testHelpers'; -import { getServicesProjection } from '../../../../common/projections/services'; +import { getServicesProjection } from '../../../projections/services'; describe('local ui filter queries', () => { let mock: SearchParamsMock; diff --git a/x-pack/plugins/apm/common/projections/errors.ts b/x-pack/plugins/apm/server/projections/errors.ts similarity index 70% rename from x-pack/plugins/apm/common/projections/errors.ts rename to x-pack/plugins/apm/server/projections/errors.ts index 390a8a0968102..49a0e9f479d26 100644 --- a/x-pack/plugins/apm/common/projections/errors.ts +++ b/x-pack/plugins/apm/server/projections/errors.ts @@ -8,15 +8,13 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; import { - PROCESSOR_EVENT, SERVICE_NAME, ERROR_GROUP_ID, -} from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../utils/range_filter'; +} from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { ProcessorEvent } from '../../common/processor_event'; export function getErrorGroupsProjection({ setup, @@ -25,16 +23,17 @@ export function getErrorGroupsProjection({ setup: Setup & SetupTimeRange & SetupUIFilters; serviceName: string; }) { - const { start, end, uiFiltersES, indices } = setup; + const { start, end, uiFiltersES } = setup; return { - index: indices['apm_oss.errorIndices'], + apm: { + events: [ProcessorEvent.error as const], + }, body: { query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'error' } }, { range: rangeFilter(start, end) }, ...uiFiltersES, ], diff --git a/x-pack/plugins/apm/common/projections/metrics.ts b/x-pack/plugins/apm/server/projections/metrics.ts similarity index 72% rename from x-pack/plugins/apm/common/projections/metrics.ts rename to x-pack/plugins/apm/server/projections/metrics.ts index 45998bfe82e96..eb80a6bc73248 100644 --- a/x-pack/plugins/apm/common/projections/metrics.ts +++ b/x-pack/plugins/apm/server/projections/metrics.ts @@ -8,16 +8,14 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; import { SERVICE_NAME, - PROCESSOR_EVENT, SERVICE_NODE_NAME, -} from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../utils/range_filter'; -import { SERVICE_NODE_NAME_MISSING } from '../service_nodes'; +} from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { SERVICE_NODE_NAME_MISSING } from '../../common/service_nodes'; +import { ProcessorEvent } from '../../common/processor_event'; function getServiceNodeNameFilters(serviceNodeName?: string) { if (!serviceNodeName) { @@ -40,18 +38,19 @@ export function getMetricsProjection({ serviceName: string; serviceNodeName?: string; }) { - const { start, end, uiFiltersES, indices } = setup; + const { start, end, uiFiltersES } = setup; const filter = [ { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'metric' } }, { range: rangeFilter(start, end) }, ...getServiceNodeNameFilters(serviceNodeName), ...uiFiltersES, ]; return { - index: indices['apm_oss.metricsIndices'], + apm: { + events: [ProcessorEvent.metric], + }, body: { query: { bool: { diff --git a/x-pack/plugins/apm/common/projections/rum_overview.ts b/x-pack/plugins/apm/server/projections/rum_overview.ts similarity index 71% rename from x-pack/plugins/apm/common/projections/rum_overview.ts rename to x-pack/plugins/apm/server/projections/rum_overview.ts index b1218546d09ff..4588ec2a0451f 100644 --- a/x-pack/plugins/apm/common/projections/rum_overview.ts +++ b/x-pack/plugins/apm/server/projections/rum_overview.ts @@ -8,22 +8,21 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; -import { PROCESSOR_EVENT, TRANSACTION_TYPE } from '../elasticsearch_fieldnames'; -import { rangeFilter } from '../utils/range_filter'; +import { TRANSACTION_TYPE } from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { ProcessorEvent } from '../../common/processor_event'; export function getRumOverviewProjection({ setup, }: { setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, uiFiltersES, indices } = setup; + const { start, end, uiFiltersES } = setup; const bool = { filter: [ { range: rangeFilter(start, end) }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, { term: { [TRANSACTION_TYPE]: 'page-load' } }, { // Adding this filter to cater for some inconsistent rum data @@ -36,7 +35,9 @@ export function getRumOverviewProjection({ }; return { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction], + }, body: { query: { bool, diff --git a/x-pack/plugins/apm/common/projections/service_nodes.ts b/x-pack/plugins/apm/server/projections/service_nodes.ts similarity index 88% rename from x-pack/plugins/apm/common/projections/service_nodes.ts rename to x-pack/plugins/apm/server/projections/service_nodes.ts index 1bc68f51a26ed..87fe815a12d0d 100644 --- a/x-pack/plugins/apm/common/projections/service_nodes.ts +++ b/x-pack/plugins/apm/server/projections/service_nodes.ts @@ -8,9 +8,8 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; -import { SERVICE_NODE_NAME } from '../elasticsearch_fieldnames'; +import { SERVICE_NODE_NAME } from '../../common/elasticsearch_fieldnames'; import { mergeProjection } from './util/merge_projection'; import { getMetricsProjection } from './metrics'; diff --git a/x-pack/plugins/apm/server/projections/services.ts b/x-pack/plugins/apm/server/projections/services.ts new file mode 100644 index 0000000000000..18fa79f31d6f3 --- /dev/null +++ b/x-pack/plugins/apm/server/projections/services.ts @@ -0,0 +1,47 @@ +/* + * 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 { + Setup, + SetupUIFilters, + SetupTimeRange, +} from '../../server/lib/helpers/setup_request'; +import { SERVICE_NAME } from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { ProcessorEvent } from '../../common/processor_event'; + +export function getServicesProjection({ + setup, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; +}) { + const { start, end, uiFiltersES } = setup; + + return { + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.metric, + ProcessorEvent.error, + ], + }, + body: { + size: 0, + query: { + bool: { + filter: [{ range: rangeFilter(start, end) }, ...uiFiltersES], + }, + }, + aggs: { + services: { + terms: { + field: SERVICE_NAME, + }, + }, + }, + }, + }; +} diff --git a/x-pack/plugins/apm/common/projections/transaction_groups.ts b/x-pack/plugins/apm/server/projections/transaction_groups.ts similarity index 86% rename from x-pack/plugins/apm/common/projections/transaction_groups.ts rename to x-pack/plugins/apm/server/projections/transaction_groups.ts index 1708d89aad4ec..8aa085cccf82a 100644 --- a/x-pack/plugins/apm/common/projections/transaction_groups.ts +++ b/x-pack/plugins/apm/server/projections/transaction_groups.ts @@ -8,10 +8,11 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; -import { TRANSACTION_NAME, PARENT_ID } from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { + TRANSACTION_NAME, + PARENT_ID, +} from '../../common/elasticsearch_fieldnames'; import { Options } from '../../server/lib/transaction_groups/fetcher'; import { getTransactionsProjection } from './transactions'; import { mergeProjection } from './util/merge_projection'; diff --git a/x-pack/plugins/apm/common/projections/transactions.ts b/x-pack/plugins/apm/server/projections/transactions.ts similarity index 76% rename from x-pack/plugins/apm/common/projections/transactions.ts rename to x-pack/plugins/apm/server/projections/transactions.ts index b6cd73ca9aaad..f428a76a8b0cb 100644 --- a/x-pack/plugins/apm/common/projections/transactions.ts +++ b/x-pack/plugins/apm/server/projections/transactions.ts @@ -8,16 +8,14 @@ import { Setup, SetupTimeRange, SetupUIFilters, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/helpers/setup_request'; import { SERVICE_NAME, TRANSACTION_TYPE, - PROCESSOR_EVENT, TRANSACTION_NAME, -} from '../elasticsearch_fieldnames'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../utils/range_filter'; +} from '../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../common/utils/range_filter'; +import { ProcessorEvent } from '../../common/processor_event'; export function getTransactionsProjection({ setup, @@ -30,7 +28,7 @@ export function getTransactionsProjection({ transactionName?: string; transactionType?: string; }) { - const { start, end, uiFiltersES, indices } = setup; + const { start, end, uiFiltersES } = setup; const transactionNameFilter = transactionName ? [{ term: { [TRANSACTION_NAME]: transactionName } }] @@ -45,7 +43,6 @@ export function getTransactionsProjection({ const bool = { filter: [ { range: rangeFilter(start, end) }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, ...transactionNameFilter, ...transactionTypeFilter, ...serviceNameFilter, @@ -54,7 +51,9 @@ export function getTransactionsProjection({ }; return { - index: indices['apm_oss.transactionIndices'], + apm: { + events: [ProcessorEvent.transaction as const], + }, body: { query: { bool, diff --git a/x-pack/plugins/apm/common/projections/typings.ts b/x-pack/plugins/apm/server/projections/typings.ts similarity index 56% rename from x-pack/plugins/apm/common/projections/typings.ts rename to x-pack/plugins/apm/server/projections/typings.ts index 693795b09e1d0..77a5beaf54605 100644 --- a/x-pack/plugins/apm/common/projections/typings.ts +++ b/x-pack/plugins/apm/server/projections/typings.ts @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESSearchRequest, ESSearchBody } from '../../typings/elasticsearch'; +import { ESSearchBody } from '../../typings/elasticsearch'; import { AggregationOptionsByType, AggregationInputMap, } from '../../typings/elasticsearch/aggregations'; +import { APMEventESSearchRequest } from '../lib/helpers/create_es_client/create_apm_event_client'; -export type Projection = Omit & { +export type Projection = Omit & { body: Omit & { aggs?: { [key: string]: { @@ -20,14 +21,3 @@ export type Projection = Omit & { }; }; }; - -export enum PROJECTION { - SERVICES = 'services', - TRANSACTION_GROUPS = 'transactionGroups', - TRACES = 'traces', - TRANSACTIONS = 'transactions', - METRICS = 'metrics', - ERROR_GROUPS = 'errorGroups', - SERVICE_NODES = 'serviceNodes', - RUM_OVERVIEW = 'rumOverview', -} diff --git a/x-pack/plugins/apm/common/projections/util/merge_projection/index.test.ts b/x-pack/plugins/apm/server/projections/util/merge_projection/index.test.ts similarity index 73% rename from x-pack/plugins/apm/common/projections/util/merge_projection/index.test.ts rename to x-pack/plugins/apm/server/projections/util/merge_projection/index.test.ts index 33727fcb9c735..aa02c8898d218 100644 --- a/x-pack/plugins/apm/common/projections/util/merge_projection/index.test.ts +++ b/x-pack/plugins/apm/server/projections/util/merge_projection/index.test.ts @@ -10,10 +10,19 @@ describe('mergeProjection', () => { it('overrides arrays', () => { expect( mergeProjection( - { body: { query: { bool: { must: [{ terms: ['a'] }] } } } }, - { body: { query: { bool: { must: [{ term: 'b' }] } } } } + { + apm: { events: [] }, + body: { query: { bool: { must: [{ terms: ['a'] }] } } }, + }, + { + apm: { events: [] }, + body: { query: { bool: { must: [{ term: 'b' }] } } }, + } ) ).toEqual({ + apm: { + events: [], + }, body: { query: { bool: { @@ -32,8 +41,11 @@ describe('mergeProjection', () => { const termsAgg = { terms: { field: 'bar' } }; expect( mergeProjection( - { body: { query: {}, aggs: { foo: termsAgg } } }, + { apm: { events: [] }, body: { query: {}, aggs: { foo: termsAgg } } }, { + apm: { + events: [], + }, body: { aggs: { foo: { ...termsAgg, aggs: { bar: { terms: { field: 'baz' } } } }, @@ -42,6 +54,9 @@ describe('mergeProjection', () => { } ) ).toEqual({ + apm: { + events: [], + }, body: { query: {}, aggs: { diff --git a/x-pack/plugins/apm/common/projections/util/merge_projection/index.ts b/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts similarity index 82% rename from x-pack/plugins/apm/common/projections/util/merge_projection/index.ts rename to x-pack/plugins/apm/server/projections/util/merge_projection/index.ts index 9dc1c815bf169..ea7267dd337c2 100644 --- a/x-pack/plugins/apm/common/projections/util/merge_projection/index.ts +++ b/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts @@ -6,15 +6,13 @@ import { mergeWith, isPlainObject, cloneDeep } from 'lodash'; import { DeepPartial } from 'utility-types'; import { AggregationInputMap } from '../../../../typings/elasticsearch/aggregations'; -import { - ESSearchRequest, - ESSearchBody, -} from '../../../../typings/elasticsearch'; +import { ESSearchBody } from '../../../../typings/elasticsearch'; import { Projection } from '../../typings'; +import { APMEventESSearchRequest } from '../../../lib/helpers/create_es_client/create_apm_event_client'; type PlainObject = Record; -type SourceProjection = Omit, 'body'> & { +type SourceProjection = Omit, 'body'> & { body: Omit, 'aggs'> & { aggs?: AggregationInputMap; }; diff --git a/x-pack/plugins/apm/server/routes/ui_filters.ts b/x-pack/plugins/apm/server/routes/ui_filters.ts index a47d72751dfc4..864f5033c9d62 100644 --- a/x-pack/plugins/apm/server/routes/ui_filters.ts +++ b/x-pack/plugins/apm/server/routes/ui_filters.ts @@ -13,23 +13,23 @@ import { SetupTimeRange, } from '../lib/helpers/setup_request'; import { getEnvironments } from '../lib/ui_filters/get_environments'; -import { Projection } from '../../common/projections/typings'; +import { Projection } from '../projections/typings'; import { localUIFilterNames, LocalUIFilterName, } from '../lib/ui_filters/local_ui_filters/config'; import { getUiFiltersES } from '../lib/helpers/convert_ui_filters/get_ui_filters_es'; import { getLocalUIFilters } from '../lib/ui_filters/local_ui_filters'; -import { getServicesProjection } from '../../common/projections/services'; -import { getTransactionGroupsProjection } from '../../common/projections/transaction_groups'; -import { getMetricsProjection } from '../../common/projections/metrics'; -import { getErrorGroupsProjection } from '../../common/projections/errors'; -import { getTransactionsProjection } from '../../common/projections/transactions'; +import { getServicesProjection } from '../projections/services'; +import { getTransactionGroupsProjection } from '../projections/transaction_groups'; +import { getMetricsProjection } from '../projections/metrics'; +import { getErrorGroupsProjection } from '../projections/errors'; +import { getTransactionsProjection } from '../projections/transactions'; import { createRoute } from './create_route'; import { uiFiltersRt, rangeRt } from './default_api_types'; import { jsonRt } from '../../common/runtime_types/json_rt'; -import { getServiceNodesProjection } from '../../common/projections/service_nodes'; -import { getRumOverviewProjection } from '../../common/projections/rum_overview'; +import { getServiceNodesProjection } from '../projections/service_nodes'; +import { getRumOverviewProjection } from '../projections/rum_overview'; export const uiFiltersEnvironmentsRoute = createRoute(() => ({ path: '/api/apm/ui_filters/environments',