From 3c597d07185361952e1f7ada7be51fb874cc4fcd Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 17 Sep 2020 15:32:29 -0700 Subject: [PATCH 01/25] Fixes typo in data recognizer text (#77691) --- .../modules/siem_auditbeat/ml/linux_rare_user_compiler.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_user_compiler.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_user_compiler.json index 245b7e0819c7d..bb0323ed9ae78 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_user_compiler.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_user_compiler.json @@ -1,6 +1,6 @@ { "job_type": "anomaly_detector", - "description": "Security: Auditbeat - Looks for compiler activity by a user context which does not normally run compilers. This can be ad-hoc software changes or unauthorized software deployment. This can also be due to local privliege elevation via locally run exploits or malware activity.", + "description": "Security: Auditbeat - Looks for compiler activity by a user context which does not normally run compilers. This can be ad-hoc software changes or unauthorized software deployment. This can also be due to local privilege elevation via locally run exploits or malware activity.", "groups": [ "security", "auditbeat", From 5d7c60f0ae8e992027d175ed20079a75de85c405 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 17 Sep 2020 17:51:47 -0700 Subject: [PATCH 02/25] skip flaky suite (#77835) --- .../test/security_solution_endpoint/apps/endpoint/resolver.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts index 725edb6d98198..e8fa18aa01b4c 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts @@ -13,7 +13,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const queryBar = getService('queryBar'); - describe('Endpoint Event Resolver', function () { + // FLAKY: https://github.com/elastic/kibana/issues/77835 + describe.skip('Endpoint Event Resolver', function () { before(async () => { await esArchiver.load('endpoint/resolver_tree', { useCreate: true }); await pageObjects.hosts.navigateToSecurityHostsPage(); From 12e614d6aca0a020a30a52f7b5d1bf9ef9db2829 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 17 Sep 2020 19:40:15 -0700 Subject: [PATCH 03/25] remove visual aspects of baseline job (#77815) Co-authored-by: spalger Co-authored-by: Elastic Machine --- .ci/Jenkinsfile_baseline_capture | 12 ++++++------ ...kins_visual_regression.sh => jenkins_baseline.sh} | 7 ------- ...isual_regression.sh => jenkins_xpack_baseline.sh} | 11 ----------- vars/kibanaPipeline.groovy | 8 +++++--- vars/workers.groovy | 4 ++-- 5 files changed, 13 insertions(+), 29 deletions(-) rename test/scripts/{jenkins_visual_regression.sh => jenkins_baseline.sh} (63%) rename test/scripts/{jenkins_xpack_visual_regression.sh => jenkins_xpack_baseline.sh} (64%) diff --git a/.ci/Jenkinsfile_baseline_capture b/.ci/Jenkinsfile_baseline_capture index 9a49c19b94df2..33ecfcd84fd3e 100644 --- a/.ci/Jenkinsfile_baseline_capture +++ b/.ci/Jenkinsfile_baseline_capture @@ -11,14 +11,14 @@ kibanaPipeline(timeoutMinutes: 120) { 'CI_PARALLEL_PROCESS_NUMBER=1' ]) { parallel([ - 'oss-visualRegression': { - workers.ci(name: 'oss-visualRegression', size: 's-highmem', ramDisk: true) { - kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh')() + 'oss-baseline': { + workers.ci(name: 'oss-baseline', size: 's-highmem', ramDisk: true, runErrorReporter: false) { + kibanaPipeline.functionalTestProcess('oss-baseline', './test/scripts/jenkins_baseline.sh')() } }, - 'xpack-visualRegression': { - workers.ci(name: 'xpack-visualRegression', size: 's-highmem', ramDisk: true) { - kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh')() + 'xpack-baseline': { + workers.ci(name: 'xpack-baseline', size: 's-highmem', ramDisk: true, runErrorReporter: false) { + kibanaPipeline.functionalTestProcess('xpack-baseline', './test/scripts/jenkins_xpack_baseline.sh')() } }, ]) diff --git a/test/scripts/jenkins_visual_regression.sh b/test/scripts/jenkins_baseline.sh similarity index 63% rename from test/scripts/jenkins_visual_regression.sh rename to test/scripts/jenkins_baseline.sh index 17345d4301882..e679ac7f31bd1 100755 --- a/test/scripts/jenkins_visual_regression.sh +++ b/test/scripts/jenkins_baseline.sh @@ -9,10 +9,3 @@ linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" installDir="$PARENT_DIR/install/kibana" mkdir -p "$installDir" tar -xzf "$linuxBuild" -C "$installDir" --strip=1 - -echo " -> running visual regression tests from kibana directory" -yarn percy exec -t 10000 -- -- \ - node scripts/functional_tests \ - --debug --bail \ - --kibana-install-dir "$installDir" \ - --config test/visual_regression/config.ts; diff --git a/test/scripts/jenkins_xpack_visual_regression.sh b/test/scripts/jenkins_xpack_baseline.sh similarity index 64% rename from test/scripts/jenkins_xpack_visual_regression.sh rename to test/scripts/jenkins_xpack_baseline.sh index 55d4a524820c5..7577b6927d166 100755 --- a/test/scripts/jenkins_xpack_visual_regression.sh +++ b/test/scripts/jenkins_xpack_baseline.sh @@ -14,16 +14,5 @@ tar -xzf "$linuxBuild" -C "$installDir" --strip=1 mkdir -p "$WORKSPACE/kibana-build-xpack" cp -pR install/kibana/. $WORKSPACE/kibana-build-xpack/ -# cd "$KIBANA_DIR" -# source "test/scripts/jenkins_xpack_page_load_metrics.sh" - cd "$KIBANA_DIR" source "test/scripts/jenkins_xpack_saved_objects_field_metrics.sh" - -echo " -> running visual regression tests from x-pack directory" -cd "$XPACK_DIR" -yarn percy exec -t 10000 -- -- \ - node scripts/functional_tests \ - --debug --bail \ - --kibana-install-dir "$installDir" \ - --config test/visual_regression/config.ts; diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index e5b39584a519b..28eb94405abbb 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -1,4 +1,4 @@ -def withPostBuildReporting(Closure closure) { +def withPostBuildReporting(Map params, Closure closure) { try { closure() } finally { @@ -9,8 +9,10 @@ def withPostBuildReporting(Closure closure) { print ex } - catchErrors { - runErrorReporter([pwd()] + parallelWorkspaces) + if (params.runErrorReporter) { + catchErrors { + runErrorReporter([pwd()] + parallelWorkspaces) + } } catchErrors { diff --git a/vars/workers.groovy b/vars/workers.groovy index e582e996a78b5..b6ff5b27667dd 100644 --- a/vars/workers.groovy +++ b/vars/workers.groovy @@ -118,11 +118,11 @@ def base(Map params, Closure closure) { // Worker for ci processes. Extends the base worker and adds GCS artifact upload, error reporting, junit processing def ci(Map params, Closure closure) { - def config = [ramDisk: true, bootstrapped: true] + params + def config = [ramDisk: true, bootstrapped: true, runErrorReporter: true] + params return base(config) { kibanaPipeline.withGcsArtifactUpload(config.name) { - kibanaPipeline.withPostBuildReporting { + kibanaPipeline.withPostBuildReporting(config) { closure() } } From 61226103740595ad20e1c96f5ce8dbf627ce5e50 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 17 Sep 2020 22:35:21 -0700 Subject: [PATCH 04/25] skip flaky suite (#76239) --- .../security_api_integration/tests/session_idle/cleanup.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts index c4302b7637923..f288bc925123e 100644 --- a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts +++ b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts @@ -33,7 +33,8 @@ export default function ({ getService }: FtrProviderContext) { return (await es.search({ index: '.kibana_security_session*' })).hits.total.value; } - describe('Session Idle cleanup', () => { + // FLAKY: https://github.com/elastic/kibana/issues/76239 + describe.skip('Session Idle cleanup', () => { beforeEach(async () => { await es.cluster.health({ index: '.kibana_security_session*', waitForStatus: 'green' }); await es.deleteByQuery({ From eb825db09ec98298a10b05dab9eae8a5154f0ca0 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Fri, 18 Sep 2020 08:43:12 +0200 Subject: [PATCH 05/25] [Lens] change name of custom query to filters (#77725) --- .../dimension_panel/bucket_nesting_editor.tsx | 2 +- .../definitions/filters/filters.test.tsx | 6 +++--- .../operations/definitions/filters/filters.tsx | 16 ++++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx index 281c72915e46e..325f18ee9833a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx @@ -69,7 +69,7 @@ export function BucketNestingEditor({ values: { field: fieldName }, }), filters: i18n.translate('xpack.lens.indexPattern.groupingOverallFilters', { - defaultMessage: 'Top values for each custom query', + defaultMessage: 'Top values for each filter', }), date_histogram: i18n.translate('xpack.lens.indexPattern.groupingOverallDateHistogram', { defaultMessage: 'Top values for each {field}', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx index 6364d3913bf57..2d79c5faf74fe 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx @@ -54,7 +54,7 @@ describe('filters', () => { columnOrder: ['col1', 'col2'], columns: { col1: { - label: 'Custom query', + label: 'filters', dataType: 'document', operationType: 'filters', scale: 'ordinal', @@ -209,7 +209,7 @@ describe('filters', () => { }); }); - describe('Modify custom query', () => { + describe('Modify filters', () => { it('should correctly show existing filters ', () => { const setStateSpy = jest.fn(); const instance = mount( @@ -236,7 +236,7 @@ describe('filters', () => { ).toEqual('src : 2'); }); - it('should remove custom query', () => { + it('should remove filter', () => { const setStateSpy = jest.fn(); const instance = mount( = { type: 'filters', - displayName: customQueryLabel, + displayName: filtersLabel, priority: 3, // Higher than any metric getPossibleOperationForField: ({ type }) => { if (type === 'document') { @@ -102,7 +102,7 @@ export const filtersOperation: OperationDefinition = } return { - label: customQueryLabel, + label: filtersLabel, dataType: 'string', operationType: 'filters', scale: 'ordinal', @@ -223,8 +223,8 @@ export const FilterList = ({ defaultMessage: 'This query is invalid', })} onRemoveClick={() => onRemoveFilter(filter.id)} - removeTitle={i18n.translate('xpack.lens.indexPattern.filters.removeCustomQuery', { - defaultMessage: 'Remove custom query', + removeTitle={i18n.translate('xpack.lens.indexPattern.filters.removeFilter', { + defaultMessage: 'Remove a filter', })} > From 3759063e813f364c39f2fd93d59827319a4681a5 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Fri, 18 Sep 2020 09:30:13 +0200 Subject: [PATCH 06/25] remove legacy ES plugin (#77703) * remove legacy ES plug * fix types * delete some `legacy/core_plugins` references * fix type --- .github/CODEOWNERS | 5 - kibana.d.ts | 10 - src/cli/cluster/cluster_manager.ts | 1 - src/cli/serve/serve.js | 2 +- src/dev/jest/config.js | 4 - src/dev/precommit_hook/casing_check_config.js | 5 - .../core_plugins/elasticsearch/index.d.ts | 526 ------------------ .../core_plugins/elasticsearch/index.js | 58 -- .../core_plugins/elasticsearch/package.json | 5 - .../server/lib/abortable_request_handler.js | 34 -- .../lib/abortable_request_handler.test.js | 58 -- .../elasticsearch/server/lib/cluster.ts | 51 -- .../elasticsearch/server/lib/create_proxy.js | 79 --- src/legacy/server/kbn_server.d.ts | 4 - .../apis/elasticsearch/index.js | 40 -- test/api_integration/apis/index.js | 1 - x-pack/dev-tools/jest/create_jest_config.js | 1 - x-pack/legacy/plugins/spaces/index.ts | 2 +- x-pack/legacy/plugins/xpack_main/index.js | 2 +- .../call_with_request_factory/index.d.ts | 4 +- .../canvas/server/collectors/collector.ts | 4 +- .../shareable_runtime/webpack.config.js | 4 - x-pack/plugins/canvas/types/telemetry.ts | 4 +- .../server/usage/get_reporting_usage.ts | 4 +- .../server/usage/reporting_usage_collector.ts | 4 +- .../rollup/server/collectors/register.ts | 10 +- .../plugins/searchprofiler/server/plugin.ts | 3 +- x-pack/plugins/searchprofiler/server/types.ts | 3 - .../server/telemetry_collection/get_xpack.ts | 4 +- .../transform/server/routes/api/transforms.ts | 8 +- .../plugins/upgrade_assistant/common/types.ts | 3 +- .../components/tabs/checkup/constants.tsx | 4 +- .../components/tabs/checkup/controls.tsx | 4 +- .../checkup/deprecations/grouped.test.tsx | 4 +- .../tabs/checkup/deprecations/grouped.tsx | 3 +- .../tabs/checkup/deprecations/health.tsx | 3 +- .../tabs/checkup/deprecations/list.tsx | 3 +- .../tabs/checkup/filter_bar.test.tsx | 3 +- .../components/tabs/checkup/filter_bar.tsx | 3 +- .../server/lib/es_migration_apis.test.ts | 3 +- .../server/lib/es_migration_apis.ts | 3 +- x-pack/tsconfig.json | 21 +- 42 files changed, 53 insertions(+), 944 deletions(-) delete mode 100644 src/legacy/core_plugins/elasticsearch/index.d.ts delete mode 100644 src/legacy/core_plugins/elasticsearch/index.js delete mode 100644 src/legacy/core_plugins/elasticsearch/package.json delete mode 100644 src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.js delete mode 100644 src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.test.js delete mode 100644 src/legacy/core_plugins/elasticsearch/server/lib/cluster.ts delete mode 100644 src/legacy/core_plugins/elasticsearch/server/lib/create_proxy.js delete mode 100644 test/api_integration/apis/elasticsearch/index.js diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1077230812b60..7daa42af7024d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -59,7 +59,6 @@ # APM /x-pack/plugins/apm/ @elastic/apm-ui /x-pack/test/functional/apps/apm/ @elastic/apm-ui -/src/legacy/core_plugins/apm_oss/ @elastic/apm-ui /src/plugins/apm_oss/ @elastic/apm-ui /src/apm.js @watson @vigneshshanmugam @@ -83,9 +82,6 @@ /src/plugins/home/public @elastic/kibana-core-ui /src/plugins/home/server/*.ts @elastic/kibana-core-ui /src/plugins/home/server/services/ @elastic/kibana-core-ui -# Exclude tutorial resources folder for now because they are not owned by Kibana app and most will move out soon -/src/legacy/core_plugins/kibana/public/home/*.ts @elastic/kibana-core-ui -/src/legacy/core_plugins/kibana/public/home/np_ready/ @elastic/kibana-core-ui /x-pack/plugins/global_search_bar/ @elastic/kibana-core-ui # Observability UIs @@ -285,7 +281,6 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Core design /src/plugins/dashboard/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/canvas/**/*.scss @elastic/kibana-core-ui-designers -/src/legacy/core_plugins/kibana/public/home/**/*.scss @elastic/kibana-core-ui-designers /x-pack/legacy/plugins/spaces/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/spaces/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/security/**/*.scss @elastic/kibana-core-ui-designers diff --git a/kibana.d.ts b/kibana.d.ts index 517bda374af9d..b707405ffbeaf 100644 --- a/kibana.d.ts +++ b/kibana.d.ts @@ -28,7 +28,6 @@ export { Public, Server }; /** * All exports from TS ambient definitions (where types are added for JS source in a .d.ts file). */ -import * as LegacyElasticsearch from './src/legacy/core_plugins/elasticsearch'; import * as LegacyKibanaPluginSpec from './src/legacy/plugin_discovery/plugin_spec/plugin_spec_options'; import * as LegacyKibanaServer from './src/legacy/server/kbn_server'; @@ -44,13 +43,4 @@ export namespace Legacy { export type InitPluginFunction = LegacyKibanaPluginSpec.InitPluginFunction; export type UiExports = LegacyKibanaPluginSpec.UiExports; export type PluginSpecOptions = LegacyKibanaPluginSpec.PluginSpecOptions; - - export namespace Plugins { - export namespace elasticsearch { - export type Plugin = LegacyElasticsearch.ElasticsearchPlugin; - export type Cluster = LegacyElasticsearch.Cluster; - export type ClusterConfig = LegacyElasticsearch.ClusterConfig; - export type CallClusterOptions = LegacyElasticsearch.CallClusterOptions; - } - } } diff --git a/src/cli/cluster/cluster_manager.ts b/src/cli/cluster/cluster_manager.ts index b1d1335eb1888..78472bb3f517d 100644 --- a/src/cli/cluster/cluster_manager.ts +++ b/src/cli/cluster/cluster_manager.ts @@ -224,7 +224,6 @@ export class ClusterManager { new Set( [ fromRoot('src/core'), - fromRoot('src/legacy/core_plugins'), fromRoot('src/legacy/server'), fromRoot('src/legacy/ui'), fromRoot('src/legacy/utils'), diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index eeb5564667ec4..d8bd39b9dcdf4 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -178,7 +178,7 @@ export default function (program) { 'A path to scan for plugins, this can be specified multiple ' + 'times to specify multiple directories', pluginDirCollector, - [fromRoot('plugins'), fromRoot('src/legacy/core_plugins')] + [fromRoot('plugins')] ) .option( '--plugin-path ', diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 486c8563c5456..5d31db63773fa 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -25,7 +25,6 @@ export default { '/src/plugins', '/src/legacy/ui', '/src/core', - '/src/legacy/core_plugins', '/src/legacy/server', '/src/cli', '/src/cli_keystore', @@ -51,14 +50,11 @@ export default { 'packages/kbn-ui-framework/src/services/**/*.js', '!packages/kbn-ui-framework/src/services/index.js', '!packages/kbn-ui-framework/src/services/**/*/index.js', - 'src/legacy/core_plugins/**/*.{js,mjs,jsx,ts,tsx}', - '!src/legacy/core_plugins/**/{__test__,__snapshots__}/**/*', ], moduleNameMapper: { '@elastic/eui$': '/node_modules/@elastic/eui/test-env', '@elastic/eui/lib/(.*)?': '/node_modules/@elastic/eui/test-env/$1', '^src/plugins/(.*)': '/src/plugins/$1', - '^plugins/([^/.]*)(.*)': '/src/legacy/core_plugins/$1/public$2', '^uiExports/(.*)': '/src/dev/jest/mocks/file_mock.js', '^test_utils/(.*)': '/src/test_utils/public/$1', '^fixtures/(.*)': '/src/fixtures/$1', diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index e22dc03cf57aa..ba58dcdfa4d58 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -132,11 +132,6 @@ export const REMOVE_EXTENSION = ['packages/kbn-plugin-generator/template/**/*.ej * @type {Array} */ export const TEMPORARILY_IGNORED_PATHS = [ - 'src/legacy/core_plugins/console/public/src/directives/helpExample.txt', - 'src/legacy/core_plugins/console/public/src/sense_editor/theme-sense-dark.js', - 'src/legacy/core_plugins/tile_map/public/__tests__/scaledCircleMarkers.png', - 'src/legacy/core_plugins/tile_map/public/__tests__/shadedCircleMarkers.png', - 'src/legacy/core_plugins/tile_map/public/__tests__/shadedGeohashGrid.png', 'src/fixtures/config_upgrade_from_4.0.0_to_4.0.1-snapshot.json', 'src/core/server/core_app/assets/favicons/android-chrome-192x192.png', 'src/core/server/core_app/assets/favicons/android-chrome-256x256.png', diff --git a/src/legacy/core_plugins/elasticsearch/index.d.ts b/src/legacy/core_plugins/elasticsearch/index.d.ts deleted file mode 100644 index 83e7bb19e57ba..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/index.d.ts +++ /dev/null @@ -1,526 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { - Client as ESClient, - GenericParams, - // root params - BulkIndexDocumentsParams, - ClearScrollParams, - CountParams, - CreateDocumentParams, - DeleteDocumentParams, - DeleteDocumentByQueryParams, - DeleteScriptParams, - DeleteTemplateParams, - ExistsParams, - ExplainParams, - FieldStatsParams, - GetParams, - GetResponse, - GetScriptParams, - GetSourceParams, - GetTemplateParams, - IndexDocumentParams, - InfoParams, - MGetParams, - MSearchParams, - MSearchTemplateParams, - MTermVectorsParams, - PingParams, - PutScriptParams, - PutTemplateParams, - ReindexParams, - ReindexRethrottleParams, - RenderSearchTemplateParams, - ScrollParams, - SearchParams, - SearchShardsParams, - SearchTemplateParams, - SuggestParams, - TermvectorsParams, - UpdateDocumentParams, - UpdateDocumentByQueryParams, - MGetResponse, - MSearchResponse, - SearchResponse, - // cat - CatAliasesParams, - CatAllocationParams, - CatFielddataParams, - CatHealthParams, - CatHelpParams, - CatIndicesParams, - CatCommonParams, - CatRecoveryParams, - CatSegmentsParams, - CatShardsParams, - CatSnapshotsParams, - CatTasksParams, - CatThreadPoolParams, - // cluster - ClusterAllocationExplainParams, - ClusterGetSettingsParams, - ClusterHealthParams, - ClusterPendingTasksParams, - ClusterPutSettingsParams, - ClusterRerouteParams, - ClusterStateParams, - ClusterStatsParams, - // indices - IndicesAnalyzeParams, - IndicesClearCacheParams, - IndicesCloseParams, - IndicesCreateParams, - IndicesDeleteParams, - IndicesDeleteAliasParams, - IndicesDeleteTemplateParams, - IndicesExistsParams, - IndicesExistsAliasParams, - IndicesExistsTemplateParams, - IndicesExistsTypeParams, - IndicesFlushParams, - IndicesFlushSyncedParams, - IndicesForcemergeParams, - IndicesGetParams, - IndicesGetAliasParams, - IndicesGetFieldMappingParams, - IndicesGetMappingParams, - IndicesGetSettingsParams, - IndicesGetTemplateParams, - IndicesGetUpgradeParams, - IndicesOpenParams, - IndicesPutAliasParams, - IndicesPutMappingParams, - IndicesPutSettingsParams, - IndicesPutTemplateParams, - IndicesRecoveryParams, - IndicesRefreshParams, - IndicesRolloverParams, - IndicesSegmentsParams, - IndicesShardStoresParams, - IndicesShrinkParams, - IndicesStatsParams, - IndicesUpdateAliasesParams, - IndicesUpgradeParams, - IndicesValidateQueryParams, - // ingest - IngestDeletePipelineParams, - IngestGetPipelineParams, - IngestPutPipelineParams, - IngestSimulateParams, - // nodes - NodesHotThreadsParams, - NodesInfoParams, - NodesStatsParams, - // snapshot - SnapshotCreateParams, - SnapshotCreateRepositoryParams, - SnapshotDeleteParams, - SnapshotDeleteRepositoryParams, - SnapshotGetParams, - SnapshotGetRepositoryParams, - SnapshotRestoreParams, - SnapshotStatusParams, - SnapshotVerifyRepositoryParams, - // tasks - TasksCancelParams, - TasksGetParams, - TasksListParams, -} from 'elasticsearch'; - -export class Cluster { - public callWithRequest: CallClusterWithRequest; - public callWithInternalUser: CallCluster; - constructor(config: ClusterConfig); -} - -export interface ClusterConfig { - [option: string]: any; -} - -export interface Request { - headers: RequestHeaders; -} - -interface RequestHeaders { - [name: string]: string; -} - -interface AssistantAPIClientParams extends GenericParams { - path: '/_migration/assistance'; - method: 'GET'; -} - -type MIGRATION_ASSISTANCE_INDEX_ACTION = 'upgrade' | 'reindex'; -type MIGRATION_DEPRECATION_LEVEL = 'none' | 'info' | 'warning' | 'critical'; - -export interface AssistanceAPIResponse { - indices: { - [indexName: string]: { - action_required: MIGRATION_ASSISTANCE_INDEX_ACTION; - }; - }; -} - -interface DeprecationAPIClientParams extends GenericParams { - path: '/_migration/deprecations'; - method: 'GET'; -} - -export interface DeprecationInfo { - level: MIGRATION_DEPRECATION_LEVEL; - message: string; - url: string; - details?: string; -} - -export interface IndexSettingsDeprecationInfo { - [indexName: string]: DeprecationInfo[]; -} - -export interface DeprecationAPIResponse { - cluster_settings: DeprecationInfo[]; - ml_settings: DeprecationInfo[]; - node_settings: DeprecationInfo[]; - index_settings: IndexSettingsDeprecationInfo; -} - -export interface CallClusterOptions { - wrap401Errors?: boolean; - signal?: AbortSignal; -} - -export interface CallClusterWithRequest { - /* eslint-disable */ - (request: Request, endpoint: 'bulk', params: BulkIndexDocumentsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'clearScroll', params: ClearScrollParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'count', params: CountParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'create', params: CreateDocumentParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'delete', params: DeleteDocumentParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'deleteByQuery', params: DeleteDocumentByQueryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'deleteScript', params: DeleteScriptParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'deleteTemplate', params: DeleteTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'exists', params: ExistsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'explain', params: ExplainParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'fieldStats', params: FieldStatsParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (request: Request, endpoint: 'get', params: GetParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'getScript', params: GetScriptParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'getSource', params: GetSourceParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'getTemplate', params: GetTemplateParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (request: Request, endpoint: 'index', params: IndexDocumentParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'info', params: InfoParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (request: Request, endpoint: 'mget', params: MGetParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'msearch', params: MSearchParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'msearchTemplate', params: MSearchTemplateParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'mtermvectors', params: MTermVectorsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'ping', params: PingParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'putScript', params: PutScriptParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'putTemplate', params: PutTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'reindex', params: ReindexParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'reindexRethrottle', params: ReindexRethrottleParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'renderSearchTemplate', params: RenderSearchTemplateParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (request: Request, endpoint: 'scroll', params: ScrollParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'search', params: SearchParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'searchShards', params: SearchShardsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'searchTemplate', params: SearchTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'suggest', params: SuggestParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'termvectors', params: TermvectorsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'update', params: UpdateDocumentParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'updateByQuery', params: UpdateDocumentByQueryParams, options?: CallClusterOptions): ReturnType; - - // cat namespace - (request: Request, endpoint: 'cat.aliases', params: CatAliasesParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.allocation', params: CatAllocationParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.count', params: CatAllocationParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.fielddata', params: CatFielddataParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.health', params: CatHealthParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.help', params: CatHelpParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.indices', params: CatIndicesParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.master', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.nodeattrs', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.nodes', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.pendingTasks', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.plugins', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.recovery', params: CatRecoveryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.repositories', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.segments', params: CatSegmentsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.shards', params: CatShardsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.snapshots', params: CatSnapshotsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.tasks', params: CatTasksParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.threadPool', params: CatThreadPoolParams, options?: CallClusterOptions): ReturnType; - - // cluster namespace - (request: Request, endpoint: 'cluster.allocationExplain', params: ClusterAllocationExplainParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.getSettings', params: ClusterGetSettingsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.health', params: ClusterHealthParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.pendingTasks', params: ClusterPendingTasksParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.putSettings', params: ClusterPutSettingsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.reroute', params: ClusterRerouteParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.state', params: ClusterStateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.stats', params: ClusterStatsParams, options?: CallClusterOptions): ReturnType; - - // indices namespace - (request: Request, endpoint: 'indices.analyze', params: IndicesAnalyzeParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.clearCache', params: IndicesClearCacheParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.close', params: IndicesCloseParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.create', params: IndicesCreateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.delete', params: IndicesDeleteParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.deleteAlias', params: IndicesDeleteAliasParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.deleteTemplate', params: IndicesDeleteTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.exists', params: IndicesExistsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.existsAlias', params: IndicesExistsAliasParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.existsTemplate', params: IndicesExistsTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.existsType', params: IndicesExistsTypeParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.flush', params: IndicesFlushParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.flushSynced', params: IndicesFlushSyncedParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.forcemerge', params: IndicesForcemergeParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.get', params: IndicesGetParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getAlias', params: IndicesGetAliasParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getFieldMapping', params: IndicesGetFieldMappingParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getMapping', params: IndicesGetMappingParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getSettings', params: IndicesGetSettingsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getTemplate', params: IndicesGetTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getUpgrade', params: IndicesGetUpgradeParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.open', params: IndicesOpenParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.putAlias', params: IndicesPutAliasParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.putMapping', params: IndicesPutMappingParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.putSettings', params: IndicesPutSettingsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.putTemplate', params: IndicesPutTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.recovery', params: IndicesRecoveryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.refresh', params: IndicesRefreshParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.rollover', params: IndicesRolloverParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.segments', params: IndicesSegmentsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.shardStores', params: IndicesShardStoresParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.shrink', params: IndicesShrinkParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.stats', params: IndicesStatsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.updateAliases', params: IndicesUpdateAliasesParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.upgrade', params: IndicesUpgradeParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.validateQuery', params: IndicesValidateQueryParams, options?: CallClusterOptions): ReturnType; - - // ingest namepsace - (request: Request, endpoint: 'ingest.deletePipeline', params: IngestDeletePipelineParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'ingest.getPipeline', params: IngestGetPipelineParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'ingest.putPipeline', params: IngestPutPipelineParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'ingest.simulate', params: IngestSimulateParams, options?: CallClusterOptions): ReturnType; - - // nodes namespace - (request: Request, endpoint: 'nodes.hotThreads', params: NodesHotThreadsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'nodes.info', params: NodesInfoParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'nodes.stats', params: NodesStatsParams, options?: CallClusterOptions): ReturnType; - - // snapshot namespace - (request: Request, endpoint: 'snapshot.create', params: SnapshotCreateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.createRepository', params: SnapshotCreateRepositoryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.delete', params: SnapshotDeleteParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.deleteRepository', params: SnapshotDeleteRepositoryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.get', params: SnapshotGetParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.getRepository', params: SnapshotGetRepositoryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.restore', params: SnapshotRestoreParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.status', params: SnapshotStatusParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.verifyRepository', params: SnapshotVerifyRepositoryParams, options?: CallClusterOptions): ReturnType; - - // tasks namespace - (request: Request, endpoint: 'tasks.cancel', params: TasksCancelParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'tasks.get', params: TasksGetParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'tasks.list', params: TasksListParams, options?: CallClusterOptions): ReturnType; - - // other APIs accessed via transport.request - ( - request: Request, - endpoint: 'transport.request', - clientParams: AssistantAPIClientParams, - options?: {} - ): Promise; - ( - request: Request, - endpoint: 'transport.request', - clientParams: DeprecationAPIClientParams, - options?: {} - ): Promise; - - // Catch-all definition - ( - request: Request, - endpoint: string, - clientParams?: any, - options?: CallClusterOptions - ): Promise; - /* eslint-enable */ -} - -export interface CallCluster { - /* eslint-disable */ - (endpoint: 'bulk', params: BulkIndexDocumentsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'clearScroll', params: ClearScrollParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'count', params: CountParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'create', params: CreateDocumentParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'delete', params: DeleteDocumentParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'deleteByQuery', params: DeleteDocumentByQueryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'deleteScript', params: DeleteScriptParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'deleteTemplate', params: DeleteTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'exists', params: ExistsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'explain', params: ExplainParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'fieldStats', params: FieldStatsParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (endpoint: 'get', params: GetParams, options?: CallClusterOptions): Promise>; - (endpoint: 'getScript', params: GetScriptParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'getSource', params: GetSourceParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'getTemplate', params: GetTemplateParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (endpoint: 'index', params: IndexDocumentParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'info', params: InfoParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (endpoint: 'mget', params: MGetParams, options?: CallClusterOptions): Promise>; - (endpoint: 'msearch', params: MSearchParams, options?: CallClusterOptions): Promise>; - (endpoint: 'msearchTemplate', params: MSearchTemplateParams, options?: CallClusterOptions): Promise>; - (endpoint: 'mtermvectors', params: MTermVectorsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'ping', params: PingParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'putScript', params: PutScriptParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'putTemplate', params: PutTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'reindex', params: ReindexParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'reindexRethrottle', params: ReindexRethrottleParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'renderSearchTemplate', params: RenderSearchTemplateParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (endpoint: 'scroll', params: ScrollParams, options?: CallClusterOptions): Promise>; - (endpoint: 'search', params: SearchParams, options?: CallClusterOptions): Promise>; - (endpoint: 'searchShards', params: SearchShardsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'searchTemplate', params: SearchTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'suggest', params: SuggestParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'termvectors', params: TermvectorsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'update', params: UpdateDocumentParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'updateByQuery', params: UpdateDocumentByQueryParams, options?: CallClusterOptions): ReturnType; - - // cat namespace - (endpoint: 'cat.aliases', params: CatAliasesParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.allocation', params: CatAllocationParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.count', params: CatAllocationParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.fielddata', params: CatFielddataParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.health', params: CatHealthParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.help', params: CatHelpParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.indices', params: CatIndicesParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.master', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.nodeattrs', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.nodes', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.pendingTasks', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.plugins', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.recovery', params: CatRecoveryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.repositories', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.segments', params: CatSegmentsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.shards', params: CatShardsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.snapshots', params: CatSnapshotsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.tasks', params: CatTasksParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.threadPool', params: CatThreadPoolParams, options?: CallClusterOptions): ReturnType; - - // cluster namespace - (endpoint: 'cluster.allocationExplain', params: ClusterAllocationExplainParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.getSettings', params: ClusterGetSettingsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.health', params: ClusterHealthParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.pendingTasks', params: ClusterPendingTasksParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.putSettings', params: ClusterPutSettingsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.reroute', params: ClusterRerouteParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.state', params: ClusterStateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.stats', params: ClusterStatsParams, options?: CallClusterOptions): ReturnType; - - // indices namespace - (endpoint: 'indices.analyze', params: IndicesAnalyzeParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.clearCache', params: IndicesClearCacheParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.close', params: IndicesCloseParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.create', params: IndicesCreateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.delete', params: IndicesDeleteParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.deleteAlias', params: IndicesDeleteAliasParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.deleteTemplate', params: IndicesDeleteTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.exists', params: IndicesExistsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.existsAlias', params: IndicesExistsAliasParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.existsTemplate', params: IndicesExistsTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.existsType', params: IndicesExistsTypeParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.flush', params: IndicesFlushParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.flushSynced', params: IndicesFlushSyncedParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.forcemerge', params: IndicesForcemergeParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.get', params: IndicesGetParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getAlias', params: IndicesGetAliasParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getFieldMapping', params: IndicesGetFieldMappingParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getMapping', params: IndicesGetMappingParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getSettings', params: IndicesGetSettingsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getTemplate', params: IndicesGetTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getUpgrade', params: IndicesGetUpgradeParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.open', params: IndicesOpenParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.putAlias', params: IndicesPutAliasParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.putMapping', params: IndicesPutMappingParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.putSettings', params: IndicesPutSettingsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.putTemplate', params: IndicesPutTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.recovery', params: IndicesRecoveryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.refresh', params: IndicesRefreshParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.rollover', params: IndicesRolloverParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.segments', params: IndicesSegmentsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.shardStores', params: IndicesShardStoresParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.shrink', params: IndicesShrinkParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.stats', params: IndicesStatsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.updateAliases', params: IndicesUpdateAliasesParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.upgrade', params: IndicesUpgradeParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.validateQuery', params: IndicesValidateQueryParams, options?: CallClusterOptions): ReturnType; - - // ingest namespace - (endpoint: 'ingest.deletePipeline', params: IngestDeletePipelineParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'ingest.getPipeline', params: IngestGetPipelineParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'ingest.putPipeline', params: IngestPutPipelineParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'ingest.simulate', params: IngestSimulateParams, options?: CallClusterOptions): ReturnType; - - // nodes namespace - (endpoint: 'nodes.hotThreads', params: NodesHotThreadsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'nodes.info', params: NodesInfoParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'nodes.stats', params: NodesStatsParams, options?: CallClusterOptions): ReturnType; - - // snapshot namespace - (endpoint: 'snapshot.create', params: SnapshotCreateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.createRepository', params: SnapshotCreateRepositoryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.delete', params: SnapshotDeleteParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.deleteRepository', params: SnapshotDeleteRepositoryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.get', params: SnapshotGetParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.getRepository', params: SnapshotGetRepositoryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.restore', params: SnapshotRestoreParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.status', params: SnapshotStatusParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.verifyRepository', params: SnapshotVerifyRepositoryParams, options?: CallClusterOptions): ReturnType; - - // tasks namespace - (endpoint: 'tasks.cancel', params: TasksCancelParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'tasks.get', params: TasksGetParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'tasks.list', params: TasksListParams, options?: CallClusterOptions): ReturnType; - - // other APIs accessed via transport.request - (endpoint: 'transport.request', clientParams: AssistantAPIClientParams, options?: {}): Promise< - AssistanceAPIResponse - >; - (endpoint: 'transport.request', clientParams: DeprecationAPIClientParams, options?: {}): Promise< - DeprecationAPIResponse - >; - - // Catch-all definition - (endpoint: string, clientParams?: any, options?: CallClusterOptions): Promise; - /* eslint-enable */ -} - -export interface ElasticsearchPlugin { - status: { on: (status: string, cb: () => void) => void }; - getCluster(name: string): Cluster; -} diff --git a/src/legacy/core_plugins/elasticsearch/index.js b/src/legacy/core_plugins/elasticsearch/index.js deleted file mode 100644 index f90f490d68035..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/index.js +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { Cluster } from './server/lib/cluster'; -import { createProxy } from './server/lib/create_proxy'; - -export default function (kibana) { - return new kibana.Plugin({ - require: [], - - async init(server) { - // All methods that ES plugin exposes are synchronous so we should get the first - // value from all observables here to be able to synchronously return and create - // cluster clients afterwards. - const { client } = server.newPlatform.setup.core.elasticsearch.legacy; - const adminCluster = new Cluster(client); - const dataCluster = new Cluster(client); - - const clusters = new Map(); - server.expose('getCluster', (name) => { - if (name === 'admin') { - return adminCluster; - } - - if (name === 'data') { - return dataCluster; - } - - return clusters.get(name); - }); - - server.events.on('stop', () => { - for (const cluster of clusters.values()) { - cluster.close(); - } - - clusters.clear(); - }); - - createProxy(server); - }, - }); -} diff --git a/src/legacy/core_plugins/elasticsearch/package.json b/src/legacy/core_plugins/elasticsearch/package.json deleted file mode 100644 index b5403e1f13de7..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "elasticsearch", - "version": "kibana", - "types": "index.d.ts" -} diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.js b/src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.js deleted file mode 100644 index 0b8786f0c2841..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { AbortController } from 'abortcontroller-polyfill/dist/cjs-ponyfill'; - -/* - * A simple utility for generating a handler that provides a signal to the handler that signals when - * the client has closed the connection on this request. - */ -export function abortableRequestHandler(fn) { - return (req, ...args) => { - const controller = new AbortController(); - req.events.once('disconnect', () => { - controller.abort(); - }); - return fn(controller.signal, req, ...args); - }; -} diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.test.js b/src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.test.js deleted file mode 100644 index d79dd4ae4e449..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.test.js +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { AbortSignal } from 'abortcontroller-polyfill/dist/cjs-ponyfill'; -import { abortableRequestHandler } from './abortable_request_handler'; - -describe('abortableRequestHandler', () => { - jest.useFakeTimers(); - - it('should call abort if disconnected', () => { - const eventHandlers = new Map(); - const mockRequest = { - events: { - once: jest.fn((key, fn) => eventHandlers.set(key, fn)), - }, - }; - - const handler = jest.fn(); - const onAborted = jest.fn(); - const abortableHandler = abortableRequestHandler(handler); - abortableHandler(mockRequest); - - const [signal, request] = handler.mock.calls[0]; - - expect(signal instanceof AbortSignal).toBe(true); - expect(request).toBe(mockRequest); - - signal.addEventListener('abort', onAborted); - - // Shouldn't be aborted or call onAborted prior to disconnecting - expect(signal.aborted).toBe(false); - expect(onAborted).not.toBeCalled(); - - expect(eventHandlers.has('disconnect')).toBe(true); - eventHandlers.get('disconnect')(); - jest.runAllTimers(); - - // Should be aborted and call onAborted after disconnecting - expect(signal.aborted).toBe(true); - expect(onAborted).toBeCalled(); - }); -}); diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/cluster.ts b/src/legacy/core_plugins/elasticsearch/server/lib/cluster.ts deleted file mode 100644 index 0e7692f6be755..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/cluster.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Request } from 'hapi'; -import { errors } from 'elasticsearch'; -import { LegacyCallAPIOptions, LegacyClusterClient, FakeRequest } from 'kibana/server'; - -export class Cluster { - public readonly errors = errors; - - constructor(private readonly clusterClient: LegacyClusterClient) {} - - public callWithRequest = async ( - req: Request | FakeRequest, - endpoint: string, - clientParams?: Record, - options?: LegacyCallAPIOptions - ) => { - return await this.clusterClient - .asScoped(req) - .callAsCurrentUser(endpoint, clientParams, options); - }; - - public callWithInternalUser = async ( - endpoint: string, - clientParams?: Record, - options?: LegacyCallAPIOptions - ) => { - return await this.clusterClient.callAsInternalUser(endpoint, clientParams, options); - }; - - public close() { - this.clusterClient.close(); - } -} diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/create_proxy.js b/src/legacy/core_plugins/elasticsearch/server/lib/create_proxy.js deleted file mode 100644 index 7302241c46939..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/create_proxy.js +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import Joi from 'joi'; -import { abortableRequestHandler } from './abortable_request_handler'; - -export function createProxy(server) { - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); - - server.route({ - method: 'POST', - path: '/elasticsearch/_msearch', - config: { - payload: { - parse: 'gunzip', - }, - }, - handler: abortableRequestHandler((signal, req, h) => { - const { query, payload } = req; - return callWithRequest( - req, - 'transport.request', - { - path: '/_msearch', - method: 'POST', - query, - body: payload.toString('utf8'), - }, - { signal } - ).finally((r) => h.response(r)); - }), - }); - - server.route({ - method: 'POST', - path: '/elasticsearch/{index}/_search', - config: { - validate: { - params: Joi.object().keys({ - index: Joi.string().required(), - }), - }, - }, - handler: abortableRequestHandler(async (signal, req) => { - const { query, payload: body } = req; - try { - return await callWithRequest( - req, - 'transport.request', - { - path: `/${encodeURIComponent(req.params.index)}/_search`, - method: 'POST', - query, - body, - }, - { signal } - ); - } catch (error) { - return JSON.parse(error.response); - } - }), - }); -} diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 8827dc53c5275..3cfda0e0696bb 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -33,7 +33,6 @@ import { import { LegacyConfig, ILegacyInternals } from '../../core/server/legacy'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { UiPlugins } from '../../core/server/plugins'; -import { ElasticsearchPlugin } from '../core_plugins/elasticsearch'; // lot of legacy code was assuming this type only had these two methods export type KibanaConfig = Pick; @@ -41,10 +40,7 @@ export type KibanaConfig = Pick; // Extend the defaults with the plugins and server methods we need. declare module 'hapi' { interface PluginProperties { - elasticsearch: ElasticsearchPlugin; - kibana: any; spaces: any; - // add new plugin types here } interface Server { diff --git a/test/api_integration/apis/elasticsearch/index.js b/test/api_integration/apis/elasticsearch/index.js deleted file mode 100644 index 5a3dc47aab9bb..0000000000000 --- a/test/api_integration/apis/elasticsearch/index.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export default function ({ getService }) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - describe('elasticsearch', () => { - before(() => esArchiver.load('elasticsearch')); - after(() => esArchiver.unload('elasticsearch')); - - it('allows search to specific index', async () => - await supertest.post('/elasticsearch/elasticsearch/_search').expect(200)); - - it('allows msearch', async () => - await supertest - .post('/elasticsearch/_msearch') - .set('content-type', 'application/x-ndjson') - .send( - '{"index":"logstash-2015.04.21","ignore_unavailable":true}\n{"size":500,"sort":{"@timestamp":"desc"},"query":{"bool":{"must":[{"query_string":{"analyze_wildcard":true,"query":"*"}},{"bool":{"must":[{"range":{"@timestamp":{"gte":1429577068175,"lte":1429577968175}}}],"must_not":[]}}],"must_not":[]}},"highlight":{"pre_tags":["@kibana-highlighted-field@"],"post_tags":["@/kibana-highlighted-field@"],"fields":{"*":{}}},"aggs":{"2":{"date_histogram":{"field":"@timestamp","interval":"30s","min_doc_count":0,"extended_bounds":{"min":1429577068175,"max":1429577968175}}}},"stored_fields":["*"],"_source": true,"script_fields":{},"docvalue_fields":["timestamp_offset","@timestamp","utc_time"]}\n' - ) - .expect(200)); - }); -} diff --git a/test/api_integration/apis/index.js b/test/api_integration/apis/index.js index bfbf873cf0616..d07c099634005 100644 --- a/test/api_integration/apis/index.js +++ b/test/api_integration/apis/index.js @@ -20,7 +20,6 @@ export default function ({ loadTestFile }) { describe('apis', () => { loadTestFile(require.resolve('./core')); - loadTestFile(require.resolve('./elasticsearch')); loadTestFile(require.resolve('./general')); loadTestFile(require.resolve('./home')); loadTestFile(require.resolve('./index_patterns')); diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index a693e008db6ea..e6f160ce8c654 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -18,7 +18,6 @@ export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirector '^src/core/(.*)': `${kibanaDirectory}/src/core/$1`, '^src/legacy/(.*)': `${kibanaDirectory}/src/legacy/$1`, '^src/plugins/(.*)': `${kibanaDirectory}/src/plugins/$1`, - '^plugins/([^/.]*)(.*)': `${kibanaDirectory}/src/legacy/core_plugins/$1/public$2`, '^legacy/plugins/xpack_main/(.*);': `${xPackKibanaDirectory}/legacy/plugins/xpack_main/public/$1`, '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': fileMockPath, '\\.module.(css|scss)$': `${kibanaDirectory}/src/dev/jest/mocks/css_module_mock.js`, diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts index 725d022879e0d..aec06a4596203 100644 --- a/x-pack/legacy/plugins/spaces/index.ts +++ b/x-pack/legacy/plugins/spaces/index.ts @@ -16,7 +16,7 @@ export const spaces = (kibana: Record) => id: 'spaces', configPrefix: 'xpack.spaces', publicDir: resolve(__dirname, 'public'), - require: ['elasticsearch', 'xpack_main'], + require: ['xpack_main'], config(Joi: any) { return Joi.object({ enabled: Joi.boolean().default(true), diff --git a/x-pack/legacy/plugins/xpack_main/index.js b/x-pack/legacy/plugins/xpack_main/index.js index 679c75a0d5942..a3bd66e744fda 100644 --- a/x-pack/legacy/plugins/xpack_main/index.js +++ b/x-pack/legacy/plugins/xpack_main/index.js @@ -13,7 +13,7 @@ export const xpackMain = (kibana) => { id: 'xpack_main', configPrefix: 'xpack.xpack_main', publicDir: resolve(__dirname, 'public'), - require: ['elasticsearch'], + require: [], config(Joi) { return Joi.object({ diff --git a/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.d.ts b/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.d.ts index 4ec5bc13eea81..3537d1bf42079 100644 --- a/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.d.ts +++ b/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.d.ts @@ -5,9 +5,9 @@ */ import { Legacy } from 'kibana'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { LegacyAPICaller } from '../../../../../../src/core/server'; -export type CallWithRequest = (...args: any[]) => CallCluster; +export type CallWithRequest = (...args: any[]) => LegacyAPICaller; export declare function callWithRequestFactory( server: Legacy.Server, diff --git a/x-pack/plugins/canvas/server/collectors/collector.ts b/x-pack/plugins/canvas/server/collectors/collector.ts index 48396d93d13e6..eb650ca5ad152 100644 --- a/x-pack/plugins/canvas/server/collectors/collector.ts +++ b/x-pack/plugins/canvas/server/collectors/collector.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { LegacyAPICaller } from 'kibana/server'; import { TelemetryCollector } from '../../types'; import { workpadCollector } from './workpad_collector'; @@ -32,7 +32,7 @@ export function registerCanvasUsageCollector( const canvasCollector = usageCollection.makeUsageCollector({ type: 'canvas', isReady: () => true, - fetch: async (callCluster: CallCluster) => { + fetch: async (callCluster: LegacyAPICaller) => { const collectorResults = await Promise.all( collectors.map((collector) => collector(kibanaIndex, callCluster)) ); diff --git a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js index 3517c958b27b8..466a7cc20a497 100644 --- a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js +++ b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js @@ -38,10 +38,6 @@ module.exports = { 'src/plugins/data/public/expressions/interpreter' ), 'kbn/interpreter': path.resolve(KIBANA_ROOT, 'packages/kbn-interpreter/target/common'), - 'types/interpreter': path.resolve( - KIBANA_ROOT, - 'src/legacy/core_plugins/interpreter/public/types' - ), tinymath: path.resolve(KIBANA_ROOT, 'node_modules/tinymath/lib/tinymath.es5.js'), core_app_image_assets: path.resolve(KIBANA_ROOT, 'src/core/public/core_app/images'), }, diff --git a/x-pack/plugins/canvas/types/telemetry.ts b/x-pack/plugins/canvas/types/telemetry.ts index 0b354d7677f6e..3b635b2e57926 100644 --- a/x-pack/plugins/canvas/types/telemetry.ts +++ b/x-pack/plugins/canvas/types/telemetry.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { LegacyAPICaller } from 'kibana/server'; /** Function for collecting information about canvas usage @@ -13,7 +13,7 @@ export type TelemetryCollector = ( /** The server instance */ kibanaIndex: string, /** Function for calling elasticsearch */ - callCluster: CallCluster + callCluster: LegacyAPICaller ) => Record; export interface TelemetryCustomElementDocument { diff --git a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts index 213bea3bc3eec..1211d4c2cf1c3 100644 --- a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts +++ b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { LegacyAPICaller } from 'kibana/server'; import { ReportingConfig } from '../'; import { ExportTypesRegistry } from '../lib/export_types_registry'; import { GetLicense } from './'; @@ -118,7 +118,7 @@ async function handleResponse(response: SearchResponse): Promise { const reportingIndex = config.get('index'); diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts index 100d09a2da7e4..8f26579726ff1 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts @@ -5,7 +5,7 @@ */ import { first, map } from 'rxjs/operators'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { LegacyAPICaller } from 'kibana/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { ReportingCore } from '../'; import { ExportTypesRegistry } from '../lib/export_types_registry'; @@ -36,7 +36,7 @@ export function getReportingUsageCollector( ) { return usageCollection.makeUsageCollector({ type: 'reporting', - fetch: (callCluster: CallCluster) => { + fetch: (callCluster: LegacyAPICaller) => { const config = reporting.getConfig(); return getReportingUsage(config, getLicense, callCluster, exportTypesRegistry); }, diff --git a/x-pack/plugins/rollup/server/collectors/register.ts b/x-pack/plugins/rollup/server/collectors/register.ts index aa06d3f696d00..daacc065629a4 100644 --- a/x-pack/plugins/rollup/server/collectors/register.ts +++ b/x-pack/plugins/rollup/server/collectors/register.ts @@ -6,7 +6,7 @@ import { get } from 'lodash'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { LegacyAPICaller } from 'kibana/server'; interface IdToFlagMap { [key: string]: boolean; @@ -27,7 +27,7 @@ function createIdToFlagMap(ids: string[]) { }, {} as any); } -async function fetchRollupIndexPatterns(kibanaIndex: string, callCluster: CallCluster) { +async function fetchRollupIndexPatterns(kibanaIndex: string, callCluster: LegacyAPICaller) { const searchParams = { size: ES_MAX_RESULT_WINDOW_DEFAULT_VALUE, index: kibanaIndex, @@ -56,7 +56,7 @@ async function fetchRollupIndexPatterns(kibanaIndex: string, callCluster: CallCl async function fetchRollupSavedSearches( kibanaIndex: string, - callCluster: CallCluster, + callCluster: LegacyAPICaller, rollupIndexPatternToFlagMap: IdToFlagMap ) { const searchParams = { @@ -104,7 +104,7 @@ async function fetchRollupSavedSearches( async function fetchRollupVisualizations( kibanaIndex: string, - callCluster: CallCluster, + callCluster: LegacyAPICaller, rollupIndexPatternToFlagMap: IdToFlagMap, rollupSavedSearchesToFlagMap: IdToFlagMap ) { @@ -211,7 +211,7 @@ export function registerRollupUsageCollector( total: { type: 'long' }, }, }, - fetch: async (callCluster: CallCluster) => { + fetch: async (callCluster: LegacyAPICaller) => { const rollupIndexPatterns = await fetchRollupIndexPatterns(kibanaIndex, callCluster); const rollupIndexPatternToFlagMap = createIdToFlagMap(rollupIndexPatterns); diff --git a/x-pack/plugins/searchprofiler/server/plugin.ts b/x-pack/plugins/searchprofiler/server/plugin.ts index 0dfb65aa6f857..032593d5e3b31 100644 --- a/x-pack/plugins/searchprofiler/server/plugin.ts +++ b/x-pack/plugins/searchprofiler/server/plugin.ts @@ -20,10 +20,9 @@ export class SearchProfilerServerPlugin implements Plugin { this.licenseStatus = { valid: false }; } - async setup({ http }: CoreSetup, { licensing, elasticsearch }: AppServerPluginDependencies) { + async setup({ http }: CoreSetup, { licensing }: AppServerPluginDependencies) { const router = http.createRouter(); profileRoute.register({ - elasticsearch, router, getLicenseStatus: () => this.licenseStatus, log: this.log, diff --git a/x-pack/plugins/searchprofiler/server/types.ts b/x-pack/plugins/searchprofiler/server/types.ts index 7aa0032afba13..84733b0ccfd95 100644 --- a/x-pack/plugins/searchprofiler/server/types.ts +++ b/x-pack/plugins/searchprofiler/server/types.ts @@ -5,18 +5,15 @@ */ import { IRouter, Logger } from 'kibana/server'; -import { ElasticsearchPlugin } from '../../../../src/legacy/core_plugins/elasticsearch'; import { LicensingPluginSetup } from '../../licensing/server'; import { LicenseStatus } from '../common'; export interface AppServerPluginDependencies { licensing: LicensingPluginSetup; - elasticsearch: ElasticsearchPlugin; } export interface RouteDependencies { getLicenseStatus: () => LicenseStatus; - elasticsearch: ElasticsearchPlugin; router: IRouter; log: Logger; } diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_xpack.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_xpack.ts index 9b69540007e5f..0ff75717c8b65 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_xpack.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_xpack.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { LegacyAPICaller } from 'kibana/server'; import { TIMEOUT } from './constants'; /** @@ -14,7 +14,7 @@ import { TIMEOUT } from './constants'; * * Like any X-Pack related API, X-Pack must installed for this to work. */ -export function getXPackUsage(callCluster: CallCluster) { +export function getXPackUsage(callCluster: LegacyAPICaller) { return callCluster('transport.request', { method: 'GET', path: '/_xpack/usage', diff --git a/x-pack/plugins/transform/server/routes/api/transforms.ts b/x-pack/plugins/transform/server/routes/api/transforms.ts index c02bc06ad6060..3d2018eb5801f 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms.ts @@ -10,8 +10,8 @@ import { RequestHandler, RequestHandlerContext, SavedObjectsClientContract, + LegacyAPICaller, } from 'kibana/server'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; import { wrapEsError } from '../../../../../legacy/server/lib/create_router/error_wrappers'; import { TRANSFORM_STATE } from '../../../common/constants'; @@ -394,7 +394,7 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { const getTransforms = async ( options: { transformId?: string }, - callAsCurrentUser: CallCluster + callAsCurrentUser: LegacyAPICaller ): Promise => { return await callAsCurrentUser('transform.getTransforms', options); }; @@ -570,7 +570,7 @@ const startTransformsHandler: RequestHandler< async function startTransforms( transformsInfo: StartTransformsRequestSchema, - callAsCurrentUser: CallCluster + callAsCurrentUser: LegacyAPICaller ) { const results: StartTransformsResponseSchema = {}; @@ -612,7 +612,7 @@ const stopTransformsHandler: RequestHandler< async function stopTransforms( transformsInfo: StopTransformsRequestSchema, - callAsCurrentUser: CallCluster + callAsCurrentUser: LegacyAPICaller ) { const results: StopTransformsResponseSchema = {}; diff --git a/x-pack/plugins/upgrade_assistant/common/types.ts b/x-pack/plugins/upgrade_assistant/common/types.ts index a29c37c9a988c..2a94b39ca6c66 100644 --- a/x-pack/plugins/upgrade_assistant/common/types.ts +++ b/x-pack/plugins/upgrade_assistant/common/types.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; import { SavedObject, SavedObjectAttributes } from 'src/core/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationInfo } from '../../../../src/core/server/elasticsearch/legacy/api_types'; export enum ReindexStep { // Enum values are spaced out by 10 to give us room to insert steps in between. diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx index b7eafb7bf5c88..66c802097055b 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx @@ -6,8 +6,8 @@ import { IconColor } from '@elastic/eui'; import { invert } from 'lodash'; - -import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationInfo } from '../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; export const LEVEL_MAP: { [level: string]: number } = { warning: 0, diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/controls.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/controls.tsx index 60db71c76357a..d75a25a95d67f 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/controls.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/controls.tsx @@ -8,8 +8,8 @@ import React, { FunctionComponent, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButton, EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiCallOut } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; - -import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationInfo } from '../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; import { GroupByOption, LevelFilterOption, LoadingState } from '../../types'; import { FilterBar } from './filter_bar'; import { GroupByBar } from './group_by_bar'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.test.tsx index f2469fe1225e3..affeeb84f35d7 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.test.tsx @@ -7,10 +7,10 @@ import { range } from 'lodash'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; - import { EuiBadge, EuiPagination } from '@elastic/eui'; -import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationInfo } from '../../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; import { EnrichedDeprecationInfo } from '../../../../../../common/types'; import { GroupByOption, LevelFilterOption } from '../../../types'; import { DeprecationAccordion, filterDeps, GroupedDeprecations } from './grouped'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.tsx index bdc7fa5e58e8a..de1a5a996d75f 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.tsx @@ -18,7 +18,8 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationInfo } from '../../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; import { EnrichedDeprecationInfo } from '../../../../../../common/types'; import { GroupByOption, LevelFilterOption } from '../../../types'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/health.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/health.tsx index 72a36d72bab6e..3ce40d0c4fdf0 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/health.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/health.tsx @@ -10,7 +10,8 @@ import React, { FunctionComponent } from 'react'; import { EuiBadge, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationInfo } from '../../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; import { COLOR_MAP, LEVEL_MAP, REVERSE_LEVEL_MAP } from '../constants'; const LocalizedLevels: { [level: string]: string } = { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.tsx index 5e11e4d0a5283..038f05aace4c3 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.tsx @@ -6,7 +6,8 @@ import React, { FunctionComponent } from 'react'; -import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationInfo } from '../../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; import { EnrichedDeprecationInfo } from '../../../../../../common/types'; import { GroupByOption } from '../../../types'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.test.tsx index 9d0c9ea9ddb56..053ef21d6b309 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.test.tsx @@ -7,7 +7,8 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; -import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationInfo } from '../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; import { LevelFilterOption } from '../../types'; import { FilterBar } from './filter_bar'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.tsx index 0dafe2105cdb7..6939c547fee57 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.tsx @@ -10,7 +10,8 @@ import React from 'react'; import { EuiFilterButton, EuiFilterGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationInfo } from '../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; import { LevelFilterOption } from '../../types'; const LocalizedOptions: { [option: string]: string } = { diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts index 6e524a98afdc6..f97a056d5cd36 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts @@ -6,7 +6,8 @@ import _ from 'lodash'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; -import { DeprecationAPIResponse } from 'src/legacy/core_plugins/elasticsearch'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationAPIResponse } from '../../../../../src/core/server/elasticsearch/legacy/api_types'; import { getUpgradeAssistantStatus } from './es_migration_apis'; import fakeDeprecations from './__fixtures__/fake_deprecations.json'; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts index abbeb8a89e12a..9f55b9d049735 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts @@ -5,7 +5,8 @@ */ import { ILegacyScopedClusterClient } from 'src/core/server'; -import { DeprecationAPIResponse } from 'src/legacy/core_plugins/elasticsearch'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationAPIResponse } from '../../../../../src/core/server/elasticsearch/legacy/api_types'; import { EnrichedDeprecationInfo, UpgradeAssistantStatus } from '../../common/types'; import { esIndicesStateCheck } from './es_indices_state_check'; diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 38851653898a8..0f5a7526d98bc 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -21,22 +21,13 @@ "paths": { "kibana/public": ["src/core/public"], "kibana/server": ["src/core/server"], - "plugins/xpack_main/*": [ - "x-pack/legacy/plugins/xpack_main/public/*" - ], - "plugins/spaces/*": [ - "x-pack/legacy/plugins/spaces/public/*" - ], - "test_utils/*": [ - "x-pack/test_utils/*" - ], - "plugins/*": ["src/legacy/core_plugins/*/public/"], - "fixtures/*": ["src/fixtures/*"], + "plugins/xpack_main/*": ["x-pack/legacy/plugins/xpack_main/public/*"], + "plugins/spaces/*": ["x-pack/legacy/plugins/spaces/public/*"], + "test_utils/*": ["x-pack/test_utils/*"], + "fixtures/*": ["src/fixtures/*"] }, // overhead is too significant - "incremental": false, + "incremental": false }, - "references": [ - { "path": "../src/core/tsconfig.json" } - ] + "references": [{ "path": "../src/core/tsconfig.json" }] } From 89ef12eaa1e98a8096d10daf72665d8f8ec688ac Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Fri, 18 Sep 2020 10:40:19 +0300 Subject: [PATCH 07/25] Visualize: Bad request when working with histogram aggregation (#77684) * Visualize: Bad request when working with histogram aggregation Closes: #77023 * :pencil2: Add some more fix context * :white_check_mark: Add test Co-authored-by: dej611 Co-authored-by: Elastic Machine --- .../lib/histogram_calculate_interval.test.ts | 13 +++++++++++++ .../buckets/lib/histogram_calculate_interval.ts | 15 +++++++-------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts index fd788d3339295..d3a95b32cd425 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts @@ -139,6 +139,19 @@ describe('calculateHistogramInterval', () => { }) ).toEqual(0.02); }); + + test('should correctly fallback to the default value for empty string', () => { + expect( + calculateHistogramInterval({ + ...params, + maxBucketsUserInput: '', + values: { + min: 0.1, + max: 0.9, + }, + }) + ).toBe(0.01); + }); }); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts index f4e42fa8881ef..378340e876296 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts @@ -27,7 +27,7 @@ interface IntervalValuesRange { export interface CalculateHistogramIntervalParams { interval: string; maxBucketsUiSettings: number; - maxBucketsUserInput?: number; + maxBucketsUserInput?: number | ''; intervalBase?: number; values?: IntervalValuesRange; } @@ -77,12 +77,7 @@ const calculateForGivenInterval = ( - The lower power of 10, times 2 - The lower power of 10, times 5 **/ -const calculateAutoInterval = ( - diff: number, - maxBucketsUiSettings: CalculateHistogramIntervalParams['maxBucketsUiSettings'], - maxBucketsUserInput: CalculateHistogramIntervalParams['maxBucketsUserInput'] -) => { - const maxBars = Math.min(maxBucketsUiSettings, maxBucketsUserInput ?? maxBucketsUiSettings); +const calculateAutoInterval = (diff: number, maxBars: number) => { const exactInterval = diff / maxBars; const lowerPower = Math.pow(10, Math.floor(Math.log10(exactInterval))); @@ -122,7 +117,11 @@ export const calculateHistogramInterval = ({ if (diff) { calculatedInterval = isAuto - ? calculateAutoInterval(diff, maxBucketsUiSettings, maxBucketsUserInput) + ? calculateAutoInterval( + diff, + // Mind maxBucketsUserInput can be an empty string, hence we need to ensure it here + Math.min(maxBucketsUiSettings, maxBucketsUserInput || maxBucketsUiSettings) + ) : calculateForGivenInterval(diff, calculatedInterval, maxBucketsUiSettings); } } From e7b0e2823742d4f5eba773a6be12ac01d4a27036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 18 Sep 2020 09:14:13 +0100 Subject: [PATCH 08/25] [APM] Track usage of Gold+ features (#77630) * adding license check * fixing api test * refactoring --- .../plugins/apm/common/custom_link/index.ts | 14 ++ .../Settings/CustomizeUI/CustomLink/index.tsx | 12 +- x-pack/plugins/apm/server/feature.ts | 47 +++++- x-pack/plugins/apm/server/plugin.ts | 12 +- .../plugins/apm/server/routes/service_map.ts | 8 +- .../routes/settings/anomaly_detection.ts | 5 + .../apm/server/routes/settings/custom_link.ts | 26 +++ .../basic/tests/feature_controls.ts | 7 - .../basic/tests/settings/custom_link.ts | 143 ++-------------- .../apm_api_integration/trial/tests/index.ts | 1 + .../trial/tests/settings/custom_link.ts | 159 ++++++++++++++++++ 11 files changed, 271 insertions(+), 163 deletions(-) create mode 100644 x-pack/plugins/apm/common/custom_link/index.ts create mode 100644 x-pack/test/apm_api_integration/trial/tests/settings/custom_link.ts diff --git a/x-pack/plugins/apm/common/custom_link/index.ts b/x-pack/plugins/apm/common/custom_link/index.ts new file mode 100644 index 0000000000000..bc0ffefd79c4d --- /dev/null +++ b/x-pack/plugins/apm/common/custom_link/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 { i18n } from '@kbn/i18n'; + +export const INVALID_LICENSE = i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.license.text', + { + defaultMessage: + "To create custom links, you must be subscribed to an Elastic Gold license or above. With it, you'll have the ability to create custom links to improve your workflow when analyzing your services.", + } +); diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx index aa34515ea460a..45a7fa2a118f2 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx @@ -7,7 +7,7 @@ import { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { isEmpty } from 'lodash'; import React, { useEffect, useState } from 'react'; -import { i18n } from '@kbn/i18n'; +import { INVALID_LICENSE } from '../../../../../../common/custom_link'; import { CustomLink } from '../../../../../../common/custom_link/custom_link_types'; import { useLicense } from '../../../../../hooks/useLicense'; import { useFetcher, FETCH_STATUS } from '../../../../../hooks/useFetcher'; @@ -94,15 +94,7 @@ export function CustomLinkOverview() { /> ) ) : ( - + )} diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index 0f6061653f352..1cda70a140c67 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -5,7 +5,12 @@ */ import { i18n } from '@kbn/i18n'; +import { LicenseType } from '../../licensing/common/types'; import { AlertType } from '../common/alert_types'; +import { + LicensingPluginSetup, + LicensingRequestHandlerContext, +} from '../../licensing/server'; export const APM_FEATURE = { id: 'apm', @@ -58,5 +63,43 @@ export const APM_FEATURE = { }, }; -export const APM_SERVICE_MAPS_FEATURE_NAME = 'APM service maps'; -export const APM_SERVICE_MAPS_LICENSE_TYPE = 'platinum'; +interface Feature { + name: string; + license: LicenseType; +} +type FeatureName = 'serviceMaps' | 'ml' | 'customLinks'; +export const features: Record = { + serviceMaps: { + name: 'APM service maps', + license: 'platinum', + }, + ml: { + name: 'APM machine learning', + license: 'platinum', + }, + customLinks: { + name: 'APM custom links', + license: 'gold', + }, +}; + +export function registerFeaturesUsage({ + licensingPlugin, +}: { + licensingPlugin: LicensingPluginSetup; +}) { + Object.values(features).forEach(({ name, license }) => { + licensingPlugin.featureUsage.register(name, license); + }); +} + +export function notifyFeatureUsage({ + licensingPlugin, + featureName, +}: { + licensingPlugin: LicensingRequestHandlerContext; + featureName: FeatureName; +}) { + const feature = features[featureName]; + licensingPlugin.featureUsage.notifyUsage(feature.name); +} diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index f25e37927f094..b417f8689b229 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -26,11 +26,7 @@ import { MlPluginSetup } from '../../ml/server'; import { ObservabilityPluginSetup } from '../../observability/server'; import { SecurityPluginSetup } from '../../security/server'; import { TaskManagerSetupContract } from '../../task_manager/server'; -import { - APM_FEATURE, - APM_SERVICE_MAPS_FEATURE_NAME, - APM_SERVICE_MAPS_LICENSE_TYPE, -} from './feature'; +import { APM_FEATURE, registerFeaturesUsage } from './feature'; import { registerApmAlerts } from './lib/alerts/register_apm_alerts'; import { createApmTelemetry } from './lib/apm_telemetry'; import { getInternalSavedObjectsClient } from './lib/helpers/get_internal_saved_objects_client'; @@ -128,10 +124,8 @@ export class APMPlugin implements Plugin { }); plugins.features.registerKibanaFeature(APM_FEATURE); - plugins.licensing.featureUsage.register( - APM_SERVICE_MAPS_FEATURE_NAME, - APM_SERVICE_MAPS_LICENSE_TYPE - ); + + registerFeaturesUsage({ licensingPlugin: plugins.licensing }); createApmApi().init(core, { config$: mergedConfig$, diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index 04807cfac1cea..1996d4d4a262d 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -15,7 +15,7 @@ import { getServiceMap } from '../lib/service_map/get_service_map'; import { getServiceMapServiceNodeInfo } from '../lib/service_map/get_service_map_service_node_info'; import { createRoute } from './create_route'; import { rangeRt, uiFiltersRt } from './default_api_types'; -import { APM_SERVICE_MAPS_FEATURE_NAME } from '../feature'; +import { notifyFeatureUsage } from '../feature'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { getParsedUiFilters } from '../lib/helpers/convert_ui_filters/get_parsed_ui_filters'; @@ -37,7 +37,11 @@ export const serviceMapRoute = createRoute(() => ({ if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(invalidLicenseMessage); } - context.licensing.featureUsage.notifyUsage(APM_SERVICE_MAPS_FEATURE_NAME); + + notifyFeatureUsage({ + licensingPlugin: context.licensing, + featureName: 'serviceMaps', + }); const logger = context.logger; const setup = await setupRequest(context, request); diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index 2cc0cdb1c2b91..f0a22356d074b 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -15,6 +15,7 @@ import { setupRequest } from '../../lib/helpers/setup_request'; import { getAllEnvironments } from '../../lib/environments/get_all_environments'; import { hasLegacyJobs } from '../../lib/anomaly_detection/has_legacy_jobs'; import { getSearchAggregatedTransactions } from '../../lib/helpers/aggregated_transactions'; +import { notifyFeatureUsage } from '../../feature'; // get ML anomaly detection jobs for each environment export const anomalyDetectionJobsRoute = createRoute(() => ({ @@ -62,6 +63,10 @@ export const createAnomalyDetectionJobsRoute = createRoute(() => ({ } await createAnomalyDetectionJobs(setup, environments, context.logger); + notifyFeatureUsage({ + licensingPlugin: context.licensing, + featureName: 'ml', + }); }, })); diff --git a/x-pack/plugins/apm/server/routes/settings/custom_link.ts b/x-pack/plugins/apm/server/routes/settings/custom_link.ts index 83c23a75e999d..7882383d78ab0 100644 --- a/x-pack/plugins/apm/server/routes/settings/custom_link.ts +++ b/x-pack/plugins/apm/server/routes/settings/custom_link.ts @@ -3,9 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import Boom from 'boom'; import * as t from 'io-ts'; import { pick } from 'lodash'; +import { INVALID_LICENSE } from '../../../common/custom_link'; +import { ILicense } from '../../../../licensing/common/types'; import { FILTER_OPTIONS } from '../../../common/custom_link/custom_link_filter_options'; +import { notifyFeatureUsage } from '../../feature'; import { setupRequest } from '../../lib/helpers/setup_request'; import { createOrUpdateCustomLink } from '../../lib/settings/custom_link/create_or_update_custom_link'; import { @@ -17,6 +22,10 @@ import { getTransaction } from '../../lib/settings/custom_link/get_transaction'; import { listCustomLinks } from '../../lib/settings/custom_link/list_custom_links'; import { createRoute } from '../create_route'; +function isActiveGoldLicense(license: ILicense) { + return license.isActive && license.hasAtLeast('gold'); +} + export const customLinkTransactionRoute = createRoute(() => ({ path: '/api/apm/settings/custom_links/transaction', params: { @@ -37,6 +46,9 @@ export const listCustomLinksRoute = createRoute(() => ({ query: filterOptionsRt, }, handler: async ({ context, request }) => { + if (!isActiveGoldLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } const setup = await setupRequest(context, request); const { query } = context.params; // picks only the items listed in FILTER_OPTIONS @@ -55,9 +67,17 @@ export const createCustomLinkRoute = createRoute(() => ({ tags: ['access:apm', 'access:apm_write'], }, handler: async ({ context, request }) => { + if (!isActiveGoldLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } const setup = await setupRequest(context, request); const customLink = context.params.body; const res = await createOrUpdateCustomLink({ customLink, setup }); + + notifyFeatureUsage({ + licensingPlugin: context.licensing, + featureName: 'customLinks', + }); return res; }, })); @@ -75,6 +95,9 @@ export const updateCustomLinkRoute = createRoute(() => ({ tags: ['access:apm', 'access:apm_write'], }, handler: async ({ context, request }) => { + if (!isActiveGoldLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } const setup = await setupRequest(context, request); const { id } = context.params.path; const customLink = context.params.body; @@ -99,6 +122,9 @@ export const deleteCustomLinkRoute = createRoute(() => ({ tags: ['access:apm', 'access:apm_write'], }, handler: async ({ context, request }) => { + if (!isActiveGoldLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } const setup = await setupRequest(context, request); const { id } = context.params.path; const res = await deleteCustomLink({ diff --git a/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts b/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts index 400d0d294bf02..e0e13b7b7fb98 100644 --- a/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts +++ b/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts @@ -149,13 +149,6 @@ export default function featureControlsTests({ getService }: FtrProviderContext) log.error(JSON.stringify(res, null, 2)); }, }, - { - req: { - url: `/api/apm/settings/custom_links`, - }, - expectForbidden: expect404, - expectResponse: expect200, - }, { req: { url: `/api/apm/settings/custom_links/transaction`, diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts b/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts index a1c647a854bf6..60b4020e73dce 100644 --- a/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts +++ b/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts @@ -3,75 +3,16 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import URL from 'url'; import expect from '@kbn/expect'; +import { expectSnapshot } from '../../../common/match_snapshot'; import { CustomLink } from '../../../../../plugins/apm/common/custom_link/custom_link_types'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function customLinksTests({ getService }: FtrProviderContext) { - const supertestRead = getService('supertestAsApmReadUser'); const supertestWrite = getService('supertestAsApmWriteUser'); - const log = getService('log'); - const esArchiver = getService('esArchiver'); - - const archiveName = 'apm_8.0.0'; - - function searchCustomLinks(filters?: any) { - const path = URL.format({ - pathname: `/api/apm/settings/custom_links`, - query: filters, - }); - return supertestRead.get(path).set('kbn-xsrf', 'foo'); - } - - async function createCustomLink(customLink: CustomLink) { - log.debug('creating configuration', customLink); - const res = await supertestWrite - .post(`/api/apm/settings/custom_links`) - .send(customLink) - .set('kbn-xsrf', 'foo'); - - throwOnError(res); - - return res; - } - - async function updateCustomLink(id: string, customLink: CustomLink) { - log.debug('updating configuration', id, customLink); - const res = await supertestWrite - .put(`/api/apm/settings/custom_links/${id}`) - .send(customLink) - .set('kbn-xsrf', 'foo'); - - throwOnError(res); - - return res; - } - - async function deleteCustomLink(id: string) { - log.debug('deleting configuration', id); - const res = await supertestWrite - .delete(`/api/apm/settings/custom_links/${id}`) - .set('kbn-xsrf', 'foo'); - - throwOnError(res); - - return res; - } - - function throwOnError(res: any) { - const { statusCode, req, body } = res; - if (statusCode !== 200) { - throw new Error(` - Endpoint: ${req.method} ${req.path} - Service: ${JSON.stringify(res.request._data.service)} - Status code: ${statusCode} - Response: ${body.message}`); - } - } describe('custom links', () => { - before(async () => { + it('is only be available to users with Gold license (or higher)', async () => { const customLink = { url: 'https://elastic.co', label: 'with filters', @@ -80,80 +21,16 @@ export default function customLinksTests({ getService }: FtrProviderContext) { { key: 'transaction.type', value: 'qux' }, ], } as CustomLink; - await createCustomLink(customLink); - }); - it('fetches a custom link', async () => { - const { status, body } = await searchCustomLinks({ - 'service.name': 'baz', - 'transaction.type': 'qux', - }); - const { label, url, filters } = body[0]; - - expect(status).to.equal(200); - expect({ label, url, filters }).to.eql({ - label: 'with filters', - url: 'https://elastic.co', - filters: [ - { key: 'service.name', value: 'baz' }, - { key: 'transaction.type', value: 'qux' }, - ], - }); - }); - it('updates a custom link', async () => { - let { status, body } = await searchCustomLinks({ - 'service.name': 'baz', - 'transaction.type': 'qux', - }); - expect(status).to.equal(200); - await updateCustomLink(body[0].id, { - label: 'foo', - url: 'https://elastic.co?service.name={{service.name}}', - filters: [ - { key: 'service.name', value: 'quz' }, - { key: 'transaction.name', value: 'bar' }, - ], - }); - ({ status, body } = await searchCustomLinks({ - 'service.name': 'quz', - 'transaction.name': 'bar', - })); - const { label, url, filters } = body[0]; - expect(status).to.equal(200); - expect({ label, url, filters }).to.eql({ - label: 'foo', - url: 'https://elastic.co?service.name={{service.name}}', - filters: [ - { key: 'service.name', value: 'quz' }, - { key: 'transaction.name', value: 'bar' }, - ], - }); - }); - it('deletes a custom link', async () => { - let { status, body } = await searchCustomLinks({ - 'service.name': 'quz', - 'transaction.name': 'bar', - }); - expect(status).to.equal(200); - await deleteCustomLink(body[0].id); - ({ status, body } = await searchCustomLinks({ - 'service.name': 'quz', - 'transaction.name': 'bar', - })); - expect(status).to.equal(200); - expect(body).to.eql([]); - }); + const response = await supertestWrite + .post(`/api/apm/settings/custom_links`) + .send(customLink) + .set('kbn-xsrf', 'foo'); - describe('transaction', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); + expect(response.status).to.be(403); - it('fetches a transaction sample', async () => { - const response = await supertestRead.get( - '/api/apm/settings/custom_links/transaction?service.name=opbeans-java' - ); - expect(response.status).to.be(200); - expect(response.body.service.name).to.eql('opbeans-java'); - }); + expectSnapshot(response.body.message).toMatchInline( + `"To create custom links, you must be subscribed to an Elastic Gold license or above. With it, you'll have the ability to create custom links to improve your workflow when analyzing your services."` + ); }); }); } diff --git a/x-pack/test/apm_api_integration/trial/tests/index.ts b/x-pack/test/apm_api_integration/trial/tests/index.ts index bf32c4661afd5..ae62253c62d81 100644 --- a/x-pack/test/apm_api_integration/trial/tests/index.ts +++ b/x-pack/test/apm_api_integration/trial/tests/index.ts @@ -19,6 +19,7 @@ export default function observabilityApiIntegrationTests({ loadTestFile }: FtrPr }); describe('Settings', function () { + loadTestFile(require.resolve('./settings/custom_link.ts')); describe('Anomaly detection', function () { loadTestFile(require.resolve('./settings/anomaly_detection/no_access_user')); loadTestFile(require.resolve('./settings/anomaly_detection/read_user')); diff --git a/x-pack/test/apm_api_integration/trial/tests/settings/custom_link.ts b/x-pack/test/apm_api_integration/trial/tests/settings/custom_link.ts new file mode 100644 index 0000000000000..bcfe8fce4b948 --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/settings/custom_link.ts @@ -0,0 +1,159 @@ +/* + * 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 URL from 'url'; +import expect from '@kbn/expect'; +import { CustomLink } from '../../../../../plugins/apm/common/custom_link/custom_link_types'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default function customLinksTests({ getService }: FtrProviderContext) { + const supertestRead = getService('supertest'); + const supertestWrite = getService('supertestAsApmWriteUser'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + + const archiveName = 'apm_8.0.0'; + + function searchCustomLinks(filters?: any) { + const path = URL.format({ + pathname: `/api/apm/settings/custom_links`, + query: filters, + }); + return supertestRead.get(path).set('kbn-xsrf', 'foo'); + } + + async function createCustomLink(customLink: CustomLink) { + log.debug('creating configuration', customLink); + const res = await supertestWrite + .post(`/api/apm/settings/custom_links`) + .send(customLink) + .set('kbn-xsrf', 'foo'); + + throwOnError(res); + + return res; + } + + async function updateCustomLink(id: string, customLink: CustomLink) { + log.debug('updating configuration', id, customLink); + const res = await supertestWrite + .put(`/api/apm/settings/custom_links/${id}`) + .send(customLink) + .set('kbn-xsrf', 'foo'); + + throwOnError(res); + + return res; + } + + async function deleteCustomLink(id: string) { + log.debug('deleting configuration', id); + const res = await supertestWrite + .delete(`/api/apm/settings/custom_links/${id}`) + .set('kbn-xsrf', 'foo'); + + throwOnError(res); + + return res; + } + + function throwOnError(res: any) { + const { statusCode, req, body } = res; + if (statusCode !== 200) { + throw new Error(` + Endpoint: ${req.method} ${req.path} + Service: ${JSON.stringify(res.request._data.service)} + Status code: ${statusCode} + Response: ${body.message}`); + } + } + + describe('custom links', () => { + before(async () => { + const customLink = { + url: 'https://elastic.co', + label: 'with filters', + filters: [ + { key: 'service.name', value: 'baz' }, + { key: 'transaction.type', value: 'qux' }, + ], + } as CustomLink; + await createCustomLink(customLink); + }); + it('fetches a custom link', async () => { + const { status, body } = await searchCustomLinks({ + 'service.name': 'baz', + 'transaction.type': 'qux', + }); + const { label, url, filters } = body[0]; + + expect(status).to.equal(200); + expect({ label, url, filters }).to.eql({ + label: 'with filters', + url: 'https://elastic.co', + filters: [ + { key: 'service.name', value: 'baz' }, + { key: 'transaction.type', value: 'qux' }, + ], + }); + }); + it('updates a custom link', async () => { + let { status, body } = await searchCustomLinks({ + 'service.name': 'baz', + 'transaction.type': 'qux', + }); + expect(status).to.equal(200); + await updateCustomLink(body[0].id, { + label: 'foo', + url: 'https://elastic.co?service.name={{service.name}}', + filters: [ + { key: 'service.name', value: 'quz' }, + { key: 'transaction.name', value: 'bar' }, + ], + }); + ({ status, body } = await searchCustomLinks({ + 'service.name': 'quz', + 'transaction.name': 'bar', + })); + const { label, url, filters } = body[0]; + expect(status).to.equal(200); + expect({ label, url, filters }).to.eql({ + label: 'foo', + url: 'https://elastic.co?service.name={{service.name}}', + filters: [ + { key: 'service.name', value: 'quz' }, + { key: 'transaction.name', value: 'bar' }, + ], + }); + }); + it('deletes a custom link', async () => { + let { status, body } = await searchCustomLinks({ + 'service.name': 'quz', + 'transaction.name': 'bar', + }); + expect(status).to.equal(200); + await deleteCustomLink(body[0].id); + ({ status, body } = await searchCustomLinks({ + 'service.name': 'quz', + 'transaction.name': 'bar', + })); + expect(status).to.equal(200); + expect(body).to.eql([]); + }); + + describe('transaction', () => { + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); + + it('fetches a transaction sample', async () => { + const response = await supertestRead.get( + '/api/apm/settings/custom_links/transaction?service.name=opbeans-java' + ); + expect(response.status).to.be(200); + expect(response.body.service.name).to.eql('opbeans-java'); + }); + }); + }); +} From c7abea0c5195fc20d181bdce7f97e7723d5acdeb Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 18 Sep 2020 09:51:18 +0100 Subject: [PATCH 09/25] fixed react warning in Suspense in alert flyout (#77777) --- .../public/application/sections/alert_form/alert_form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index c0674e6c4a5f7..6177262557e07 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -244,7 +244,7 @@ export const AlertForm = ({ ) : null} {AlertParamsExpressionComponent ? ( - + }> Date: Fri, 18 Sep 2020 09:53:35 +0100 Subject: [PATCH 10/25] [Alerting & Actions] Overwrite SOs when updating instead of partially updating (#73688) This PR changes the Alerts & Actions clients to ensure they require full updates (rather than partial) to SOs and overwrites the entire document when making the update. This is to prevent the situation where nested objects get _merged_ instead of replaced when a user makes an `update`. We also enhanced the EncryptedSavedObjectsClient to allow specified `id`s when overwriting an existing object. --- .../actions/server/actions_client.test.ts | 28 ++- .../plugins/actions/server/actions_client.ts | 32 ++- .../alerts/server/alerts_client.test.ts | 212 +++++++++++------- x-pack/plugins/alerts/server/alerts_client.ts | 10 +- .../task_runner/create_execution_handler.ts | 5 +- .../alerts/server/task_runner/task_runner.ts | 12 +- ...ypted_saved_objects_client_wrapper.test.ts | 123 ++++++++++ .../encrypted_saved_objects_client_wrapper.ts | 18 +- .../fixtures/plugins/alerts/server/routes.ts | 8 +- .../actions/builtin_action_types/webhook.ts | 71 ++++++ 10 files changed, 402 insertions(+), 117 deletions(-) diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 573fb0e1be580..adef12454f2d5 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -893,7 +893,7 @@ describe('update()', () => { }, references: [], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: 'my-action', type: 'action', attributes: { @@ -946,7 +946,7 @@ describe('update()', () => { }, references: [], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: 'my-action', type: 'action', attributes: { @@ -972,17 +972,21 @@ describe('update()', () => { name: 'my name', config: {}, }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(` Array [ "action", - "my-action", Object { "actionTypeId": "my-action-type", "config": Object {}, "name": "my name", "secrets": Object {}, }, + Object { + "id": "my-action", + "overwrite": true, + "references": Array [], + }, ] `); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); @@ -1043,7 +1047,7 @@ describe('update()', () => { }, references: [], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: 'my-action', type: 'action', attributes: { @@ -1081,11 +1085,10 @@ describe('update()', () => { c: true, }, }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(` Array [ "action", - "my-action", Object { "actionTypeId": "my-action-type", "config": Object { @@ -1096,6 +1099,11 @@ describe('update()', () => { "name": "my name", "secrets": Object {}, }, + Object { + "id": "my-action", + "overwrite": true, + "references": Array [], + }, ] `); }); @@ -1118,7 +1126,7 @@ describe('update()', () => { }, references: [], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: 'my-action', type: 'action', attributes: { diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 06c9555f3a18d..01015f9046654 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -13,6 +13,7 @@ import { } from 'src/core/server'; import { i18n } from '@kbn/i18n'; +import { omitBy, isUndefined } from 'lodash'; import { ActionTypeRegistry } from './action_type_registry'; import { validateConfig, validateSecrets, ActionExecutorContract } from './lib'; import { @@ -151,8 +152,10 @@ export class ActionsClient { 'update' ); } - const existingObject = await this.unsecuredSavedObjectsClient.get('action', id); - const { actionTypeId } = existingObject.attributes; + const { attributes, references, version } = await this.unsecuredSavedObjectsClient.get< + RawAction + >('action', id); + const { actionTypeId } = attributes; const { name, config, secrets } = action; const actionType = this.actionTypeRegistry.get(actionTypeId); const validatedActionTypeConfig = validateConfig(actionType, config); @@ -160,12 +163,25 @@ export class ActionsClient { this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); - const result = await this.unsecuredSavedObjectsClient.update('action', id, { - actionTypeId, - name, - config: validatedActionTypeConfig as SavedObjectAttributes, - secrets: validatedActionTypeSecrets as SavedObjectAttributes, - }); + const result = await this.unsecuredSavedObjectsClient.create( + 'action', + { + ...attributes, + actionTypeId, + name, + config: validatedActionTypeConfig as SavedObjectAttributes, + secrets: validatedActionTypeSecrets as SavedObjectAttributes, + }, + omitBy( + { + id, + overwrite: true, + references, + version, + }, + isUndefined + ) + ); return { id, diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index 4b5af942024c0..a6cffb0284815 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -220,7 +220,7 @@ describe('create()', () => { params: {}, ownerId: null, }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -270,27 +270,33 @@ describe('create()', () => { test('creates an alert', async () => { const data = getMockData(); + const createdAttributes = { + ...data, + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: '2019-02-12T21:01:22.479Z', + createdBy: 'elastic', + updatedBy: 'elastic', + muteAll: false, + mutedInstanceIds: [], + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }; unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - createdAt: '2019-02-12T21:01:22.479Z', - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - }, + attributes: createdAttributes, references: [ { name: 'action_0', @@ -312,11 +318,11 @@ describe('create()', () => { params: {}, ownerId: null, }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { - actions: [], + ...createdAttributes, scheduledTaskId: 'task-123', }, references: [ @@ -342,8 +348,14 @@ describe('create()', () => { }, ], "alertTypeId": "123", + "consumer": "bar", "createdAt": 2019-02-12T21:01:22.479Z, + "createdBy": "elastic", + "enabled": true, "id": "1", + "muteAll": false, + "mutedInstanceIds": Array [], + "name": "abc", "params": Object { "bar": true, }, @@ -351,7 +363,12 @@ describe('create()', () => { "interval": "10s", }, "scheduledTaskId": "task-123", + "tags": Array [ + "foo", + ], + "throttle": null, "updatedAt": 2019-02-12T21:01:22.479Z, + "updatedBy": "elastic", } `); expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); @@ -531,7 +548,7 @@ describe('create()', () => { params: {}, ownerId: null, }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -965,7 +982,7 @@ describe('create()', () => { params: {}, ownerId: null, }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1081,7 +1098,7 @@ describe('create()', () => { params: {}, ownerId: null, }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1175,6 +1192,16 @@ describe('enable()', () => { alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false, }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + enabled: true, + apiKey: null, + apiKeyOwner: null, + updatedBy: 'elastic', + }, + }); taskManager.schedule.mockResolvedValue({ id: 'task-123', scheduledAt: new Date(), @@ -1233,6 +1260,17 @@ describe('enable()', () => { }); test('enables an alert', async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + enabled: true, + apiKey: null, + apiKeyOwner: null, + updatedBy: 'elastic', + }, + }); + await alertsClient.enable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { @@ -1317,7 +1355,7 @@ describe('enable()', () => { await alertsClient.enable({ id: '1' }); expect(alertsClientParams.getUserName).not.toHaveBeenCalled(); expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); }); @@ -1384,6 +1422,7 @@ describe('enable()', () => { }); test('throws error when failing to update the first time', async () => { + unsecuredSavedObjectsClient.update.mockReset(); unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail to update')); await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( @@ -1396,6 +1435,7 @@ describe('enable()', () => { }); test('throws error when failing to update the second time', async () => { + unsecuredSavedObjectsClient.update.mockReset(); unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ ...existingAlert, attributes: { @@ -1460,6 +1500,8 @@ describe('disable()', () => { ...existingAlert.attributes, apiKey: Buffer.from('123:abc').toString('base64'), }, + version: '123', + references: [], }; beforeEach(() => { @@ -1501,13 +1543,13 @@ describe('disable()', () => { consumer: 'myApp', schedule: { interval: '10s' }, alertTypeId: 'myType', - apiKey: null, - apiKeyOwner: null, enabled: false, meta: { versionApiKeyLastmodified: kibanaVersion, }, scheduledTaskId: null, + apiKey: null, + apiKeyOwner: null, updatedBy: 'elastic', actions: [ { @@ -1544,13 +1586,13 @@ describe('disable()', () => { consumer: 'myApp', schedule: { interval: '10s' }, alertTypeId: 'myType', - apiKey: null, - apiKeyOwner: null, enabled: false, meta: { versionApiKeyLastmodified: kibanaVersion, }, scheduledTaskId: null, + apiKey: null, + apiKeyOwner: null, updatedBy: 'elastic', actions: [ { @@ -1739,6 +1781,7 @@ describe('unmuteAll()', () => { muteAll: true, }, references: [], + version: '123', }); await alertsClient.unmuteAll({ id: '1' }); @@ -1829,7 +1872,9 @@ describe('muteInstance()', () => { mutedInstanceIds: ['2'], updatedBy: 'elastic', }, - { version: '123' } + { + version: '123', + } ); }); @@ -1850,7 +1895,7 @@ describe('muteInstance()', () => { }); await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test('skips muting when alert is muted', async () => { @@ -1871,7 +1916,7 @@ describe('muteInstance()', () => { }); await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); describe('authorization', () => { @@ -1983,7 +2028,7 @@ describe('unmuteInstance()', () => { }); await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test('skips unmuting when alert is muted', async () => { @@ -2004,7 +2049,7 @@ describe('unmuteInstance()', () => { }); await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); describe('authorization', () => { @@ -3052,7 +3097,7 @@ describe('update()', () => { }); test('updates given parameters', async () => { - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -3189,11 +3234,10 @@ describe('update()', () => { namespace: 'default', }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(4); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1'); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` Object { "actions": Array [ Object { @@ -3244,8 +3288,10 @@ describe('update()', () => { "updatedBy": "elastic", } `); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` Object { + "id": "1", + "overwrite": true, "references": Array [ Object { "id": "1", @@ -3286,7 +3332,7 @@ describe('update()', () => { apiKeysEnabled: true, result: { id: '123', name: '123', api_key: 'abc' }, }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -3365,11 +3411,10 @@ describe('update()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(4); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1'); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` Object { "actions": Array [ Object { @@ -3404,18 +3449,20 @@ describe('update()', () => { "updatedBy": "elastic", } `); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` - Object { - "references": Array [ - Object { - "id": "1", - "name": "action_0", - "type": "action", - }, - ], - "version": "123", - } - `); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "id": "1", + "overwrite": true, + "references": Array [ + Object { + "id": "1", + "name": "action_0", + "type": "action", + }, + ], + "version": "123", + } + `); }); it(`doesn't call the createAPIKey function when alert is disabled`, async () => { @@ -3439,7 +3486,7 @@ describe('update()', () => { }, ], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -3519,11 +3566,10 @@ describe('update()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(4); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1'); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` Object { "actions": Array [ Object { @@ -3558,18 +3604,20 @@ describe('update()', () => { "updatedBy": "elastic", } `); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` - Object { - "references": Array [ - Object { - "id": "1", - "name": "action_0", - "type": "action", - }, - ], - "version": "123", - } - `); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "id": "1", + "overwrite": true, + "references": Array [ + Object { + "id": "1", + "name": "action_0", + "type": "action", + }, + ], + "version": "123", + } + `); }); it('should validate params', async () => { @@ -3627,7 +3675,7 @@ describe('update()', () => { }, ], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -3686,7 +3734,7 @@ describe('update()', () => { }, ], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -3765,7 +3813,7 @@ describe('update()', () => { }, ], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -3919,7 +3967,7 @@ describe('update()', () => { params: {}, ownerId: null, }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: alertId, type: 'alert', attributes: { @@ -4091,7 +4139,7 @@ describe('update()', () => { describe('authorization', () => { beforeEach(() => { - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 0a08ca848c73d..671b1d6411d7f 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -251,7 +251,7 @@ export class AlertsClient { } throw e; } - await this.unsecuredSavedObjectsClient.update('alert', createdAlert.id, { + await this.unsecuredSavedObjectsClient.update('alert', createdAlert.id, { scheduledTaskId: scheduledTask.id, }); createdAlert.attributes.scheduledTaskId = scheduledTask.id; @@ -488,9 +488,8 @@ export class AlertsClient { : null; const apiKeyAttributes = this.apiKeyAsAlertAttributes(createdAPIKey, username); - const updatedObject = await this.unsecuredSavedObjectsClient.update( + const updatedObject = await this.unsecuredSavedObjectsClient.create( 'alert', - id, this.updateMeta({ ...attributes, ...data, @@ -500,6 +499,8 @@ export class AlertsClient { updatedBy: username, }), { + id, + overwrite: true, version, references, } @@ -798,6 +799,7 @@ export class AlertsClient { 'alert', alertId ); + await this.authorization.ensureAuthorized( attributes.alertTypeId, attributes.consumer, @@ -809,7 +811,7 @@ export class AlertsClient { const mutedInstanceIds = attributes.mutedInstanceIds || []; if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) { - await this.unsecuredSavedObjectsClient.update( + await this.unsecuredSavedObjectsClient.update( 'alert', alertId, this.updateMeta({ diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts index f873b0178ece9..aca447b6adedd 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts @@ -19,6 +19,7 @@ import { AlertInstanceContext, AlertType, AlertTypeParams, + RawAlert, } from '../types'; interface CreateExecutionHandlerOptions { @@ -28,7 +29,7 @@ interface CreateExecutionHandlerOptions { actionsPlugin: ActionsPluginStartContract; actions: AlertAction[]; spaceId: string; - apiKey: string | null; + apiKey: RawAlert['apiKey']; alertType: AlertType; logger: Logger; eventLogger: IEventLogger; @@ -99,7 +100,7 @@ export function createExecutionHandler({ id: action.id, params: action.params, spaceId, - apiKey, + apiKey: apiKey ?? null, source: asSavedObjectExecutionSource({ id: alertId, type: 'alert', diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 4c16d23b485b5..5be684eca4651 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -73,7 +73,7 @@ export class TaskRunner { return apiKey; } - private getFakeKibanaRequest(spaceId: string, apiKey: string | null) { + private getFakeKibanaRequest(spaceId: string, apiKey: RawAlert['apiKey']) { const requestHeaders: Record = {}; if (apiKey) { @@ -98,7 +98,7 @@ export class TaskRunner { private getServicesWithSpaceLevelPermissions( spaceId: string, - apiKey: string | null + apiKey: RawAlert['apiKey'] ): [Services, PublicMethodsOf] { const request = this.getFakeKibanaRequest(spaceId, apiKey); return [this.context.getServices(request), this.context.getAlertsClientWithRequest(request)]; @@ -109,7 +109,7 @@ export class TaskRunner { alertName: string, tags: string[] | undefined, spaceId: string, - apiKey: string | null, + apiKey: RawAlert['apiKey'], actions: Alert['actions'], alertParams: RawAlert['params'] ) { @@ -250,7 +250,11 @@ export class TaskRunner { }; } - async validateAndExecuteAlert(services: Services, apiKey: string | null, alert: SanitizedAlert) { + async validateAndExecuteAlert( + services: Services, + apiKey: RawAlert['apiKey'], + alert: SanitizedAlert + ) { const { params: { alertId, spaceId }, } = this.taskInstance; diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts index 18834f55af0a5..e516ab1cb73b3 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts @@ -82,6 +82,58 @@ describe('#create', () => { expect(mockBaseClient.create).not.toHaveBeenCalled(); }); + it('allows a specified ID when overwriting an existing object', async () => { + const attributes = { + attrOne: 'one', + attrSecret: 'secret', + attrNotSoSecret: 'not-so-secret', + attrThree: 'three', + }; + const options = { id: 'predefined-uuid', overwrite: true, version: 'some-version' }; + const mockedResponse = { + id: 'predefined-uuid', + type: 'known-type', + attributes: { + attrOne: 'one', + attrSecret: '*secret*', + attrNotSoSecret: '*not-so-secret*', + attrThree: 'three', + }, + references: [], + }; + + mockBaseClient.create.mockResolvedValue(mockedResponse); + + expect(await wrapper.create('known-type', attributes, options)).toEqual({ + ...mockedResponse, + attributes: { attrOne: 'one', attrNotSoSecret: 'not-so-secret', attrThree: 'three' }, + }); + + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1); + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith( + { type: 'known-type', id: 'predefined-uuid' }, + { + attrOne: 'one', + attrSecret: 'secret', + attrNotSoSecret: 'not-so-secret', + attrThree: 'three', + }, + { user: mockAuthenticatedUser() } + ); + + expect(mockBaseClient.create).toHaveBeenCalledTimes(1); + expect(mockBaseClient.create).toHaveBeenCalledWith( + 'known-type', + { + attrOne: 'one', + attrSecret: '*secret*', + attrNotSoSecret: '*not-so-secret*', + attrThree: 'three', + }, + { id: 'predefined-uuid', overwrite: true, version: 'some-version' } + ); + }); + it('generates ID, encrypts attributes and strips them from response except for ones with `dangerouslyExposeValue` set to `true`', async () => { const attributes = { attrOne: 'one', @@ -262,6 +314,77 @@ describe('#bulkCreate', () => { expect(mockBaseClient.bulkCreate).not.toHaveBeenCalled(); }); + it('allows a specified ID when overwriting an existing object', async () => { + const attributes = { + attrOne: 'one', + attrSecret: 'secret', + attrNotSoSecret: 'not-so-secret', + attrThree: 'three', + }; + const mockedResponse = { + saved_objects: [ + { + id: 'predefined-uuid', + type: 'known-type', + attributes: { ...attributes, attrSecret: '*secret*', attrNotSoSecret: '*not-so-secret*' }, + references: [], + }, + { + id: 'some-id', + type: 'unknown-type', + attributes, + references: [], + }, + ], + }; + + mockBaseClient.bulkCreate.mockResolvedValue(mockedResponse); + + const bulkCreateParams = [ + { id: 'predefined-uuid', type: 'known-type', attributes, version: 'some-version' }, + { type: 'unknown-type', attributes }, + ]; + + await expect(wrapper.bulkCreate(bulkCreateParams, { overwrite: true })).resolves.toEqual({ + saved_objects: [ + { + ...mockedResponse.saved_objects[0], + attributes: { attrOne: 'one', attrNotSoSecret: 'not-so-secret', attrThree: 'three' }, + }, + mockedResponse.saved_objects[1], + ], + }); + + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1); + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith( + { type: 'known-type', id: 'predefined-uuid' }, + { + attrOne: 'one', + attrSecret: 'secret', + attrNotSoSecret: 'not-so-secret', + attrThree: 'three', + }, + { user: mockAuthenticatedUser() } + ); + + expect(mockBaseClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(mockBaseClient.bulkCreate).toHaveBeenCalledWith( + [ + { + ...bulkCreateParams[0], + attributes: { + attrOne: 'one', + attrSecret: '*secret*', + attrNotSoSecret: '*not-so-secret*', + attrThree: 'three', + }, + }, + bulkCreateParams[1], + ], + { overwrite: true } + ); + }); + it('generates ID, encrypts attributes and strips them from response except for ones with `dangerouslyExposeValue` set to `true`', async () => { const attributes = { attrOne: 'one', diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts index 0eeb9943b5be9..eef389186d670 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts @@ -68,13 +68,18 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon // Saved objects with encrypted attributes should have IDs that are hard to guess especially // since IDs are part of the AAD used during encryption, that's why we control them within this // wrapper and don't allow consumers to specify their own IDs directly. - if (options.id) { + + // only allow a specified ID if we're overwriting an existing ESO with a Version + // this helps us ensure that the document really was previously created using ESO + // and not being used to get around the specified ID limitation + const canSpecifyID = options.overwrite && options.version; + if (options.id && !canSpecifyID) { throw new Error( 'Predefined IDs are not allowed for saved objects with encrypted attributes.' ); } - const id = generateID(); + const id = options.id ?? generateID(); const namespace = getDescriptorNamespace( this.options.baseTypeRegistry, type, @@ -97,7 +102,7 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon public async bulkCreate( objects: Array>, - options?: SavedObjectsBaseOptions + options?: SavedObjectsBaseOptions & Pick ) { // We encrypt attributes for every object in parallel and that can potentially exhaust libuv or // NodeJS thread pool. If it turns out to be a problem, we can consider switching to the @@ -110,14 +115,15 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon // Saved objects with encrypted attributes should have IDs that are hard to guess especially // since IDs are part of the AAD used during encryption, that's why we control them within this - // wrapper and don't allow consumers to specify their own IDs directly. - if (object.id) { + // wrapper and don't allow consumers to specify their own IDs directly unless overwriting the original document. + const canSpecifyID = options?.overwrite && object.version; + if (object.id && !canSpecifyID) { throw new Error( 'Predefined IDs are not allowed for saved objects with encrypted attributes.' ); } - const id = generateID(); + const id = object.id ?? generateID(); const namespace = getDescriptorNamespace( this.options.baseTypeRegistry, object.type, diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts index 02d39bba42659..bd8c72f57d8f2 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts @@ -138,7 +138,13 @@ export function defineRoutes( const savedObjectsWithAlerts = await savedObjects.getScopedClient(req, { includedHiddenTypes: ['alert'], }); - const result = await savedObjectsWithAlerts.update(type, id, attributes, options); + const savedAlert = await savedObjectsWithAlerts.get(type, id); + const result = await savedObjectsWithAlerts.update( + type, + id, + { ...savedAlert.attributes, ...attributes }, + options + ); return res.ok({ body: result }); } ); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts index d82d116396cd6..ef14dd9ec2eff 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts @@ -141,6 +141,77 @@ export default function webhookTest({ getService }: FtrProviderContext) { }); }); + it('should remove headers when a webhook is updated', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'test') + .send({ + name: 'A generic Webhook action', + actionTypeId: '.webhook', + secrets: { + user: 'username', + password: 'mypassphrase', + }, + config: { + url: webhookSimulatorURL, + headers: { + someHeader: '123', + }, + }, + }) + .expect(200); + + expect(createdAction).to.eql({ + id: createdAction.id, + isPreconfigured: false, + name: 'A generic Webhook action', + actionTypeId: '.webhook', + config: { + ...defaultValues, + url: webhookSimulatorURL, + headers: { + someHeader: '123', + }, + }, + }); + + await supertest + .put(`/api/actions/action/${createdAction.id}`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'A generic Webhook action', + secrets: { + user: 'username', + password: 'mypassphrase', + }, + config: { + url: webhookSimulatorURL, + headers: { + someOtherHeader: '456', + }, + }, + }) + .expect(200); + + const { body: fetchedAction } = await supertest + .get(`/api/actions/action/${createdAction.id}`) + .expect(200); + + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + isPreconfigured: false, + name: 'A generic Webhook action', + actionTypeId: '.webhook', + config: { + ...defaultValues, + url: webhookSimulatorURL, + headers: { + someOtherHeader: '456', + }, + }, + }); + }); + it('should send authentication to the webhook target', async () => { const webhookActionId = await createWebhookAction(webhookSimulatorURL, {}, kibanaURL); const { body: result } = await supertest From 3101ca3195d214de53b8a27bc40677c4426b3b40 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 18 Sep 2020 09:54:49 +0100 Subject: [PATCH 11/25] [Alerting] renames code in alerting RBAC exemption to make it easier to maintain (#77598) Refactor of code to make it a little clearer what it's doing and improve maintenance. --- .../plugins/actions/server/actions_client.ts | 15 +++++-- .../actions_authorization.test.ts | 5 ++- .../authorization/actions_authorization.ts | 15 ++++--- ... get_authorization_mode_by_source.test.ts} | 43 +++++++++++-------- ...ts => get_authorization_mode_by_source.ts} | 28 +++++++----- .../actions/server/lib/action_executor.ts | 2 +- x-pack/plugins/actions/server/plugin.ts | 16 ++++--- .../fixtures/plugins/alerts/server/routes.ts | 2 + 8 files changed, 78 insertions(+), 48 deletions(-) rename x-pack/plugins/actions/server/authorization/{should_legacy_rbac_apply_by_source.test.ts => get_authorization_mode_by_source.test.ts} (67%) rename x-pack/plugins/actions/server/authorization/{should_legacy_rbac_apply_by_source.ts => get_authorization_mode_by_source.ts} (55%) diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 01015f9046654..4079a6ddeeb8a 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -31,7 +31,10 @@ import { } from './create_execute_function'; import { ActionsAuthorization } from './authorization/actions_authorization'; import { ActionType } from '../common'; -import { shouldLegacyRbacApplyBySource } from './authorization/should_legacy_rbac_apply_by_source'; +import { + getAuthorizationModeBySource, + AuthorizationMode, +} from './authorization/get_authorization_mode_by_source'; // We are assuming there won't be many actions. This is why we will load // all the actions in advance and assume the total count to not go over 10000. @@ -317,7 +320,10 @@ export class ActionsClient { params, source, }: Omit): Promise> { - if (!(await shouldLegacyRbacApplyBySource(this.unsecuredSavedObjectsClient, source))) { + if ( + (await getAuthorizationModeBySource(this.unsecuredSavedObjectsClient, source)) === + AuthorizationMode.RBAC + ) { await this.authorization.ensureAuthorized('execute'); } return this.actionExecutor.execute({ actionId, params, source, request: this.request }); @@ -325,7 +331,10 @@ export class ActionsClient { public async enqueueExecution(options: EnqueueExecutionOptions): Promise { const { source } = options; - if (!(await shouldLegacyRbacApplyBySource(this.unsecuredSavedObjectsClient, source))) { + if ( + (await getAuthorizationModeBySource(this.unsecuredSavedObjectsClient, source)) === + AuthorizationMode.RBAC + ) { await this.authorization.ensureAuthorized('execute'); } return this.executionEnqueuer(this.unsecuredSavedObjectsClient, options); diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts index 08c4472f8007b..a19a662f8323c 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts @@ -10,6 +10,7 @@ import { actionsAuthorizationAuditLoggerMock } from './audit_logger.mock'; import { ActionsAuthorizationAuditLogger, AuthorizationResult } from './audit_logger'; import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../saved_objects'; import { AuthenticatedUser } from '../../../security/server'; +import { AuthorizationMode } from './get_authorization_mode_by_source'; const request = {} as KibanaRequest; @@ -195,7 +196,7 @@ describe('ensureAuthorized', () => { `); }); - test('exempts users from requiring privileges to execute actions when shouldUseLegacyRbac is true', async () => { + test('exempts users from requiring privileges to execute actions when authorizationMode is Legacy', async () => { const { authorization, authentication } = mockSecurity(); const checkPrivileges: jest.MockedFunction { authorization, authentication, auditLogger, - shouldUseLegacyRbac: true, + authorizationMode: AuthorizationMode.Legacy, }); authentication.getCurrentUser.mockReturnValueOnce(({ diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.ts index bd6e355c2cf9d..cad58bed50981 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.ts @@ -9,6 +9,7 @@ import { KibanaRequest } from 'src/core/server'; import { SecurityPluginSetup } from '../../../security/server'; import { ActionsAuthorizationAuditLogger } from './audit_logger'; import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../saved_objects'; +import { AuthorizationMode } from './get_authorization_mode_by_source'; export interface ConstructorOptions { request: KibanaRequest; @@ -22,7 +23,7 @@ export interface ConstructorOptions { // actions to continue to execute - which requires that we exempt auth on // `get` for Connectors and `execute` for Action execution when used by // these legacy alerts - shouldUseLegacyRbac?: boolean; + authorizationMode?: AuthorizationMode; } const operationAlias: Record< @@ -43,20 +44,19 @@ export class ActionsAuthorization { private readonly authorization?: SecurityPluginSetup['authz']; private readonly authentication?: SecurityPluginSetup['authc']; private readonly auditLogger: ActionsAuthorizationAuditLogger; - private readonly shouldUseLegacyRbac: boolean; - + private readonly authorizationMode: AuthorizationMode; constructor({ request, authorization, authentication, auditLogger, - shouldUseLegacyRbac = false, + authorizationMode = AuthorizationMode.RBAC, }: ConstructorOptions) { this.request = request; this.authorization = authorization; this.authentication = authentication; this.auditLogger = auditLogger; - this.shouldUseLegacyRbac = shouldUseLegacyRbac; + this.authorizationMode = authorizationMode; } public async ensureAuthorized(operation: string, actionTypeId?: string) { @@ -87,6 +87,9 @@ export class ActionsAuthorization { } private isOperationExemptDueToLegacyRbac(operation: string) { - return this.shouldUseLegacyRbac && LEGACY_RBAC_EXEMPT_OPERATIONS.has(operation); + return ( + this.authorizationMode === AuthorizationMode.Legacy && + LEGACY_RBAC_EXEMPT_OPERATIONS.has(operation) + ); } } diff --git a/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.test.ts b/x-pack/plugins/actions/server/authorization/get_authorization_mode_by_source.test.ts similarity index 67% rename from x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.test.ts rename to x-pack/plugins/actions/server/authorization/get_authorization_mode_by_source.test.ts index 03062994adeb6..4980c476e60ea 100644 --- a/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.test.ts +++ b/x-pack/plugins/actions/server/authorization/get_authorization_mode_by_source.test.ts @@ -3,88 +3,93 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { shouldLegacyRbacApplyBySource } from './should_legacy_rbac_apply_by_source'; +import { + getAuthorizationModeBySource, + AuthorizationMode, +} from './get_authorization_mode_by_source'; import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import uuid from 'uuid'; import { asSavedObjectExecutionSource } from '../lib'; const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); -describe(`#shouldLegacyRbacApplyBySource`, () => { - test('should return false if no source is provided', async () => { - expect(await shouldLegacyRbacApplyBySource(unsecuredSavedObjectsClient)).toEqual(false); +describe(`#getAuthorizationModeBySource`, () => { + test('should return RBAC if no source is provided', async () => { + expect(await getAuthorizationModeBySource(unsecuredSavedObjectsClient)).toEqual( + AuthorizationMode.RBAC + ); }); - test('should return false if source is not an alert', async () => { + test('should return RBAC if source is not an alert', async () => { expect( - await shouldLegacyRbacApplyBySource( + await getAuthorizationModeBySource( unsecuredSavedObjectsClient, asSavedObjectExecutionSource({ type: 'action', id: uuid.v4(), }) ) - ).toEqual(false); + ).toEqual(AuthorizationMode.RBAC); }); - test('should return false if source alert is not marked as legacy', async () => { + test('should return RBAC if source alert is not marked as legacy', async () => { const id = uuid.v4(); unsecuredSavedObjectsClient.get.mockResolvedValue(mockAlert({ id })); expect( - await shouldLegacyRbacApplyBySource( + await getAuthorizationModeBySource( unsecuredSavedObjectsClient, asSavedObjectExecutionSource({ type: 'alert', id, }) ) - ).toEqual(false); + ).toEqual(AuthorizationMode.RBAC); }); - test('should return true if source alert is marked as legacy', async () => { + test('should return Legacy if source alert is marked as legacy', async () => { const id = uuid.v4(); unsecuredSavedObjectsClient.get.mockResolvedValue( mockAlert({ id, attributes: { meta: { versionApiKeyLastmodified: 'pre-7.10.0' } } }) ); expect( - await shouldLegacyRbacApplyBySource( + await getAuthorizationModeBySource( unsecuredSavedObjectsClient, asSavedObjectExecutionSource({ type: 'alert', id, }) ) - ).toEqual(true); + ).toEqual(AuthorizationMode.Legacy); }); - test('should return false if source alert is marked as modern', async () => { + test('should return RBAC if source alert is marked as modern', async () => { const id = uuid.v4(); unsecuredSavedObjectsClient.get.mockResolvedValue( mockAlert({ id, attributes: { meta: { versionApiKeyLastmodified: '7.10.0' } } }) ); expect( - await shouldLegacyRbacApplyBySource( + await getAuthorizationModeBySource( unsecuredSavedObjectsClient, asSavedObjectExecutionSource({ type: 'alert', id, }) ) - ).toEqual(false); + ).toEqual(AuthorizationMode.RBAC); }); - test('should return false if source alert is marked with a last modified version', async () => { + test('should return RBAC if source alert doesnt have a last modified version', async () => { const id = uuid.v4(); unsecuredSavedObjectsClient.get.mockResolvedValue(mockAlert({ id, attributes: { meta: {} } })); expect( - await shouldLegacyRbacApplyBySource( + await getAuthorizationModeBySource( unsecuredSavedObjectsClient, asSavedObjectExecutionSource({ type: 'alert', id, }) ) - ).toEqual(false); + ).toEqual(AuthorizationMode.RBAC); }); }); diff --git a/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts b/x-pack/plugins/actions/server/authorization/get_authorization_mode_by_source.ts similarity index 55% rename from x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts rename to x-pack/plugins/actions/server/authorization/get_authorization_mode_by_source.ts index 06d5776003ede..85d646c75defa 100644 --- a/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts +++ b/x-pack/plugins/actions/server/authorization/get_authorization_mode_by_source.ts @@ -10,18 +10,24 @@ import { ALERT_SAVED_OBJECT_TYPE } from '../saved_objects'; const LEGACY_VERSION = 'pre-7.10.0'; -export async function shouldLegacyRbacApplyBySource( +export enum AuthorizationMode { + Legacy, + RBAC, +} + +export async function getAuthorizationModeBySource( unsecuredSavedObjectsClient: SavedObjectsClientContract, executionSource?: ActionExecutionSource -): Promise { +): Promise { return isSavedObjectExecutionSource(executionSource) && - executionSource?.source?.type === ALERT_SAVED_OBJECT_TYPE - ? ( - await unsecuredSavedObjectsClient.get<{ - meta?: { - versionApiKeyLastmodified?: string; - }; - }>(ALERT_SAVED_OBJECT_TYPE, executionSource.source.id) - ).attributes.meta?.versionApiKeyLastmodified === LEGACY_VERSION - : false; + executionSource?.source?.type === ALERT_SAVED_OBJECT_TYPE && + ( + await unsecuredSavedObjectsClient.get<{ + meta?: { + versionApiKeyLastmodified?: string; + }; + }>(ALERT_SAVED_OBJECT_TYPE, executionSource.source.id) + ).attributes.meta?.versionApiKeyLastmodified === LEGACY_VERSION + ? AuthorizationMode.Legacy + : AuthorizationMode.RBAC; } diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index a607dc0de0bda..73434d5c1eaa2 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -27,7 +27,7 @@ export interface ActionExecutorContext { getServices: GetServicesFunction; getActionsClientWithRequest: ( request: KibanaRequest, - executionSource?: ActionExecutionSource + authorizationContext?: ActionExecutionSource ) => Promise>; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; actionTypeRegistry: ActionTypeRegistryContract; diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 97cefafad4385..dca1114f0ae44 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -71,7 +71,10 @@ import { ACTIONS_FEATURE } from './feature'; import { ActionsAuthorization } from './authorization/actions_authorization'; import { ActionsAuthorizationAuditLogger } from './authorization/audit_logger'; import { ActionExecutionSource } from './lib/action_execution_source'; -import { shouldLegacyRbacApplyBySource } from './authorization/should_legacy_rbac_apply_by_source'; +import { + getAuthorizationModeBySource, + AuthorizationMode, +} from './authorization/get_authorization_mode_by_source'; const EVENT_LOG_PROVIDER = 'actions'; export const EVENT_LOG_ACTIONS = { @@ -281,7 +284,7 @@ export class ActionsPlugin implements Plugin, Plugi const getActionsClientWithRequest = async ( request: KibanaRequest, - source?: ActionExecutionSource + authorizationContext?: ActionExecutionSource ) => { if (isESOUsingEphemeralEncryptionKey === true) { throw new Error( @@ -303,7 +306,7 @@ export class ActionsPlugin implements Plugin, Plugi request, authorization: instantiateAuthorization( request, - await shouldLegacyRbacApplyBySource(unsecuredSavedObjectsClient, source) + await getAuthorizationModeBySource(unsecuredSavedObjectsClient, authorizationContext) ), actionExecutor: actionExecutor!, executionEnqueuer: createExecutionEnqueuerFunction({ @@ -316,7 +319,8 @@ export class ActionsPlugin implements Plugin, Plugi }; // Ensure the public API cannot be used to circumvent authorization - // using our legacy exemption mechanism + // using our legacy exemption mechanism by passing in a legacy SO + // as authorizationContext which would then set a Legacy AuthorizationMode const secureGetActionsClientWithRequest = (request: KibanaRequest) => getActionsClientWithRequest(request); @@ -389,11 +393,11 @@ export class ActionsPlugin implements Plugin, Plugi private instantiateAuthorization = ( request: KibanaRequest, - shouldUseLegacyRbac: boolean = false + authorizationMode?: AuthorizationMode ) => { return new ActionsAuthorization({ request, - shouldUseLegacyRbac, + authorizationMode, authorization: this.security?.authz, authentication: this.security?.authc, auditLogger: new ActionsAuthorizationAuditLogger( diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts index bd8c72f57d8f2..79fc8470a8ec2 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts @@ -50,6 +50,8 @@ export function defineRoutes( includedHiddenTypes: ['alert'], }); const savedObjectsWithAlerts = await savedObjects.getScopedClient(req, { + // Exclude the security and spaces wrappers to get around the safeguards those have in place to prevent + // us from doing what we want to do - brute force replace the ApiKey excludedWrappers: ['security', 'spaces'], includedHiddenTypes: ['alert'], }); From 9a0dfcff0488220979fdbac46c080c41c98fb8f8 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Fri, 18 Sep 2020 11:08:10 +0200 Subject: [PATCH 12/25] Re-enable session lifespan, idle timeout api integration tests and use unique names for the security test reports. (#77746) --- x-pack/test/api_integration/config_security_basic.ts | 1 + x-pack/test/api_integration/config_security_trial.ts | 1 + x-pack/test/functional/config_security_basic.ts | 2 +- .../test/security_api_integration/session_idle.config.ts | 2 +- .../security_api_integration/session_lifespan.config.ts | 2 +- .../tests/session_idle/cleanup.ts | 8 ++------ .../tests/session_lifespan/cleanup.ts | 8 ++------ x-pack/test/security_functional/login_selector.config.ts | 2 +- x-pack/test/security_functional/oidc.config.ts | 2 +- x-pack/test/security_functional/saml.config.ts | 2 +- 10 files changed, 12 insertions(+), 18 deletions(-) diff --git a/x-pack/test/api_integration/config_security_basic.ts b/x-pack/test/api_integration/config_security_basic.ts index 8489940505686..237d162e80328 100644 --- a/x-pack/test/api_integration/config_security_basic.ts +++ b/x-pack/test/api_integration/config_security_basic.ts @@ -19,6 +19,7 @@ export default async function (context: FtrConfigProviderContext) { 'xpack.security.authc.api_key.enabled=true', ]; config.testFiles = [require.resolve('./apis/security/security_basic')]; + config.junit.reportName = 'X-Pack API Integration Tests (Security Basic)'; return config; }); } diff --git a/x-pack/test/api_integration/config_security_trial.ts b/x-pack/test/api_integration/config_security_trial.ts index 4c1e2913b987c..839165d8618cf 100644 --- a/x-pack/test/api_integration/config_security_trial.ts +++ b/x-pack/test/api_integration/config_security_trial.ts @@ -12,6 +12,7 @@ import { default as createTestConfig } from './config'; export default async function (context: FtrConfigProviderContext) { return createTestConfig(context).then((config) => { config.testFiles = [require.resolve('./apis/security/security_trial')]; + config.junit.reportName = 'X-Pack API Integration Tests (Security Trial)'; return config; }); } diff --git a/x-pack/test/functional/config_security_basic.ts b/x-pack/test/functional/config_security_basic.ts index 48c397d9c37de..968281b75b7ac 100644 --- a/x-pack/test/functional/config_security_basic.ts +++ b/x-pack/test/functional/config_security_basic.ts @@ -70,7 +70,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { }, junit: { - reportName: 'Chrome X-Pack UI Functional Tests', + reportName: 'Chrome X-Pack UI Functional Tests (Security Basic)', }, }; } diff --git a/x-pack/test/security_api_integration/session_idle.config.ts b/x-pack/test/security_api_integration/session_idle.config.ts index da85c6342037e..34a23b7f5f926 100644 --- a/x-pack/test/security_api_integration/session_idle.config.ts +++ b/x-pack/test/security_api_integration/session_idle.config.ts @@ -36,7 +36,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { }, junit: { - reportName: 'X-Pack Security API Integration Tests', + reportName: 'X-Pack Security API Integration Tests (Session Idle Timeout)', }, }; } diff --git a/x-pack/test/security_api_integration/session_lifespan.config.ts b/x-pack/test/security_api_integration/session_lifespan.config.ts index 17773a7739847..b5fdf6b6914b1 100644 --- a/x-pack/test/security_api_integration/session_lifespan.config.ts +++ b/x-pack/test/security_api_integration/session_lifespan.config.ts @@ -36,7 +36,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { }, junit: { - reportName: 'X-Pack Security API Integration Tests', + reportName: 'X-Pack Security API Integration Tests (Session Lifespan)', }, }; } diff --git a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts index f288bc925123e..01e2ad76fb3d2 100644 --- a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts +++ b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts @@ -33,15 +33,11 @@ export default function ({ getService }: FtrProviderContext) { return (await es.search({ index: '.kibana_security_session*' })).hits.total.value; } - // FLAKY: https://github.com/elastic/kibana/issues/76239 - describe.skip('Session Idle cleanup', () => { + describe('Session Idle cleanup', () => { beforeEach(async () => { await es.cluster.health({ index: '.kibana_security_session*', waitForStatus: 'green' }); - await es.deleteByQuery({ + await es.indices.delete({ index: '.kibana_security_session*', - q: '*', - waitForCompletion: true, - refresh: true, ignore: [404], }); }); diff --git a/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts b/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts index dbdaf494fdf27..6036acf3d1cf1 100644 --- a/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts +++ b/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts @@ -30,15 +30,11 @@ export default function ({ getService }: FtrProviderContext) { return (await es.search({ index: '.kibana_security_session*' })).hits.total.value; } - // FLAKY: https://github.com/elastic/kibana/issues/76223 - describe.skip('Session Lifespan cleanup', () => { + describe('Session Lifespan cleanup', () => { beforeEach(async () => { await es.cluster.health({ index: '.kibana_security_session*', waitForStatus: 'green' }); - await es.deleteByQuery({ + await es.indices.delete({ index: '.kibana_security_session*', - q: '*', - waitForCompletion: true, - refresh: true, ignore: [404], }); }); diff --git a/x-pack/test/security_functional/login_selector.config.ts b/x-pack/test/security_functional/login_selector.config.ts index 48665c93c091a..bdb4778740503 100644 --- a/x-pack/test/security_functional/login_selector.config.ts +++ b/x-pack/test/security_functional/login_selector.config.ts @@ -76,7 +76,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { screenshots: { directory: resolve(__dirname, 'screenshots') }, junit: { - reportName: 'Chrome X-Pack Security Functional Tests', + reportName: 'Chrome X-Pack Security Functional Tests (Login Selector)', }, }; } diff --git a/x-pack/test/security_functional/oidc.config.ts b/x-pack/test/security_functional/oidc.config.ts index 5fd59e049a0f4..1ed5d51098420 100644 --- a/x-pack/test/security_functional/oidc.config.ts +++ b/x-pack/test/security_functional/oidc.config.ts @@ -76,7 +76,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { screenshots: { directory: resolve(__dirname, 'screenshots') }, junit: { - reportName: 'Chrome X-Pack Security Functional Tests', + reportName: 'Chrome X-Pack Security Functional Tests (OpenID Connect)', }, }; } diff --git a/x-pack/test/security_functional/saml.config.ts b/x-pack/test/security_functional/saml.config.ts index c47145f8bc039..9d925bee480a8 100644 --- a/x-pack/test/security_functional/saml.config.ts +++ b/x-pack/test/security_functional/saml.config.ts @@ -70,7 +70,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { screenshots: { directory: resolve(__dirname, 'screenshots') }, junit: { - reportName: 'Chrome X-Pack Security Functional Tests', + reportName: 'Chrome X-Pack Security Functional Tests (SAML)', }, }; } From 827ee5c4066a20eeff53c0d05984d1c43a22c747 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 18 Sep 2020 11:10:42 +0200 Subject: [PATCH 13/25] [Drilldowns] Beta badge support. Mark URL Drilldown as Beta (#75654) Team agreed that we'd like to release URL drilldown MVP as Beta. Reasons for it: 1. Caveats in current URL drilldown UX (dummy values in preview, too vague triggers, {{event.points}} hack). It might that improving those would require a breaking change in an API. We will do our best to handle changes with migrations, but there could be edge cases we won't be able to cover. 2. We decided not to rush with extending url templating capabilities with more helpers. We could find out from early feedback that essential helpers are required. Even though this won't be breaking, worth mentioning here. 3. Since URL drilldown is a new feature and relies on user's input, we might get early feedback that would required us for a significant changes in the feature. Make it Beta gives us more room for a pivot in this case. 4. API Action concept might change how we reason about URL drilldown Co-authored-by: Elastic Machine --- docs/user/dashboard/drilldowns.asciidoc | 1 + docs/user/dashboard/url-drilldown.asciidoc | 2 ++ .../drilldowns/url_drilldown/url_drilldown.tsx | 1 + .../action_wizard/action_wizard.test.tsx | 15 +++++++++++++++ .../components/action_wizard/action_wizard.tsx | 17 ++++++++++++++++- .../public/components/action_wizard/i18n.ts | 14 ++++++++++++++ .../public/drilldowns/drilldown_definition.ts | 6 ++++++ .../url_drilldown_collect_config/i18n.ts | 3 ++- .../dynamic_actions/action_factory.test.ts | 12 ++++++++++++ .../public/dynamic_actions/action_factory.ts | 1 + .../action_factory_definition.ts | 6 ++++++ .../services/ui_actions_service_enhancements.ts | 2 ++ 12 files changed, 78 insertions(+), 2 deletions(-) diff --git a/docs/user/dashboard/drilldowns.asciidoc b/docs/user/dashboard/drilldowns.asciidoc index 85230f1b6f70d..e3d0e16630c5c 100644 --- a/docs/user/dashboard/drilldowns.asciidoc +++ b/docs/user/dashboard/drilldowns.asciidoc @@ -1,3 +1,4 @@ +[role="xpack"] [[drilldowns]] == Use drilldowns for dashboard actions diff --git a/docs/user/dashboard/url-drilldown.asciidoc b/docs/user/dashboard/url-drilldown.asciidoc index 4919625340da2..e6daf89d72718 100644 --- a/docs/user/dashboard/url-drilldown.asciidoc +++ b/docs/user/dashboard/url-drilldown.asciidoc @@ -1,6 +1,8 @@ [[url-drilldown]] === URL drilldown +beta[] + The URL drilldown allows you to navigate from a dashboard to an internal or external URL. The destination URL can be dynamic, depending on the dashboard context or user’s interaction with a visualization. diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx index 04f60662d88a3..85e92d0827daa 100644 --- a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx +++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx @@ -51,6 +51,7 @@ export class UrlDrilldown implements Drilldown txtUrlDrilldownDisplayName; diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx index 9cc64defc1795..fcea8caf9090e 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx @@ -82,3 +82,18 @@ test('If not enough license, button is disabled', () => { expect(screen.getByText(/Go to URL/i)).toBeDisabled(); }); + +test('if action is beta, beta badge is shown', () => { + const betaUrl = new ActionFactory( + { + ...urlDrilldownActionFactory, + isBeta: true, + }, + { + getLicense: () => licensingMock.createLicense(), + getFeatureUsageStart: () => licensingMock.createStart().featureUsage, + } + ); + const screen = render(); + expect(screen.getByText(/Beta/i)).toBeVisible(); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx index 16d0250c5721e..a3f6cac3ba1b4 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx @@ -19,9 +19,12 @@ import { EuiTextColor, EuiTitle, EuiLink, + EuiBetaBadge, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { + txtBetaActionFactoryLabel, + txtBetaActionFactoryTooltip, txtChangeButton, txtTriggerPickerHelpText, txtTriggerPickerLabel, @@ -255,7 +258,15 @@ const SelectedActionFactory: React.FC = ({ )} -

{actionFactory.getDisplayName(context)}

+

+ {actionFactory.getDisplayName(context)}{' '} + {actionFactory.isBeta && ( + + )} +

{showDeselect && ( @@ -350,6 +361,10 @@ const ActionFactorySelector: React.FC = ({ data-test-subj={`${TEST_SUBJ_ACTION_FACTORY_ITEM}-${actionFactory.id}`} onClick={() => onActionFactorySelected(actionFactory)} disabled={!actionFactory.isCompatibleLicense()} + betaBadgeLabel={actionFactory.isBeta ? txtBetaActionFactoryLabel : undefined} + betaBadgeTooltipContent={ + actionFactory.isBeta ? txtBetaActionFactoryTooltip : undefined + } > {actionFactory.getIconType(context) && ( diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts index f494ecfb51f32..43a3bd01daf37 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts @@ -33,3 +33,17 @@ export const txtTriggerPickerHelpTooltip = i18n.translate( defaultMessage: 'Determines when the drilldown appears in context menu', } ); + +export const txtBetaActionFactoryLabel = i18n.translate( + 'xpack.uiActionsEnhanced.components.actionWizard.betaActionLabel', + { + defaultMessage: `Beta`, + } +); + +export const txtBetaActionFactoryTooltip = i18n.translate( + 'xpack.uiActionsEnhanced.components.actionWizard.betaActionTooltip', + { + defaultMessage: `This action is in beta and is subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features. Please help us by reporting any bugs or providing other feedback.`, + } +); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts index 8faccc088a327..f5e565d4090ff 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts @@ -36,6 +36,12 @@ export interface DrilldownDefinition< */ id: string; + /** + * Is this action factory not GA? + * Adds a beta badge on a list item representing this ActionFactory + */ + readonly isBeta?: boolean; + /** * Minimal license level * Empty means no restrictions diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts index 78f7218dce22e..500ef21b61dc4 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts @@ -19,7 +19,8 @@ export const txtUrlTemplatePlaceholder = i18n.translate( export const txtUrlPreviewHelpText = i18n.translate( 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewHelpText', { - defaultMessage: 'Please note that \\{\\{event.*\\}\\} variables replaced by dummy values.', + defaultMessage: + 'Please note that in preview \\{\\{event.*\\}\\} variables are substituted with dummy values.', } ); diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts index 032a4a63fe2e9..66a876bdbab85 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts @@ -114,3 +114,15 @@ describe('License & ActionFactory', () => { }); }); }); + +describe('isBeta', () => { + test('false by default', async () => { + const factory = createActionFactory(); + expect(factory.isBeta).toBe(false); + }); + + test('true', async () => { + const factory = createActionFactory({ isBeta: true }); + expect(factory.isBeta).toBe(true); + }); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts index 35a82adf9896d..3ad6d4ee39749 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts @@ -46,6 +46,7 @@ export class ActionFactory< } public readonly id = this.def.id; + public readonly isBeta = this.def.isBeta ?? false; public readonly minimalLicense = this.def.minimalLicense; public readonly licenseFeatureName = this.def.licenseFeatureName; public readonly order = this.def.order || 0; diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts index 91b8c8ec1e5ef..7ec6b21485747 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts @@ -46,6 +46,12 @@ export interface ActionFactoryDefinition< */ licenseFeatureName?: string; + /** + * Is this action factory not GA? + * Adds a beta badge on a list item representing this ActionFactory + */ + readonly isBeta?: boolean; + /** * This method should return a definition of a new action, normally used to * register it in `ui_actions` registry. diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts index b8086c16f5e71..ab0aa1200f5a7 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts @@ -89,6 +89,7 @@ export class UiActionsServiceEnhancements { ExecutionContext extends TriggerContextMapping[SupportedTriggers] = TriggerContextMapping[SupportedTriggers] >({ id: factoryId, + isBeta, order, CollectConfig, createConfig, @@ -109,6 +110,7 @@ export class UiActionsServiceEnhancements { ExecutionContext > = { id: factoryId, + isBeta, minimalLicense, licenseFeatureName, order, From b9958babf3c7b920ddfcd1e1d5e5ea2d53f01a30 Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Fri, 18 Sep 2020 11:05:31 +0100 Subject: [PATCH 14/25] [ML] Transforms: Fixes styling of preview grid pagination in summary step (#77789) --- .../step_define/step_define_summary.tsx | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx index fa4f8a7e09690..c4adb9f1f49de 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx @@ -8,7 +8,7 @@ import React, { Fragment, FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiCodeBlock, EuiForm, EuiFormRow, EuiSpacer, EuiText } from '@elastic/eui'; +import { EuiCodeBlock, EuiForm, EuiFormRow, EuiSpacer } from '@elastic/eui'; import { dictionaryToArray } from '../../../../../../common/types/common'; @@ -132,23 +132,21 @@ export const StepDefineSummary: FC = ({ - - - + ); From 63bb3bf309cb32746ffbb733faf60b12cc8d9b14 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Fri, 18 Sep 2020 12:14:49 +0200 Subject: [PATCH 15/25] [ILM] Data tiers for 7.10 (#76126) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * Revert "wip" This reverts commit 54b6f7ff3ec8b0f57b150ab2276d617686da9fb5. * Revert "Revert "wip"" This reverts commit 63868b44ec60d7431c3a0189b16aeece1db2d38e. * Refactor to using EUI button group component - also moved node attr and node allocation component to inside new folder that contains all allocation components. - only focussed on updating warm phase for now * WIP: moved form UX more in line with EUI - The described form group now has a switch for showing controls on the left. - Refactored DataTierAllocation to CustomDataTierAllocation - Removed 'node-roles' option - Updated copy - Moved JSX around a bit in the edit policy section to make logic simpler * Refactor UI to reflect custom-ness of "Custom" and "None" options - Still only implemented for warm, cold and frozen are still under way * server side changes for getting node data * double opt-in * Refactored data tier allocation type - Made types more explicit 'default', 'custom' and 'none' - Fixed issue introduced by use useCallback on state setter - need to use the function setter pattern to not have stale data being set. * Some refacoring, but main point is to add warning detection for node roles. - Refactored way we get node data to a provider component so that phases still have flexibility in how they render. Currently this also means that we fetch node stats data for each phase we render - Created a callout for when there is no node role to which data can be allocated for the default setting - Also updated the behaviour to render the entire form even when we cannot fetch node data for some reason. It is not ideal to not have node data, but we should not block the entire form. * fix i18n * fix type issue with deafult policies missing allocation type * remove "undefined" as option for setting phase data * Create referentially stable data setter for all phases - prevent infini-update * fix type issue * refactor data -> nodesData * refactor cold phase for data tiers * refactor frozen phase for data tiers * fixed existing tests for warm section * restored existing test coverage for cold phase and added test coverage for frozen * fix api integration test * remove unused translations * slight UX update to turning on custom attribute allocation * added scss file for data tier advanced section and other style updates * added tests for new warning * fix types * added correct copy for cold and frozen phases * fix types and i18n * implement copy feedback * added spacer after the enable data tier allocation switch * refactor to super select * fix replicas copy * update phase serialization for cold and frozen * Refactor so that logic determining warnings lives together - also factor out the warning of the node allocation component - revisit copy for the allocation warning * tier -> phase * Added some much needed policy serialization test coverage - also factored out policy allocation action serialization * fix import paths and added required file header * fix existing test coverage * refine copy for data tier allocation recommended option * fix showing warning for no node attrs * fix inverted warning logic 🤦🏼‍♂️ * fix typing * implement CJs copy feedback * fix i18n * remove unused or invalid translations * provide ability to not alter original policy * do not alter the original policy in the serilalization process * fix jest tests * Remove duplicate type and refactor NodeRole to NodeDataRole Also deleted unused component "AdvancedSection" for now * added comment to "false" typing * revised and refactored copy based on feedback * address copy feedback * update kibana schema to allow migrate: { enabled: false } Co-authored-by: Elastic Machine --- .../__jest__/components/edit_policy.test.tsx | 203 +++++++- .../components/helpers/edit_policy.ts | 30 ++ .../components/helpers/http_requests.ts | 2 + .../__jest__/components/helpers/index.ts | 11 + .../common/types/api.ts | 12 + .../common/types/index.ts | 2 + .../common/types/policies.ts | 26 + .../public/application/constants/policy.ts | 3 + .../data_tiers/check_phase_compatibility.ts | 36 ++ .../data_tiers/determine_allocation_type.ts | 34 ++ .../application/lib/data_tiers/index.ts | 9 + .../public/application/lib/index.ts | 7 + .../data_tier_allocation.scss | 9 + .../data_tier_allocation.tsx | 194 ++++++++ .../default_allocation_warning.tsx | 72 +++ .../components/data_tier_allocation/index.ts | 12 + .../no_node_attributes_warning.tsx | 62 +++ .../data_tier_allocation/node_allocation.tsx | 121 +++++ .../node_attrs_details.tsx | 2 +- .../node_data_provider.tsx | 70 +++ .../components/data_tier_allocation/types.ts | 22 + .../components/described_form_field.tsx | 26 + .../sections/edit_policy/components/index.ts | 10 +- .../components/node_allocation.tsx | 189 ------- .../components/toggleable_field.tsx | 38 ++ .../sections/edit_policy/edit_policy.tsx | 56 ++- .../edit_policy/phases/cold_phase.tsx | 202 ++++---- .../edit_policy/phases/frozen_phase.tsx | 196 ++++---- .../shared/data_tier_allocation_field.tsx | 88 ++++ .../edit_policy/phases/shared/index.ts | 7 + .../edit_policy/phases/warm_phase.tsx | 182 ++++--- .../public/application/services/api.ts | 6 +- .../services/policies/cold_phase.ts | 23 +- .../services/policies/frozen_phase.ts | 23 +- .../policies/policy_serialization.test.ts | 464 ++++++++++++++++++ .../services/policies/shared/index.ts | 7 + .../shared/serialize_phase_with_allocation.ts | 40 ++ .../services/policies/warm_phase.ts | 24 +- .../routes/api/nodes/register_list_route.ts | 51 +- .../api/policies/register_create_route.ts | 5 + .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - .../index_lifecycle_management/nodes.js | 2 +- 43 files changed, 2028 insertions(+), 558 deletions(-) create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/edit_policy.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/index.ts create mode 100644 x-pack/plugins/index_lifecycle_management/common/types/api.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/check_phase_compatibility.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/determine_allocation_type.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/index.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/lib/index.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.scss create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/default_allocation_warning.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/index.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/no_node_attributes_warning.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_allocation.tsx rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/{ => data_tier_allocation}/node_attrs_details.tsx (97%) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_data_provider.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/types.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_field.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/toggleable_field.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/data_tier_allocation_field.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/index.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/index.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/serialize_phase_with_allocation.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx index 3867c30655379..b0df3723ca77e 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx @@ -14,8 +14,8 @@ import { SinonFakeServer } from 'sinon'; import { ReactWrapper } from 'enzyme'; import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; +import { createMemoryHistory } from 'history'; -import { init as initHttpRequests } from './helpers/http_requests'; import { notificationServiceMock, fatalErrorsServiceMock, @@ -41,8 +41,7 @@ import { policyNameAlreadyUsedErrorMessage, maximumDocumentsRequiredMessage, } from '../../public/application/services/policies/policy_validation'; -import { HttpResponse } from './helpers/http_requests'; -import { createMemoryHistory } from 'history'; +import { editPolicyHelpers } from './helpers'; // @ts-ignore initHttp(axios.create({ adapter: axiosXhrAdapter })); @@ -54,11 +53,8 @@ initNotification( const history = createMemoryHistory(); let server: SinonFakeServer; -let httpRequestsMockHelpers: { - setPoliciesResponse: (response: HttpResponse) => void; - setNodesListResponse: (response: HttpResponse) => void; - setNodesDetailsResponse: (nodeAttributes: string, response: HttpResponse) => void; -}; +let httpRequestsMockHelpers: editPolicyHelpers.EditPolicySetup['http']['httpRequestsMockHelpers']; +let http: editPolicyHelpers.EditPolicySetup['http']; const policy = { phases: { hot: { @@ -94,6 +90,17 @@ const activatePhase = async (rendered: ReactWrapper, phase: string) => { }); rendered.update(); }; +const openNodeAttributesSection = (rendered: ReactWrapper, phase: string) => { + const getControls = () => findTestSubject(rendered, `${phase}-dataTierAllocationControls`); + act(() => { + findTestSubject(getControls(), 'dataTierSelect').simulate('click'); + }); + rendered.update(); + act(() => { + findTestSubject(getControls(), 'customDataAllocationOption').simulate('click'); + }); + rendered.update(); +}; const expectedErrorMessages = (rendered: ReactWrapper, expectedMessages: string[]) => { const errorMessages = rendered.find('.euiFormErrorText'); expect(errorMessages.length).toBe(expectedMessages.length); @@ -119,12 +126,16 @@ const setPolicyName = (rendered: ReactWrapper, policyName: string) => { policyNameField.simulate('change', { target: { value: policyName } }); rendered.update(); }; -const setPhaseAfter = (rendered: ReactWrapper, phase: string, after: string) => { +const setPhaseAfter = (rendered: ReactWrapper, phase: string, after: string | number) => { const afterInput = rendered.find(`input#${phase}-selectedMinimumAge`); afterInput.simulate('change', { target: { value: after } }); rendered.update(); }; -const setPhaseIndexPriority = (rendered: ReactWrapper, phase: string, priority: string) => { +const setPhaseIndexPriority = ( + rendered: ReactWrapper, + phase: string, + priority: string | number +) => { const priorityInput = rendered.find(`input#${phase}-phaseIndexPriority`); priorityInput.simulate('change', { target: { value: priority } }); rendered.update(); @@ -139,7 +150,9 @@ describe('edit policy', () => { component = ( ); - ({ server, httpRequestsMockHelpers } = initHttpRequests()); + + ({ http } = editPolicyHelpers.setup()); + ({ server, httpRequestsMockHelpers } = http); httpRequestsMockHelpers.setPoliciesResponse(policies); }); @@ -321,7 +334,7 @@ describe('edit policy', () => { describe('warm phase', () => { beforeEach(() => { server.respondImmediately = true; - httpRequestsMockHelpers.setNodesListResponse({}); + http.setupNodeListResponse(); httpRequestsMockHelpers.setNodesDetailsResponse('attribute:true', [ { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, ]); @@ -431,34 +444,39 @@ describe('edit policy', () => { expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy(); }); test('should show warning instead of node attributes input when none exist', async () => { + http.setupNodeListResponse({ + nodesByAttributes: {}, + nodesByRoles: { data: ['node1'] }, + }); const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(rendered.find('.euiCallOut--warning').exists()).toBeTruthy(); + openNodeAttributesSection(rendered, 'warm'); + expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeTruthy(); expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy(); }); test('should show node attributes input when attributes exist', async () => { - httpRequestsMockHelpers.setNodesListResponse({ 'attribute:true': ['node1'] }); const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); + openNodeAttributesSection(rendered, 'warm'); + expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'warm'); expect(nodeAttributesSelect.exists()).toBeTruthy(); expect(nodeAttributesSelect.find('option').length).toBe(2); }); test('should show view node attributes link when attribute selected and show flyout when clicked', async () => { - httpRequestsMockHelpers.setNodesListResponse({ 'attribute:true': ['node1'] }); const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); + openNodeAttributesSection(rendered, 'warm'); + expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'warm'); expect(nodeAttributesSelect.exists()).toBeTruthy(); expect(findTestSubject(rendered, 'warm-viewNodeDetailsFlyoutButton').exists()).toBeFalsy(); @@ -473,11 +491,23 @@ describe('edit policy', () => { rendered.update(); expect(rendered.find('.euiFlyout').exists()).toBeTruthy(); }); + test('should show default allocation warning when no node roles are found', async () => { + http.setupNodeListResponse({ + nodesByAttributes: {}, + nodesByRoles: {}, + }); + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'warm'); + expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(findTestSubject(rendered, 'defaultAllocationWarning').exists()).toBeTruthy(); + }); }); describe('cold phase', () => { beforeEach(() => { server.respondImmediately = true; - httpRequestsMockHelpers.setNodesListResponse({}); + http.setupNodeListResponse(); httpRequestsMockHelpers.setNodesDetailsResponse('attribute:true', [ { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, ]); @@ -511,34 +541,39 @@ describe('edit policy', () => { expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy(); }); test('should show warning instead of node attributes input when none exist', async () => { + http.setupNodeListResponse({ + nodesByAttributes: {}, + nodesByRoles: { data: ['node1'] }, + }); const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(rendered.find('.euiCallOut--warning').exists()).toBeTruthy(); + openNodeAttributesSection(rendered, 'cold'); + expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeTruthy(); expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy(); }); test('should show node attributes input when attributes exist', async () => { - httpRequestsMockHelpers.setNodesListResponse({ 'attribute:true': ['node1'] }); const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); + openNodeAttributesSection(rendered, 'cold'); + expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold'); expect(nodeAttributesSelect.exists()).toBeTruthy(); expect(nodeAttributesSelect.find('option').length).toBe(2); }); test('should show view node attributes link when attribute selected and show flyout when clicked', async () => { - httpRequestsMockHelpers.setNodesListResponse({ 'attribute:true': ['node1'] }); const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); + openNodeAttributesSection(rendered, 'cold'); + expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold'); expect(nodeAttributesSelect.exists()).toBeTruthy(); expect(findTestSubject(rendered, 'cold-viewNodeDetailsFlyoutButton').exists()).toBeFalsy(); @@ -563,6 +598,128 @@ describe('edit policy', () => { save(rendered); expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); }); + test('should show default allocation warning when no node roles are found', async () => { + http.setupNodeListResponse({ + nodesByAttributes: {}, + nodesByRoles: {}, + }); + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'cold'); + expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(findTestSubject(rendered, 'defaultAllocationWarning').exists()).toBeTruthy(); + }); + }); + describe('frozen phase', () => { + beforeEach(() => { + server.respondImmediately = true; + http.setupNodeListResponse(); + httpRequestsMockHelpers.setNodesDetailsResponse('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + }); + test('should allow 0 for phase timing', async () => { + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'frozen'); + setPhaseAfter(rendered, 'frozen', 0); + save(rendered); + expectedErrorMessages(rendered, []); + }); + test('should show positive number required error when trying to save cold phase with -1 for after', async () => { + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'frozen'); + setPhaseAfter(rendered, 'frozen', -1); + save(rendered); + expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); + }); + test('should show spinner for node attributes input when loading', async () => { + server.respondImmediately = false; + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'frozen'); + expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy(); + expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); + expect(getNodeAttributeSelect(rendered, 'frozen').exists()).toBeFalsy(); + }); + test('should show warning instead of node attributes input when none exist', async () => { + http.setupNodeListResponse({ + nodesByAttributes: {}, + nodesByRoles: { data: ['node1'] }, + }); + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'frozen'); + expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); + openNodeAttributesSection(rendered, 'frozen'); + expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeTruthy(); + expect(getNodeAttributeSelect(rendered, 'frozen').exists()).toBeFalsy(); + }); + test('should show node attributes input when attributes exist', async () => { + http.setupNodeListResponse(); + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'frozen'); + expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); + openNodeAttributesSection(rendered, 'frozen'); + expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); + const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'frozen'); + expect(nodeAttributesSelect.exists()).toBeTruthy(); + expect(nodeAttributesSelect.find('option').length).toBe(2); + }); + test('should show view node attributes link when attribute selected and show flyout when clicked', async () => { + http.setupNodeListResponse(); + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'frozen'); + expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); + openNodeAttributesSection(rendered, 'frozen'); + expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); + const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'frozen'); + expect(nodeAttributesSelect.exists()).toBeTruthy(); + expect(findTestSubject(rendered, 'frozen-viewNodeDetailsFlyoutButton').exists()).toBeFalsy(); + expect(nodeAttributesSelect.find('option').length).toBe(2); + nodeAttributesSelect.simulate('change', { target: { value: 'attribute:true' } }); + rendered.update(); + const flyoutButton = findTestSubject(rendered, 'frozen-viewNodeDetailsFlyoutButton'); + expect(flyoutButton.exists()).toBeTruthy(); + await act(async () => { + await flyoutButton.simulate('click'); + }); + rendered.update(); + expect(rendered.find('.euiFlyout').exists()).toBeTruthy(); + }); + test('should show positive number required error when trying to save with -1 for index priority', async () => { + http.setupNodeListResponse(); + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'frozen'); + setPhaseAfter(rendered, 'frozen', 1); + setPhaseIndexPriority(rendered, 'frozen', -1); + save(rendered); + expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); + }); + test('should show default allocation warning when no node roles are found', async () => { + http.setupNodeListResponse({ + nodesByAttributes: {}, + nodesByRoles: {}, + }); + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'frozen'); + expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(findTestSubject(rendered, 'defaultAllocationWarning').exists()).toBeTruthy(); + }); }); describe('delete phase', () => { test('should allow 0 for phase timing', async () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/edit_policy.ts b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/edit_policy.ts new file mode 100644 index 0000000000000..4eeb542671d23 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/edit_policy.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { init as initHttpRequests } from './http_requests'; + +export type EditPolicySetup = ReturnType; + +export const setup = () => { + const { httpRequestsMockHelpers, server } = initHttpRequests(); + + const setupNodeListResponse = ( + response: Record = { + nodesByAttributes: { 'attribute:true': ['node1'] }, + nodesByRoles: { data: ['node1'] }, + } + ) => { + httpRequestsMockHelpers.setNodesListResponse(response); + }; + + return { + http: { + setupNodeListResponse, + httpRequestsMockHelpers, + server, + }, + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts index 6cbe3bdf1f8c6..a9d326073e4d3 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts @@ -40,6 +40,8 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { }; }; +export type HttpRequestMockHelpers = ReturnType; + export const init = () => { const server = sinon.fakeServer.create(); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/index.ts b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/index.ts new file mode 100644 index 0000000000000..4c32ea121bb57 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as editPolicyHelpers from './edit_policy'; + +export { HttpRequestMockHelpers, init } from './http_requests'; + +export { editPolicyHelpers }; diff --git a/x-pack/plugins/index_lifecycle_management/common/types/api.ts b/x-pack/plugins/index_lifecycle_management/common/types/api.ts new file mode 100644 index 0000000000000..16b8fbd127ab6 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/common/types/api.ts @@ -0,0 +1,12 @@ +/* + * 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 type NodeDataRole = 'data' | 'data_hot' | 'data_warm' | 'data_cold' | 'data_frozen'; + +export interface ListNodesRouteResponse { + nodesByAttributes: { [attributePair: string]: string[] }; + nodesByRoles: { [role in NodeDataRole]?: string[] }; +} diff --git a/x-pack/plugins/index_lifecycle_management/common/types/index.ts b/x-pack/plugins/index_lifecycle_management/common/types/index.ts index fef79c7782bb0..a23dc647f1f65 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/index.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/index.ts @@ -4,4 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './api'; + export * from './policies'; diff --git a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts index 97effee44533a..8f913dd884dfe 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts @@ -6,6 +6,8 @@ import { Index as IndexInterface } from '../../../index_management/common/types'; +export type PhaseWithAllocation = 'warm' | 'cold' | 'frozen'; + export interface SerializedPolicy { name: string; phases: Phases; @@ -62,6 +64,7 @@ export interface SerializedWarmPhase extends SerializedPhase { set_priority?: { priority: number | null; }; + migrate?: { enabled: boolean }; }; } @@ -72,6 +75,7 @@ export interface SerializedColdPhase extends SerializedPhase { set_priority?: { priority: number | null; }; + migrate?: { enabled: boolean }; }; } @@ -82,6 +86,7 @@ export interface SerializedFrozenPhase extends SerializedPhase { set_priority?: { priority: number | null; }; + migrate?: { enabled: boolean }; }; } @@ -103,6 +108,13 @@ export interface AllocateAction { require?: { [attribute: string]: string; }; + migrate?: { + /** + * If enabled is ever set it will only be set to `false` because the default value + * for this is `true`. Rather leave unspecified for true. + */ + enabled: false; + }; } export interface Policy { @@ -125,9 +137,23 @@ export interface PhaseWithMinAge { selectedMinimumAgeUnits: string; } +/** + * Different types of allocation markers we use in deserialized policies. + * + * default - use data tier based data allocation based on node roles -- this is ES best practice mode. + * custom - use node_attrs to allocate data to specific nodes + * none - do not move data anywhere when entering a phase + */ +export type DataTierAllocationType = 'default' | 'custom' | 'none'; + export interface PhaseWithAllocationAction { selectedNodeAttrs: string; selectedReplicaCount: string; + /** + * A string value indicating allocation type. If unspecified we assume the user + * wants to use default allocation. + */ + dataTierAllocationType: DataTierAllocationType; } export interface PhaseWithIndexPriority { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts index f11860d36faf8..6d4c57d23138d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts @@ -38,6 +38,7 @@ export const defaultNewWarmPhase: WarmPhase = { selectedReplicaCount: '', warmPhaseOnRollover: true, phaseIndexPriority: '50', + dataTierAllocationType: 'default', }; export const defaultNewColdPhase: ColdPhase = { @@ -48,6 +49,7 @@ export const defaultNewColdPhase: ColdPhase = { selectedReplicaCount: '', freezeEnabled: false, phaseIndexPriority: '0', + dataTierAllocationType: 'default', }; export const defaultNewFrozenPhase: FrozenPhase = { @@ -58,6 +60,7 @@ export const defaultNewFrozenPhase: FrozenPhase = { selectedReplicaCount: '', freezeEnabled: false, phaseIndexPriority: '0', + dataTierAllocationType: 'default', }; export const defaultNewDeletePhase: DeletePhase = { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/check_phase_compatibility.ts b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/check_phase_compatibility.ts new file mode 100644 index 0000000000000..2ef0fb145551f --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/check_phase_compatibility.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 { + NodeDataRole, + ListNodesRouteResponse, + PhaseWithAllocation, +} from '../../../../common/types'; + +/** + * Given a phase and current node roles, determine whether the phase + * can use default data tier allocation. + * + * This can only be checked for phases that have an allocate action. + */ +export const isPhaseDefaultDataAllocationCompatible = ( + phase: PhaseWithAllocation, + nodesByRoles: ListNodesRouteResponse['nodesByRoles'] +): boolean => { + // The 'data' role covers all node roles, so if we have at least one node with the data role + // we can use default allocation. + if (nodesByRoles.data?.length) { + return true; + } + + // Otherwise we need to check whether a node role for the specific phase exists + if (nodesByRoles[`data_${phase}` as NodeDataRole]?.length) { + return true; + } + + // Otherwise default allocation has nowhere to allocate new shards to in this phase. + return false; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/determine_allocation_type.ts b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/determine_allocation_type.ts new file mode 100644 index 0000000000000..4067ad97fc43b --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/determine_allocation_type.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 { DataTierAllocationType, AllocateAction } from '../../../../common/types'; + +/** + * Determine what deserialized state the policy config represents. + * + * See {@DataTierAllocationType} for more information. + */ +export const determineDataTierAllocationType = ( + allocateAction?: AllocateAction +): DataTierAllocationType => { + if (!allocateAction) { + return 'default'; + } + + if (allocateAction.migrate?.enabled === false) { + return 'none'; + } + + if ( + (allocateAction.require && Object.keys(allocateAction.require).length) || + (allocateAction.include && Object.keys(allocateAction.include).length) || + (allocateAction.exclude && Object.keys(allocateAction.exclude).length) + ) { + return 'custom'; + } + + return 'default'; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/index.ts new file mode 100644 index 0000000000000..67a512cefe00c --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/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. + */ + +export * from './determine_allocation_type'; + +export * from './check_phase_compatibility'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/lib/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/lib/index.ts new file mode 100644 index 0000000000000..1dabae1a0f0c4 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/lib/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './data_tiers'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.scss new file mode 100644 index 0000000000000..62ec3f303e1e8 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.scss @@ -0,0 +1,9 @@ +.indexLifecycleManagement__phase__dataTierAllocation { + &__controlSection { + background-color: $euiColorLightestShade; + padding-top: $euiSizeM; + padding-left: $euiSizeM; + padding-right: $euiSizeM; + padding-bottom: $euiSizeM; + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.tsx new file mode 100644 index 0000000000000..3ae60a5a3d622 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.tsx @@ -0,0 +1,194 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiText, EuiFormRow, EuiSpacer, EuiSuperSelect, EuiSuperSelectOption } from '@elastic/eui'; + +import { DataTierAllocationType } from '../../../../../../common/types'; +import { NodeAllocation } from './node_allocation'; +import { SharedProps } from './types'; + +import './data_tier_allocation.scss'; + +type SelectOptions = EuiSuperSelectOption; + +const i18nTexts = { + allocationFieldLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.allocationFieldLabel', + { defaultMessage: 'Data tier options' } + ), + allocationOptions: { + warm: { + default: { + input: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.input', + { defaultMessage: 'Use warm nodes (recommended)' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.helpText', + { defaultMessage: 'Move data to nodes in the warm tier.' } + ), + }, + none: { + inputDisplay: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.noneOption.input', + { defaultMessage: 'Off' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.noneOption.helpText', + { defaultMessage: 'Do not move data in the warm phase.' } + ), + }, + custom: { + inputDisplay: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.input', + { defaultMessage: 'Custom' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.helpText', + { defaultMessage: 'Move data based on node attributes.' } + ), + }, + }, + cold: { + default: { + input: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.input', + { defaultMessage: 'Use cold nodes (recommended)' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.helpText', + { defaultMessage: 'Move data to nodes in the cold tier.' } + ), + }, + none: { + inputDisplay: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.input', + { defaultMessage: 'Off' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.helpText', + { defaultMessage: 'Do not move data in the cold phase.' } + ), + }, + custom: { + inputDisplay: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.input', + { defaultMessage: 'Custom' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.helpText', + { defaultMessage: 'Move data based on node attributes.' } + ), + }, + }, + frozen: { + default: { + input: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.defaultOption.input', + { defaultMessage: 'Use frozen nodes (recommended)' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.defaultOption.helpText', + { defaultMessage: 'Move data to nodes in the frozen tier.' } + ), + }, + none: { + inputDisplay: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.noneOption.input', + { defaultMessage: 'Off' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.noneOption.helpText', + { defaultMessage: 'Do not move data in the frozen phase.' } + ), + }, + custom: { + inputDisplay: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.customOption.input', + { defaultMessage: 'Custom' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.customOption.helpText', + { defaultMessage: 'Move data based on node attributes.' } + ), + }, + }, + }, +}; + +export const DataTierAllocation: FunctionComponent = (props) => { + const { phaseData, setPhaseData, phase, hasNodeAttributes } = props; + + return ( +
+ + setPhaseData('dataTierAllocationType', value)} + options={ + [ + { + value: 'default', + inputDisplay: i18nTexts.allocationOptions[phase].default.input, + dropdownDisplay: ( + <> + {i18nTexts.allocationOptions[phase].default.input} + +

+ {i18nTexts.allocationOptions[phase].default.helpText} +

+
+ + ), + }, + { + value: 'none', + inputDisplay: i18nTexts.allocationOptions[phase].none.inputDisplay, + dropdownDisplay: ( + <> + {i18nTexts.allocationOptions[phase].none.inputDisplay} + +

+ {i18nTexts.allocationOptions[phase].none.helpText} +

+
+ + ), + }, + { + 'data-test-subj': 'customDataAllocationOption', + value: 'custom', + inputDisplay: i18nTexts.allocationOptions[phase].custom.inputDisplay, + dropdownDisplay: ( + <> + {i18nTexts.allocationOptions[phase].custom.inputDisplay} + +

+ {i18nTexts.allocationOptions[phase].custom.helpText} +

+
+ + ), + }, + ] as SelectOptions[] + } + /> +
+ {phaseData.dataTierAllocationType === 'custom' && hasNodeAttributes && ( + <> + +
+ +
+ + )} +
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/default_allocation_warning.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/default_allocation_warning.tsx new file mode 100644 index 0000000000000..a7ebc0d2e4a24 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/default_allocation_warning.tsx @@ -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. + */ + +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; + +import { PhaseWithAllocation } from '../../../../../../common/types'; + +const i18nTexts = { + warm: { + title: i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableTitle', + { defaultMessage: 'No nodes assigned to the warm tier' } + ), + body: i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableBody', + { + defaultMessage: + 'Assign at least one node to the warm tier to use role-based allocation. The policy will fail to complete allocation if there are no warm nodes.', + } + ), + }, + cold: { + title: i18n.translate( + 'xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableTitle', + { defaultMessage: 'No nodes assigned to the cold tier' } + ), + body: i18n.translate( + 'xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableBody', + { + defaultMessage: + 'Assign at least one node to the cold tier to use role-based allocation. The policy will fail to complete allocation if there are no cold nodes.', + } + ), + }, + frozen: { + title: i18n.translate( + 'xpack.indexLifecycleMgmt.frozenPhase.dataTier.defaultAllocationNotAvailableTitle', + { defaultMessage: 'No nodes assigned to the frozen tier' } + ), + body: i18n.translate( + 'xpack.indexLifecycleMgmt.frozenPhase.dataTier.defaultAllocationNotAvailableBody', + { + defaultMessage: + 'Assign at least one node to the frozen tier to use role-based allocation. The policy will fail to complete allocation if there are no frozen nodes.', + } + ), + }, +}; + +interface Props { + phase: PhaseWithAllocation; +} + +export const DefaultAllocationWarning: FunctionComponent = ({ phase }) => { + return ( + <> + + + {i18nTexts[phase].body} + + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/index.ts new file mode 100644 index 0000000000000..26464a75ae14c --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/index.ts @@ -0,0 +1,12 @@ +/* + * 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 { NodesDataProvider } from './node_data_provider'; +export { NodeAllocation } from './node_allocation'; +export { NodeAttrsDetails } from './node_attrs_details'; +export { DataTierAllocation } from './data_tier_allocation'; +export { DefaultAllocationWarning } from './default_allocation_warning'; +export { NoNodeAttributesWarning } from './no_node_attributes_warning'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/no_node_attributes_warning.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/no_node_attributes_warning.tsx new file mode 100644 index 0000000000000..1ba82623c2b94 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/no_node_attributes_warning.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { PhaseWithAllocation } from '../../../../../../common/types'; + +const i18nTexts = { + title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel', { + defaultMessage: 'No custom node attributes configured', + }), + warm: { + body: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.warm.nodeAttributesMissingDescription', + { + defaultMessage: + 'Define custom node attributes in elasticsearch.yml to use attribute-based allocation. Warm nodes will be used instead.', + } + ), + }, + cold: { + body: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.cold.nodeAttributesMissingDescription', + { + defaultMessage: + 'Define custom node attributes in elasticsearch.yml to use attribute-based allocation. Cold nodes will be used instead.', + } + ), + }, + frozen: { + body: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.frozen.nodeAttributesMissingDescription', + { + defaultMessage: + 'Define custom node attributes in elasticsearch.yml to use attribute-based allocation. Frozen nodes will be used instead.', + } + ), + }, +}; + +export const NoNodeAttributesWarning: FunctionComponent<{ phase: PhaseWithAllocation }> = ({ + phase, +}) => { + return ( + <> + + + {i18nTexts[phase].body} + + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_allocation.tsx new file mode 100644 index 0000000000000..a57a6ba4ff2c6 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_allocation.tsx @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, FunctionComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { EuiSelect, EuiButtonEmpty, EuiText, EuiSpacer } from '@elastic/eui'; + +import { PhaseWithAllocationAction } from '../../../../../../common/types'; +import { propertyof } from '../../../../services/policies/policy_validation'; + +import { ErrableFormRow } from '../form_errors'; + +import { NodeAttrsDetails } from './node_attrs_details'; +import { SharedProps } from './types'; +import { LearnMoreLink } from '../learn_more_link'; + +const learnMoreLink = ( + + } + docPath="modules-cluster.html#cluster-shard-allocation-settings" + /> +); + +const i18nTexts = { + doNotModifyAllocationOption: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.doNotModifyAllocationOption', + { defaultMessage: 'Do not modify allocation configuration' } + ), +}; + +export const NodeAllocation: FunctionComponent = ({ + phase, + setPhaseData, + errors, + phaseData, + isShowingErrors, + nodes, +}) => { + const [selectedNodeAttrsForDetails, setSelectedNodeAttrsForDetails] = useState( + null + ); + + const nodeOptions = Object.keys(nodes).map((attrs) => ({ + text: `${attrs} (${nodes[attrs].length})`, + value: attrs, + })); + + nodeOptions.sort((a, b) => a.value.localeCompare(b.value)); + + // check that this string is a valid property + const nodeAttrsProperty = propertyof('selectedNodeAttrs'); + + return ( + <> + +

+ +

+
+ + + {/* + TODO: this field component must be revisited to support setting multiple require values and to support + setting `include and exclude values on ILM policies. See https://github.com/elastic/kibana/issues/77344 + */} + setSelectedNodeAttrsForDetails(phaseData.selectedNodeAttrs)} + > + + + ) : null + } + > + { + setPhaseData(nodeAttrsProperty, e.target.value); + }} + /> + + + {selectedNodeAttrsForDetails ? ( + setSelectedNodeAttrsForDetails(null)} + /> + ) : null} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_attrs_details.tsx similarity index 97% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_attrs_details.tsx index af8833c8082b3..c29495d13eb8e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_attrs_details.tsx @@ -20,7 +20,7 @@ import { EuiButton, } from '@elastic/eui'; -import { useLoadNodeDetails } from '../../../services/api'; +import { useLoadNodeDetails } from '../../../../services/api'; interface Props { close: () => void; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_data_provider.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_data_provider.tsx new file mode 100644 index 0000000000000..a7c0f3ec7c866 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_data_provider.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiButton, EuiCallOut, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { ListNodesRouteResponse } from '../../../../../../common/types'; +import { useLoadNodes } from '../../../../services/api'; + +interface Props { + children: (data: ListNodesRouteResponse) => JSX.Element; +} + +export const NodesDataProvider = ({ children }: Props): JSX.Element => { + const { isLoading, data, error, resendRequest } = useLoadNodes(); + + if (isLoading) { + return ( + <> + + + + ); + } + + const renderError = () => { + if (error) { + const { statusCode, message } = error; + return ( + <> + + } + color="danger" + > +

+ {message} ({statusCode}) +

+ + + +
+ + + + ); + } + return null; + }; + + return ( + <> + {renderError()} + {/* `data` will always be defined because we use an initial value when loading */} + {children(data!)} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/types.ts new file mode 100644 index 0000000000000..d4cb31a3be9e7 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/types.ts @@ -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 { + ListNodesRouteResponse, + PhaseWithAllocation, + PhaseWithAllocationAction, +} from '../../../../../../common/types'; +import { PhaseValidationErrors } from '../../../../services/policies/policy_validation'; + +export interface SharedProps { + phase: PhaseWithAllocation; + errors?: PhaseValidationErrors; + phaseData: PhaseWithAllocationAction; + setPhaseData: (dataKey: keyof PhaseWithAllocationAction, value: string) => void; + isShowingErrors: boolean; + nodes: ListNodesRouteResponse['nodesByAttributes']; + hasNodeAttributes: boolean; +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_field.tsx new file mode 100644 index 0000000000000..7bf8cd3ba6d90 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_field.tsx @@ -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 React, { FunctionComponent } from 'react'; +import { EuiDescribedFormGroup, EuiDescribedFormGroupProps } from '@elastic/eui'; + +import { ToggleableField, Props as ToggleableFieldProps } from './toggleable_field'; + +type Props = EuiDescribedFormGroupProps & { + switchProps: ToggleableFieldProps; +}; + +export const DescribedFormField: FunctionComponent = ({ + children, + switchProps, + ...restDescribedFormProps +}) => { + return ( + + {children} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts index 4410c4bb38397..2428cade0898e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts @@ -8,11 +8,17 @@ export { ActiveBadge } from './active_badge'; export { ErrableFormRow } from './form_errors'; export { LearnMoreLink } from './learn_more_link'; export { MinAgeInput } from './min_age_input'; -export { NodeAllocation } from './node_allocation'; -export { NodeAttrsDetails } from './node_attrs_details'; export { OptionalLabel } from './optional_label'; export { PhaseErrorMessage } from './phase_error_message'; export { PolicyJsonFlyout } from './policy_json_flyout'; export { SetPriorityInput } from './set_priority_input'; export { SnapshotPolicies } from './snapshot_policies'; +export { + DataTierAllocation, + NodeAllocation, + NodeAttrsDetails, + NodesDataProvider, + DefaultAllocationWarning, +} from './data_tier_allocation'; +export { DescribedFormField } from './described_form_field'; export { Forcemerge } from './forcemerge'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx deleted file mode 100644 index 6a22d8716514c..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx +++ /dev/null @@ -1,189 +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 React, { Fragment, useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { - EuiSelect, - EuiButtonEmpty, - EuiCallOut, - EuiSpacer, - EuiLoadingSpinner, - EuiButton, -} from '@elastic/eui'; - -import { LearnMoreLink } from './learn_more_link'; -import { ErrableFormRow } from './form_errors'; -import { useLoadNodes } from '../../../services/api'; -import { NodeAttrsDetails } from './node_attrs_details'; -import { PhaseWithAllocationAction, Phases } from '../../../../../common/types'; -import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; - -const learnMoreLink = ( - - - - } - docPath="modules-cluster.html#cluster-shard-allocation-settings" - /> - -); - -interface Props { - phase: keyof Phases & string; - errors?: PhaseValidationErrors; - phaseData: T; - setPhaseData: (dataKey: keyof T & string, value: string) => void; - isShowingErrors: boolean; -} -export const NodeAllocation = ({ - phase, - setPhaseData, - errors, - phaseData, - isShowingErrors, -}: React.PropsWithChildren>) => { - const { isLoading, data: nodes, error, resendRequest } = useLoadNodes(); - - const [selectedNodeAttrsForDetails, setSelectedNodeAttrsForDetails] = useState( - null - ); - - if (isLoading) { - return ( - - - - - ); - } - - if (error) { - const { statusCode, message } = error; - return ( - - - } - color="danger" - > -

- {message} ({statusCode}) -

- - - -
- - -
- ); - } - - let nodeOptions = Object.keys(nodes).map((attrs) => ({ - text: `${attrs} (${nodes[attrs].length})`, - value: attrs, - })); - - nodeOptions.sort((a, b) => a.value.localeCompare(b.value)); - if (nodeOptions.length) { - nodeOptions = [ - { - text: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.defaultNodeAllocation', { - defaultMessage: "Default allocation (don't use attributes)", - }), - value: '', - }, - ...nodeOptions, - ]; - } - if (!nodeOptions.length) { - return ( - - - } - color="warning" - > - - {learnMoreLink} - - - - - ); - } - - // check that this string is a valid property - const nodeAttrsProperty = propertyof('selectedNodeAttrs'); - - return ( - - - { - setPhaseData(nodeAttrsProperty, e.target.value); - }} - /> - - {!!phaseData.selectedNodeAttrs ? ( - setSelectedNodeAttrsForDetails(phaseData.selectedNodeAttrs)} - > - - - ) : null} - {learnMoreLink} - - - {selectedNodeAttrsForDetails ? ( - setSelectedNodeAttrsForDetails(null)} - /> - ) : null} - - ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/toggleable_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/toggleable_field.tsx new file mode 100644 index 0000000000000..ff4301808db33 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/toggleable_field.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent, useState } from 'react'; +import { EuiSpacer, EuiSwitch, EuiSwitchProps } from '@elastic/eui'; + +export interface Props extends Omit { + initialValue: boolean; + onChange: (nextValue: boolean) => void; +} + +export const ToggleableField: FunctionComponent = ({ + initialValue, + onChange, + children, + ...restProps +}) => { + const [isContentVisible, setIsContentVisible] = useState(initialValue); + + return ( + <> + { + const nextValue = e.target.checked; + setIsContentVisible(nextValue); + onChange(nextValue); + }} + /> + + {isContentVisible ? children : null} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx index f1c287788e08d..85529ef0c9a5b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useEffect, useState } from 'react'; +import React, { Fragment, useEffect, useState, useCallback } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -45,7 +45,7 @@ import { import { ErrableFormRow, LearnMoreLink, PolicyJsonFlyout } from './components'; import { ColdPhase, DeletePhase, FrozenPhase, HotPhase, WarmPhase } from './phases'; -interface Props { +export interface Props { policies: PolicyFromES[]; policyName: string; getUrlForApp: ( @@ -119,15 +119,39 @@ export const EditPolicy: React.FunctionComponent = ({ setIsShowingPolicyJsonFlyout(!isShowingPolicyJsonFlyout); }; - const setPhaseData = (phase: keyof Phases, key: string, value: any) => { - setPolicy({ - ...policy, - phases: { - ...policy.phases, - [phase]: { ...policy.phases[phase], [key]: value }, - }, - }); - }; + const setPhaseData = useCallback( + (phase: keyof Phases, key: string, value: any) => { + setPolicy((nextPolicy) => ({ + ...nextPolicy, + phases: { + ...nextPolicy.phases, + [phase]: { ...nextPolicy.phases[phase], [key]: value }, + }, + })); + }, + [setPolicy] + ); + + const setHotPhaseData = useCallback( + (key: string, value: any) => setPhaseData('hot', key, value), + [setPhaseData] + ); + const setWarmPhaseData = useCallback( + (key: string, value: any) => setPhaseData('warm', key, value), + [setPhaseData] + ); + const setColdPhaseData = useCallback( + (key: string, value: any) => setPhaseData('cold', key, value), + [setPhaseData] + ); + const setFrozenPhaseData = useCallback( + (key: string, value: any) => setPhaseData('frozen', key, value), + [setPhaseData] + ); + const setDeletePhaseData = useCallback( + (key: string, value: any) => setPhaseData('delete', key, value), + [setPhaseData] + ); const setWarmPhaseOnRollover = (value: boolean) => { setPolicy({ @@ -277,7 +301,7 @@ export const EditPolicy: React.FunctionComponent = ({ 0} - setPhaseData={(key, value) => setPhaseData('hot', key, value)} + setPhaseData={setHotPhaseData} phaseData={policy.phases.hot} setWarmPhaseOnRollover={setWarmPhaseOnRollover} /> @@ -287,7 +311,7 @@ export const EditPolicy: React.FunctionComponent = ({ 0} - setPhaseData={(key, value) => setPhaseData('warm', key, value)} + setPhaseData={setWarmPhaseData} phaseData={policy.phases.warm} hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} /> @@ -297,7 +321,7 @@ export const EditPolicy: React.FunctionComponent = ({ 0} - setPhaseData={(key, value) => setPhaseData('cold', key, value)} + setPhaseData={setColdPhaseData} phaseData={policy.phases.cold} hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} /> @@ -307,7 +331,7 @@ export const EditPolicy: React.FunctionComponent = ({ 0} - setPhaseData={(key, value) => setPhaseData('frozen', key, value)} + setPhaseData={setFrozenPhaseData} phaseData={policy.phases.frozen} hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} /> @@ -318,7 +342,7 @@ export const EditPolicy: React.FunctionComponent = ({ errors={errors?.delete} isShowingErrors={isShowingErrors && !!errors && Object.keys(errors.delete).length > 0} getUrlForApp={getUrlForApp} - setPhaseData={(key, value) => setPhaseData('delete', key, value)} + setPhaseData={setDeletePhaseData} phaseData={policy.phases.delete} hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx index ae2858e7a84ae..241a98fffa6df 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx @@ -4,19 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { PureComponent, Fragment } from 'react'; +import React, { FunctionComponent, Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiFieldNumber, - EuiDescribedFormGroup, - EuiSwitch, - EuiTextColor, -} from '@elastic/eui'; +import { EuiFieldNumber, EuiDescribedFormGroup, EuiSwitch, EuiTextColor } from '@elastic/eui'; import { ColdPhase as ColdPhaseInterface, Phases } from '../../../../../common/types'; import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; @@ -27,14 +19,24 @@ import { PhaseErrorMessage, OptionalLabel, ErrableFormRow, - MinAgeInput, - NodeAllocation, SetPriorityInput, + MinAgeInput, + DescribedFormField, } from '../components'; -const freezeLabel = i18n.translate('xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel', { - defaultMessage: 'Freeze index', -}); +import { DataTierAllocationField } from './shared'; + +const i18nTexts = { + freezeLabel: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel', { + defaultMessage: 'Freeze index', + }), + dataTierAllocation: { + description: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.dataTier.description', { + defaultMessage: + 'Move data to data nodes optimized for less frequent, read-only access. Store cold data on less-expensive hardware.', + }), + }, +}; const coldProperty: keyof Phases = 'cold'; const phaseProperty = (propertyName: keyof ColdPhaseInterface) => propertyName; @@ -46,18 +48,17 @@ interface Props { errors?: PhaseValidationErrors; hotPhaseRolloverEnabled: boolean; } -export class ColdPhase extends PureComponent { - render() { - const { - setPhaseData, - phaseData, - errors, - isShowingErrors, - hotPhaseRolloverEnabled, - } = this.props; - - return ( -
+export const ColdPhase: FunctionComponent = ({ + setPhaseData, + phaseData, + errors, + isShowingErrors, + hotPhaseRolloverEnabled, +}) => { + return ( +
+ <> + {/* Section title group; containing min age */} @@ -86,7 +87,7 @@ export class ColdPhase extends PureComponent { data-test-subj="enablePhaseSwitch-cold" label={ } @@ -101,68 +102,83 @@ export class ColdPhase extends PureComponent { } fullWidth > - - {phaseData.phaseEnabled ? ( - - - errors={errors} - phaseData={phaseData} - phase={coldProperty} - isShowingErrors={isShowingErrors} - setPhaseData={setPhaseData} - rolloverEnabled={hotPhaseRolloverEnabled} - /> - - - - phase={coldProperty} - setPhaseData={setPhaseData} - errors={errors} - phaseData={phaseData} - isShowingErrors={isShowingErrors} - /> - - - - - - - - } - isShowingErrors={isShowingErrors} - errors={errors?.freezeEnabled} - helpText={i18n.translate( - 'xpack.indexLifecycleMgmt.coldPhase.replicaCountHelpText', - { - defaultMessage: 'By default, the number of replicas remains the same.', - } - )} - > - { - setPhaseData(phaseProperty('selectedReplicaCount'), e.target.value); - }} - min={0} - /> - - - - - ) : ( -
- )} - + {phaseData.phaseEnabled ? ( + + errors={errors} + phaseData={phaseData} + phase={coldProperty} + isShowingErrors={isShowingErrors} + setPhaseData={setPhaseData} + rolloverEnabled={hotPhaseRolloverEnabled} + /> + ) : null} {phaseData.phaseEnabled ? ( + {/* Data tier allocation section */} + + + {/* Replicas section */} + + {i18n.translate('xpack.indexLifecycleMgmt.coldPhase.replicasTitle', { + defaultMessage: 'Replicas', + })} + + } + description={i18n.translate( + 'xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasDescription', + { + defaultMessage: + 'Set the number of replicas. Remains the same as the previous phase by default.', + } + )} + switchProps={{ + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.coldPhase.numberOfReplicas.switchLabel', + { defaultMessage: 'Set replicas' } + ), + initialValue: Boolean(phaseData.selectedReplicaCount), + onChange: (v) => { + if (!v) { + setPhaseData('selectedReplicaCount', ''); + } + }, + }} + fullWidth + > + + + + + } + isShowingErrors={isShowingErrors} + errors={errors?.selectedReplicaCount} + > + { + setPhaseData(phaseProperty('selectedReplicaCount'), e.target.value); + }} + min={0} + /> + + + {/* Freeze section */} @@ -191,8 +207,8 @@ export class ColdPhase extends PureComponent { onChange={(e) => { setPhaseData(phaseProperty('freezeEnabled'), e.target.checked); }} - label={freezeLabel} - aria-label={freezeLabel} + label={i18nTexts.freezeLabel} + aria-label={i18nTexts.freezeLabel} /> @@ -204,7 +220,7 @@ export class ColdPhase extends PureComponent { /> ) : null} -
- ); - } -} + +
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx index bfaf141438169..6a849cc2c3f1f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx @@ -4,19 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { PureComponent, Fragment } from 'react'; +import React, { FunctionComponent, Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiFieldNumber, - EuiDescribedFormGroup, - EuiSwitch, - EuiTextColor, -} from '@elastic/eui'; +import { EuiFieldNumber, EuiDescribedFormGroup, EuiSwitch, EuiTextColor } from '@elastic/eui'; import { FrozenPhase as FrozenPhaseInterface, Phases } from '../../../../../common/types'; import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; @@ -28,13 +20,22 @@ import { OptionalLabel, ErrableFormRow, MinAgeInput, - NodeAllocation, SetPriorityInput, + DescribedFormField, } from '../components'; +import { DataTierAllocationField } from './shared'; -const freezeLabel = i18n.translate('xpack.indexLifecycleMgmt.frozenPhase.freezeIndexLabel', { - defaultMessage: 'Freeze index', -}); +const i18nTexts = { + freezeLabel: i18n.translate('xpack.indexLifecycleMgmt.frozenPhase.freezeIndexLabel', { + defaultMessage: 'Freeze index', + }), + dataTierAllocation: { + description: i18n.translate('xpack.indexLifecycleMgmt.frozenPhase.dataTier.description', { + defaultMessage: + 'Move data to data nodes optimized for infrequent, read-only access. Store frozen data on the least-expensive hardware.', + }), + }, +}; const frozenProperty: keyof Phases = 'frozen'; const phaseProperty = (propertyName: keyof FrozenPhaseInterface) => propertyName; @@ -46,18 +47,17 @@ interface Props { errors?: PhaseValidationErrors; hotPhaseRolloverEnabled: boolean; } -export class FrozenPhase extends PureComponent { - render() { - const { - setPhaseData, - phaseData, - errors, - isShowingErrors, - hotPhaseRolloverEnabled, - } = this.props; - - return ( -
+export const FrozenPhase: FunctionComponent = ({ + setPhaseData, + phaseData, + errors, + isShowingErrors, + hotPhaseRolloverEnabled, +}) => { + return ( +
+ <> + {/* Section title group; containing min age */} @@ -101,68 +101,82 @@ export class FrozenPhase extends PureComponent { } fullWidth > - - {phaseData.phaseEnabled ? ( - - - errors={errors} - phaseData={phaseData} - phase={frozenProperty} - isShowingErrors={isShowingErrors} - setPhaseData={setPhaseData} - rolloverEnabled={hotPhaseRolloverEnabled} - /> - - - - phase={frozenProperty} - setPhaseData={setPhaseData} - errors={errors} - phaseData={phaseData} - isShowingErrors={isShowingErrors} - /> - - - - - - - - } - isShowingErrors={isShowingErrors} - errors={errors?.freezeEnabled} - helpText={i18n.translate( - 'xpack.indexLifecycleMgmt.frozenPhase.replicaCountHelpText', - { - defaultMessage: 'By default, the number of replicas remains the same.', - } - )} - > - { - setPhaseData(phaseProperty('selectedReplicaCount'), e.target.value); - }} - min={0} - /> - - - - - ) : ( -
- )} - + {phaseData.phaseEnabled ? ( + + errors={errors} + phaseData={phaseData} + phase={frozenProperty} + isShowingErrors={isShowingErrors} + setPhaseData={setPhaseData} + rolloverEnabled={hotPhaseRolloverEnabled} + /> + ) : null} {phaseData.phaseEnabled ? ( + {/* Data tier allocation section */} + + + {/* Replicas section */} + + {i18n.translate('xpack.indexLifecycleMgmt.frozenPhase.replicasTitle', { + defaultMessage: 'Replicas', + })} + + } + description={i18n.translate( + 'xpack.indexLifecycleMgmt.frozenPhase.numberOfReplicasDescription', + { + defaultMessage: + 'Set the number of replicas. Remains the same as the previous phase by default.', + } + )} + switchProps={{ + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.frozenPhase.numberOfReplicas.switchLabel', + { defaultMessage: 'Set replicas' } + ), + initialValue: Boolean(phaseData.selectedReplicaCount), + onChange: (v) => { + if (!v) { + setPhaseData('selectedReplicaCount', ''); + } + }, + }} + fullWidth + > + + + + + } + isShowingErrors={isShowingErrors} + errors={errors?.selectedReplicaCount} + > + { + setPhaseData(phaseProperty('selectedReplicaCount'), e.target.value); + }} + min={0} + /> + + @@ -191,8 +205,8 @@ export class FrozenPhase extends PureComponent { onChange={(e) => { setPhaseData(phaseProperty('freezeEnabled'), e.target.checked); }} - label={freezeLabel} - aria-label={freezeLabel} + label={i18nTexts.freezeLabel} + aria-label={i18nTexts.freezeLabel} /> @@ -204,7 +218,7 @@ export class FrozenPhase extends PureComponent { /> ) : null} -
- ); - } -} + +
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/data_tier_allocation_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/data_tier_allocation_field.tsx new file mode 100644 index 0000000000000..6475e5286a778 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/data_tier_allocation_field.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; + +import { PhaseWithAllocationAction, PhaseWithAllocation } from '../../../../../../common/types'; + +import { + DataTierAllocation, + DefaultAllocationWarning, + NoNodeAttributesWarning, + NodesDataProvider, +} from '../../components/data_tier_allocation'; +import { PhaseValidationErrors } from '../../../../services/policies/policy_validation'; +import { isPhaseDefaultDataAllocationCompatible } from '../../../../lib/data_tiers'; + +const i18nTexts = { + title: i18n.translate('xpack.indexLifecycleMgmt.common.dataTier.title', { + defaultMessage: 'Data allocation', + }), +}; + +interface Props { + description: React.ReactNode; + phase: PhaseWithAllocation; + setPhaseData: (dataKey: keyof PhaseWithAllocationAction, value: string) => void; + isShowingErrors: boolean; + errors?: PhaseValidationErrors; + phaseData: PhaseWithAllocationAction; +} + +/** + * Top-level layout control for the data tier allocation field. + */ +export const DataTierAllocationField: FunctionComponent = ({ + description, + phase, + phaseData, + setPhaseData, + isShowingErrors, + errors, +}) => { + return ( + + {(nodesData) => { + const isCompatible = isPhaseDefaultDataAllocationCompatible(phase, nodesData.nodesByRoles); + const hasNodeAttrs = Boolean(Object.keys(nodesData.nodesByAttributes ?? {}).length); + + return ( + {i18nTexts.title}} + description={description} + fullWidth + > + + <> + + + {/* Data tier related warnings */} + + {phaseData.dataTierAllocationType === 'default' && !isCompatible && ( + + )} + + {phaseData.dataTierAllocationType === 'custom' && !hasNodeAttrs && ( + + )} + + + + ); + }} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/index.ts new file mode 100644 index 0000000000000..f9e939058adb9 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { DataTierAllocationField } from './data_tier_allocation_field'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx index c806056899cac..16a740b1171c9 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, PureComponent } from 'react'; +import React, { Fragment, FunctionComponent } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { @@ -27,44 +27,53 @@ import { OptionalLabel, ErrableFormRow, SetPriorityInput, - NodeAllocation, MinAgeInput, + DescribedFormField, Forcemerge, } from '../components'; +import { DataTierAllocationField } from './shared'; -const shrinkLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel', { - defaultMessage: 'Shrink index', -}); - -const moveToWarmPhaseOnRolloverLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.moveToWarmPhaseOnRolloverLabel', - { - defaultMessage: 'Move to warm phase on rollover', - } -); +const i18nTexts = { + shrinkLabel: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel', { + defaultMessage: 'Shrink index', + }), + moveToWarmPhaseOnRolloverLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.moveToWarmPhaseOnRolloverLabel', + { + defaultMessage: 'Move to warm phase on rollover', + } + ), + dataTierAllocation: { + description: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.dataTier.description', { + defaultMessage: + 'Move warm data to nodes optimized for read-only access. Store warm data on less-expensive hardware.', + }), + }, +}; const warmProperty: keyof Phases = 'warm'; const phaseProperty = (propertyName: keyof WarmPhaseInterface) => propertyName; interface Props { - setPhaseData: (key: keyof WarmPhaseInterface & string, value: boolean | string) => void; + setPhaseData: ( + key: keyof WarmPhaseInterface & string, + value: boolean | string | undefined + ) => void; phaseData: WarmPhaseInterface; isShowingErrors: boolean; errors?: PhaseValidationErrors; hotPhaseRolloverEnabled: boolean; } -export class WarmPhase extends PureComponent { - render() { - const { - setPhaseData, - phaseData, - errors, - isShowingErrors, - hotPhaseRolloverEnabled, - } = this.props; - - return ( -
+export const WarmPhase: FunctionComponent = ({ + setPhaseData, + phaseData, + errors, + isShowingErrors, + hotPhaseRolloverEnabled, +}) => { + return ( +
+ <> @@ -115,7 +124,7 @@ export class WarmPhase extends PureComponent { { @@ -137,58 +146,75 @@ export class WarmPhase extends PureComponent { /> ) : null} - - - - - phase={warmProperty} - setPhaseData={setPhaseData} - errors={errors} - phaseData={phaseData} - isShowingErrors={isShowingErrors} - /> - - - - - - - - } - isShowingErrors={isShowingErrors} - errors={errors?.selectedReplicaCount} - helpText={i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.replicaCountHelpText', - { - defaultMessage: 'By default, the number of replicas remains the same.', - } - )} - > - { - setPhaseData('selectedReplicaCount', e.target.value); - }} - min={0} - /> - - - - - ) : null} + {phaseData.phaseEnabled ? ( + {/* Data tier allocation section */} + + + + {i18n.translate('xpack.indexLifecycleMgmt.warmPhase.replicasTitle', { + defaultMessage: 'Replicas', + })} + + } + description={i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasDescription', + { + defaultMessage: + 'Set the number of replicas. Remains the same as the previous phase by default.', + } + )} + switchProps={{ + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.warmPhase.numberOfReplicas.switchLabel', + { defaultMessage: 'Set replicas' } + ), + initialValue: Boolean(phaseData.selectedReplicaCount), + onChange: (v) => { + if (!v) { + setPhaseData('selectedReplicaCount', ''); + } + }, + }} + fullWidth + > + + + + + } + isShowingErrors={isShowingErrors} + errors={errors?.selectedReplicaCount} + > + { + setPhaseData('selectedReplicaCount', e.target.value); + }} + min={0} + /> + + @@ -217,8 +243,8 @@ export class WarmPhase extends PureComponent { onChange={(e) => { setPhaseData(phaseProperty('shrinkEnabled'), e.target.checked); }} - label={shrinkLabel} - aria-label={shrinkLabel} + label={i18nTexts.shrinkLabel} + aria-label={i18nTexts.shrinkLabel} aria-controls="shrinkContent" /> @@ -275,7 +301,7 @@ export class WarmPhase extends PureComponent { /> ) : null} -
- ); - } -} + +
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts index 3d068433becbd..b279a5647c3e8 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts @@ -6,7 +6,7 @@ import { METRIC_TYPE } from '@kbn/analytics'; -import { PolicyFromES, SerializedPolicy } from '../../../common/types'; +import { PolicyFromES, SerializedPolicy, ListNodesRouteResponse } from '../../../common/types'; import { UIM_POLICY_DELETE, @@ -23,10 +23,10 @@ interface GenericObject { } export const useLoadNodes = () => { - return useRequest({ + return useRequest({ path: `nodes/list`, method: 'get', - initialData: [], + initialData: { nodesByAttributes: {}, nodesByRoles: {} } as ListNodesRouteResponse, }); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts index 3b71c11349752..70f172de390e3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts @@ -13,6 +13,8 @@ import { PhaseValidationErrors, positiveNumberRequiredMessage, } from './policy_validation'; +import { determineDataTierAllocationType } from '../../lib'; +import { serializePhaseWithAllocation } from './shared'; const coldPhaseInitialization: ColdPhase = { phaseEnabled: false, @@ -22,6 +24,7 @@ const coldPhaseInitialization: ColdPhase = { selectedReplicaCount: '', freezeEnabled: false, phaseIndexPriority: '', + dataTierAllocationType: 'default', }; export const coldPhaseFromES = (phaseSerialized?: SerializedColdPhase): ColdPhase => { @@ -32,6 +35,12 @@ export const coldPhaseFromES = (phaseSerialized?: SerializedColdPhase): ColdPhas phase.phaseEnabled = true; + if (phaseSerialized.actions.allocate) { + phase.dataTierAllocationType = determineDataTierAllocationType( + phaseSerialized.actions.allocate + ); + } + if (phaseSerialized.min_age) { const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age); phase.selectedMinimumAge = minAge; @@ -80,19 +89,7 @@ export const coldPhaseToES = ( esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`; } - esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {}; - - if (phase.selectedNodeAttrs) { - const [name, value] = phase.selectedNodeAttrs.split(':'); - esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); - esPhase.actions.allocate.require = { - [name]: value, - }; - } else { - if (esPhase.actions.allocate) { - delete esPhase.actions.allocate.require; - } - } + esPhase.actions = serializePhaseWithAllocation(phase, esPhase.actions); if (isNumber(phase.selectedReplicaCount)) { esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/frozen_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/frozen_phase.ts index 6249507bcb407..28d18b8f89263 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/frozen_phase.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/frozen_phase.ts @@ -13,6 +13,8 @@ import { PhaseValidationErrors, positiveNumberRequiredMessage, } from './policy_validation'; +import { determineDataTierAllocationType } from '../../lib'; +import { serializePhaseWithAllocation } from './shared'; const frozenPhaseInitialization: FrozenPhase = { phaseEnabled: false, @@ -22,6 +24,7 @@ const frozenPhaseInitialization: FrozenPhase = { selectedReplicaCount: '', freezeEnabled: false, phaseIndexPriority: '', + dataTierAllocationType: 'default', }; export const frozenPhaseFromES = (phaseSerialized?: SerializedFrozenPhase): FrozenPhase => { @@ -32,6 +35,12 @@ export const frozenPhaseFromES = (phaseSerialized?: SerializedFrozenPhase): Froz phase.phaseEnabled = true; + if (phaseSerialized.actions.allocate) { + phase.dataTierAllocationType = determineDataTierAllocationType( + phaseSerialized.actions.allocate + ); + } + if (phaseSerialized.min_age) { const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age); phase.selectedMinimumAge = minAge; @@ -80,19 +89,7 @@ export const frozenPhaseToES = ( esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`; } - esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {}; - - if (phase.selectedNodeAttrs) { - const [name, value] = phase.selectedNodeAttrs.split(':'); - esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); - esPhase.actions.allocate.require = { - [name]: value, - }; - } else { - if (esPhase.actions.allocate) { - delete esPhase.actions.allocate.require; - } - } + esPhase.actions = serializePhaseWithAllocation(phase, esPhase.actions); if (isNumber(phase.selectedReplicaCount)) { esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts new file mode 100644 index 0000000000000..0e7257d437ee7 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts @@ -0,0 +1,464 @@ +/* + * 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/cloneDeep'; +import { serializePolicy } from './policy_serialization'; +import { + defaultNewColdPhase, + defaultNewDeletePhase, + defaultNewFrozenPhase, + defaultNewHotPhase, + defaultNewWarmPhase, +} from '../../constants'; +import { DataTierAllocationType } from '../../../../common/types'; + +describe('Policy serialization', () => { + test('serialize a policy using "default" data allocation', () => { + expect( + serializePolicy( + { + name: 'test', + phases: { + hot: { ...defaultNewHotPhase }, + warm: { + ...defaultNewWarmPhase, + dataTierAllocationType: 'default', + // These selected attrs should be ignored + selectedNodeAttrs: 'another:thing', + phaseEnabled: true, + }, + cold: { + ...defaultNewColdPhase, + dataTierAllocationType: 'default', + selectedNodeAttrs: 'another:thing', + phaseEnabled: true, + }, + frozen: { + ...defaultNewFrozenPhase, + dataTierAllocationType: 'default', + selectedNodeAttrs: 'another:thing', + phaseEnabled: true, + }, + delete: { ...defaultNewDeletePhase }, + }, + }, + { + name: 'test', + phases: { + hot: { actions: {} }, + warm: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + cold: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + frozen: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + }, + } + ) + ).toEqual({ + name: 'test', + phases: { + hot: { + actions: { + rollover: { + max_age: '30d', + max_size: '50gb', + }, + set_priority: { + priority: 100, + }, + }, + }, + warm: { + actions: { + set_priority: { + priority: 50, + }, + }, + }, + cold: { + actions: { + set_priority: { + priority: 0, + }, + }, + min_age: '0d', + }, + frozen: { + actions: { + set_priority: { + priority: 0, + }, + }, + min_age: '0d', + }, + }, + }); + }); + + test('serialize a policy using "custom" data allocation', () => { + expect( + serializePolicy( + { + name: 'test', + phases: { + hot: { ...defaultNewHotPhase }, + warm: { + ...defaultNewWarmPhase, + dataTierAllocationType: 'custom', + selectedNodeAttrs: 'another:thing', + phaseEnabled: true, + }, + cold: { + ...defaultNewColdPhase, + dataTierAllocationType: 'custom', + selectedNodeAttrs: 'another:thing', + phaseEnabled: true, + }, + frozen: { + ...defaultNewFrozenPhase, + dataTierAllocationType: 'custom', + selectedNodeAttrs: 'another:thing', + phaseEnabled: true, + }, + delete: { ...defaultNewDeletePhase }, + }, + }, + { + name: 'test', + phases: { + hot: { actions: {} }, + warm: { + actions: { + allocate: { + include: { keep: 'this' }, + exclude: { keep: 'this' }, + require: { something: 'here' }, + }, + }, + }, + cold: { + actions: { + allocate: { + include: { keep: 'this' }, + exclude: { keep: 'this' }, + require: { something: 'here' }, + }, + }, + }, + frozen: { + actions: { + allocate: { + include: { keep: 'this' }, + exclude: { keep: 'this' }, + require: { something: 'here' }, + }, + }, + }, + }, + } + ) + ).toEqual({ + name: 'test', + phases: { + hot: { + actions: { + rollover: { + max_age: '30d', + max_size: '50gb', + }, + set_priority: { + priority: 100, + }, + }, + }, + warm: { + actions: { + allocate: { + include: { keep: 'this' }, + exclude: { keep: 'this' }, + require: { + another: 'thing', + }, + }, + set_priority: { + priority: 50, + }, + }, + }, + cold: { + actions: { + allocate: { + include: { keep: 'this' }, + exclude: { keep: 'this' }, + require: { + another: 'thing', + }, + }, + set_priority: { + priority: 0, + }, + }, + min_age: '0d', + }, + frozen: { + actions: { + allocate: { + include: { keep: 'this' }, + exclude: { keep: 'this' }, + require: { + another: 'thing', + }, + }, + set_priority: { + priority: 0, + }, + }, + min_age: '0d', + }, + }, + }); + }); + + test('serialize a policy using "custom" data allocation with no node attributes', () => { + expect( + serializePolicy( + { + name: 'test', + phases: { + hot: { ...defaultNewHotPhase }, + warm: { + ...defaultNewWarmPhase, + dataTierAllocationType: 'custom', + selectedNodeAttrs: '', + phaseEnabled: true, + }, + cold: { + ...defaultNewColdPhase, + dataTierAllocationType: 'custom', + selectedNodeAttrs: '', + phaseEnabled: true, + }, + frozen: { + ...defaultNewFrozenPhase, + dataTierAllocationType: 'custom', + selectedNodeAttrs: '', + phaseEnabled: true, + }, + delete: { ...defaultNewDeletePhase }, + }, + }, + { + name: 'test', + phases: { + hot: { actions: {} }, + warm: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + cold: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + frozen: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + }, + } + ) + ).toEqual({ + // There should be no allocation action in any phases... + name: 'test', + phases: { + hot: { + actions: { + rollover: { + max_age: '30d', + max_size: '50gb', + }, + set_priority: { + priority: 100, + }, + }, + }, + warm: { + actions: { + allocate: { include: {}, exclude: {}, require: { something: 'here' } }, + set_priority: { + priority: 50, + }, + }, + }, + cold: { + actions: { + allocate: { include: {}, exclude: {}, require: { something: 'here' } }, + set_priority: { + priority: 0, + }, + }, + min_age: '0d', + }, + frozen: { + actions: { + allocate: { include: {}, exclude: {}, require: { something: 'here' } }, + set_priority: { + priority: 0, + }, + }, + min_age: '0d', + }, + }, + }); + }); + + test('serialize a policy using "none" data allocation with no node attributes', () => { + expect( + serializePolicy( + { + name: 'test', + phases: { + hot: { ...defaultNewHotPhase }, + warm: { + ...defaultNewWarmPhase, + dataTierAllocationType: 'none', + selectedNodeAttrs: 'ignore:this', + phaseEnabled: true, + }, + cold: { + ...defaultNewColdPhase, + dataTierAllocationType: 'none', + selectedNodeAttrs: 'ignore:this', + phaseEnabled: true, + }, + frozen: { + ...defaultNewFrozenPhase, + dataTierAllocationType: 'none', + selectedNodeAttrs: 'ignore:this', + phaseEnabled: true, + }, + delete: { ...defaultNewDeletePhase }, + }, + }, + { + name: 'test', + phases: { + hot: { actions: {} }, + warm: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + cold: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + frozen: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + }, + } + ) + ).toEqual({ + // There should be no allocation action in any phases... + name: 'test', + phases: { + hot: { + actions: { + rollover: { + max_age: '30d', + max_size: '50gb', + }, + set_priority: { + priority: 100, + }, + }, + }, + warm: { + actions: { + migrate: { + enabled: false, + }, + set_priority: { + priority: 50, + }, + }, + }, + cold: { + actions: { + migrate: { + enabled: false, + }, + set_priority: { + priority: 0, + }, + }, + min_age: '0d', + }, + frozen: { + actions: { + migrate: { + enabled: false, + }, + set_priority: { + priority: 0, + }, + }, + min_age: '0d', + }, + }, + }); + }); + + test('serialization does not alter the original policy', () => { + const originalPolicy = { + name: 'test', + phases: { + hot: { actions: {} }, + warm: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + cold: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + frozen: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + }, + }; + + const originalClone = cloneDeep(originalPolicy); + + const deserializedPolicy = { + name: 'test', + phases: { + hot: { ...defaultNewHotPhase }, + warm: { + ...defaultNewWarmPhase, + dataTierAllocationType: 'none' as DataTierAllocationType, + selectedNodeAttrs: 'ignore:this', + phaseEnabled: true, + }, + cold: { + ...defaultNewColdPhase, + dataTierAllocationType: 'none' as DataTierAllocationType, + selectedNodeAttrs: 'ignore:this', + phaseEnabled: true, + }, + frozen: { + ...defaultNewFrozenPhase, + dataTierAllocationType: 'none' as DataTierAllocationType, + selectedNodeAttrs: 'ignore:this', + phaseEnabled: true, + }, + delete: { ...defaultNewDeletePhase }, + }, + }; + + serializePolicy(deserializedPolicy, originalPolicy); + deserializedPolicy.phases.warm.dataTierAllocationType = 'custom'; + serializePolicy(deserializedPolicy, originalPolicy); + deserializedPolicy.phases.warm.dataTierAllocationType = 'default'; + serializePolicy(deserializedPolicy, originalPolicy); + expect(originalPolicy).toEqual(originalClone); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/index.ts new file mode 100644 index 0000000000000..fe97b85778a53 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { serializePhaseWithAllocation } from './serialize_phase_with_allocation'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/serialize_phase_with_allocation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/serialize_phase_with_allocation.ts new file mode 100644 index 0000000000000..5a9db3069aea6 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/serialize_phase_with_allocation.ts @@ -0,0 +1,40 @@ +/* + * 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/cloneDeep'; + +import { + AllocateAction, + PhaseWithAllocationAction, + SerializedPhase, +} from '../../../../../common/types'; + +export const serializePhaseWithAllocation = ( + phase: PhaseWithAllocationAction, + originalPhaseActions: SerializedPhase['actions'] = {} +): SerializedPhase['actions'] => { + const esPhaseActions: SerializedPhase['actions'] = cloneDeep(originalPhaseActions); + + if (phase.dataTierAllocationType === 'custom' && phase.selectedNodeAttrs) { + const [name, value] = phase.selectedNodeAttrs.split(':'); + esPhaseActions.allocate = esPhaseActions.allocate || ({} as AllocateAction); + esPhaseActions.allocate.require = { + [name]: value, + }; + } else if (phase.dataTierAllocationType === 'none') { + esPhaseActions.migrate = { enabled: false }; + if (esPhaseActions.allocate) { + delete esPhaseActions.allocate; + } + } else if (phase.dataTierAllocationType === 'default') { + if (esPhaseActions.allocate) { + delete esPhaseActions.allocate.require; + } + delete esPhaseActions.migrate; + } + + return esPhaseActions; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts index cc815d67dbc18..6971f652f986b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts @@ -16,6 +16,9 @@ import { positiveNumbersAboveZeroErrorMessage, } from './policy_validation'; +import { determineDataTierAllocationType } from '../../lib'; +import { serializePhaseWithAllocation } from './shared'; + const warmPhaseInitialization: WarmPhase = { phaseEnabled: false, warmPhaseOnRollover: false, @@ -28,6 +31,7 @@ const warmPhaseInitialization: WarmPhase = { forceMergeEnabled: false, selectedForceMergeSegments: '', phaseIndexPriority: '', + dataTierAllocationType: 'default', }; export const warmPhaseFromES = (phaseSerialized?: SerializedWarmPhase): WarmPhase => { @@ -39,6 +43,12 @@ export const warmPhaseFromES = (phaseSerialized?: SerializedWarmPhase): WarmPhas phase.phaseEnabled = true; + if (phaseSerialized.actions.allocate) { + phase.dataTierAllocationType = determineDataTierAllocationType( + phaseSerialized.actions.allocate + ); + } + if (phaseSerialized.min_age) { if (phaseSerialized.min_age === '0ms') { phase.warmPhaseOnRollover = true; @@ -99,19 +109,7 @@ export const warmPhaseToES = ( delete esPhase.min_age; } - esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {}; - - if (phase.selectedNodeAttrs) { - const [name, value] = phase.selectedNodeAttrs.split(':'); - esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); - esPhase.actions.allocate.require = { - [name]: value, - }; - } else { - if (esPhase.actions.allocate) { - delete esPhase.actions.allocate.require; - } - } + esPhase.actions = serializePhaseWithAllocation(phase, esPhase.actions); if (isNumber(phase.selectedReplicaCount)) { esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts index b30a59c997e87..99df70e7df82d 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts @@ -6,22 +6,45 @@ import { LegacyAPICaller } from 'src/core/server'; +import { ListNodesRouteResponse, NodeDataRole } from '../../../../common/types'; + import { RouteDependencies } from '../../../types'; import { addBasePath } from '../../../services'; -function convertStatsIntoList(stats: any, disallowedNodeAttributes: string[]): any { - return Object.entries(stats.nodes).reduce((accum: any, [nodeId, nodeStats]: [any, any]) => { - const attributes = nodeStats.attributes || {}; - for (const [key, value] of Object.entries(attributes)) { - const isNodeAttributeAllowed = !disallowedNodeAttributes.includes(key); - if (isNodeAttributeAllowed) { - const attributeString = `${key}:${value}`; - accum[attributeString] = accum[attributeString] || []; - accum[attributeString].push(nodeId); +interface Stats { + nodes: { + [nodeId: string]: { + attributes: Record; + roles: string[]; + }; + }; +} + +function convertStatsIntoList( + stats: Stats, + disallowedNodeAttributes: string[] +): ListNodesRouteResponse { + return Object.entries(stats.nodes).reduce( + (accum, [nodeId, nodeStats]) => { + const attributes = nodeStats.attributes || {}; + for (const [key, value] of Object.entries(attributes)) { + const isNodeAttributeAllowed = !disallowedNodeAttributes.includes(key); + if (isNodeAttributeAllowed) { + const attributeString = `${key}:${value}`; + accum.nodesByAttributes[attributeString] = accum.nodesByAttributes[attributeString] ?? []; + accum.nodesByAttributes[attributeString].push(nodeId); + } + } + + const dataRoles = nodeStats.roles.filter((r) => r.startsWith('data')) as NodeDataRole[]; + for (const role of dataRoles) { + accum.nodesByRoles[role as NodeDataRole] = accum.nodesByRoles[role] ?? []; + accum.nodesByRoles[role as NodeDataRole]!.push(nodeId); } - } - return accum; - }, {}); + return accum; + }, + { nodesByAttributes: {}, nodesByRoles: {} } as ListNodesRouteResponse + ); } async function fetchNodeStats(callAsCurrentUser: LegacyAPICaller): Promise { @@ -54,8 +77,8 @@ export function registerListRoute({ router, config, license, lib }: RouteDepende const stats = await fetchNodeStats( context.core.elasticsearch.legacy.client.callAsCurrentUser ); - const okResponse = { body: convertStatsIntoList(stats, disallowedNodeAttributes) }; - return response.ok(okResponse); + const body: ListNodesRouteResponse = convertStatsIntoList(stats, disallowedNodeAttributes); + return response.ok({ body }); } catch (e) { if (lib.isEsError(e)) { return response.customError({ diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts index ba6b8665479a9..5ef38a0e46dc3 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts @@ -40,6 +40,8 @@ const setPrioritySchema = schema.maybe( const unfollowSchema = schema.maybe(schema.object({})); // Unfollow has no options +const migrateSchema = schema.maybe(schema.object({ enabled: schema.literal(false) })); + const allocateNodeSchema = schema.maybe(schema.recordOf(schema.string(), schema.string())); const allocateSchema = schema.maybe( schema.object({ @@ -76,6 +78,7 @@ const warmPhaseSchema = schema.maybe( schema.object({ min_age: minAgeSchema, actions: schema.object({ + migrate: migrateSchema, set_priority: setPrioritySchema, unfollow: unfollowSchema, readonly: schema.maybe(schema.object({})), // Readonly has no options @@ -94,6 +97,7 @@ const coldPhaseSchema = schema.maybe( schema.object({ min_age: minAgeSchema, actions: schema.object({ + migrate: migrateSchema, set_priority: setPrioritySchema, unfollow: unfollowSchema, allocate: allocateSchema, @@ -111,6 +115,7 @@ const frozenPhaseSchema = schema.maybe( schema.object({ min_age: minAgeSchema, actions: schema.object({ + migrate: migrateSchema, set_priority: setPrioritySchema, unfollow: unfollowSchema, allocate: allocateSchema, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 916dab88d8b73..76a3b58a8f3e3 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7978,7 +7978,6 @@ "xpack.indexLifecycleMgmt.appTitle": "インデックスライフサイクルポリシー", "xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel": "インデックスを凍結", "xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel": "複製の数", - "xpack.indexLifecycleMgmt.coldPhase.replicaCountHelpText": "デフォルトで、複製の数は同じままになります。", "xpack.indexLifecycleMgmt.confirmDelete.cancelButton": "キャンセル", "xpack.indexLifecycleMgmt.confirmDelete.deleteButton": "削除", "xpack.indexLifecycleMgmt.confirmDelete.errorMessage": "ポリシー {policyName} の削除中にエラーが発生しました", @@ -7986,7 +7985,6 @@ "xpack.indexLifecycleMgmt.confirmDelete.title": "ポリシー「{name}」が削除されました", "xpack.indexLifecycleMgmt.confirmDelete.undoneWarning": "削除されたポリシーは復元できません。", "xpack.indexLifecycleMgmt.editPolicy.cancelButton": "キャンセル", - "xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateWarmPhaseSwitchLabel": "コールドフェーズを有効にする", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseDescriptionText": "インデックスへのクエリの頻度を減らすことで、大幅に性能が低いハードウェアにシャードを割り当てることができます。クエリが遅いため、複製の数を減らすことができます。", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseLabel": "コールドフェーズ", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText": "凍結されたインデックスはクラスターにほとんどオーバーヘッドがなく、書き込みオペレーションがブロックされます。凍結されたインデックスは検索できますが、クエリが遅くなります。", @@ -8032,7 +8030,6 @@ "xpack.indexLifecycleMgmt.editPolicy.maximumIndexSizeMissingError": "最大インデックスサイズが必要です。", "xpack.indexLifecycleMgmt.editPolicy.nameLabel": "名前", "xpack.indexLifecycleMgmt.editPolicy.nodeAllocationLabel": "シャードの割当をコントロールするノード属性を選択", - "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingDescription": "ノード属性なしではシャードの割り当てをコントロールできません。", "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel": "elasticsearch.yml でノード属性が構成されていません", "xpack.indexLifecycleMgmt.editPolicy.numberRequiredError": "数字が必要です。", "xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel": "コールドフェーズのタイミング", @@ -8188,7 +8185,6 @@ "xpack.indexLifecycleMgmt.warmPhase.numberOfPrimaryShardsLabel": "プライマリシャードの数", "xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel": "レプリカの数", "xpack.indexLifecycleMgmt.warmPhase.numberOfSegmentsLabel": "セグメントの数", - "xpack.indexLifecycleMgmt.warmPhase.replicaCountHelpText": "デフォルトで、レプリカの数は同じままになります。", "xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel": "インデックスを縮小", "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "なし(グループなし)", "xpack.infra.alerting.alertFlyout.groupByLabel": "グループ分けの条件", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ffaf281487fd0..89c7a03e099d3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7982,7 +7982,6 @@ "xpack.indexLifecycleMgmt.appTitle": "索引生命周期策略", "xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel": "冻结索引", "xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel": "副本分片数目", - "xpack.indexLifecycleMgmt.coldPhase.replicaCountHelpText": "默认情况下,副本分片数目仍一样。", "xpack.indexLifecycleMgmt.confirmDelete.cancelButton": "取消", "xpack.indexLifecycleMgmt.confirmDelete.deleteButton": "删除", "xpack.indexLifecycleMgmt.confirmDelete.errorMessage": "删除策略 {policyName} 时出错", @@ -7990,7 +7989,6 @@ "xpack.indexLifecycleMgmt.confirmDelete.title": "删除策略“{name}”", "xpack.indexLifecycleMgmt.confirmDelete.undoneWarning": "无法恢复删除的策略。", "xpack.indexLifecycleMgmt.editPolicy.cancelButton": "取消", - "xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateWarmPhaseSwitchLabel": "激活冷阶段", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseDescriptionText": "您查询自己索引的频率较低,因此您可以在效率较低的硬件上分配分片。因为您的查询较为缓慢,所以您可以减少副本分片数目。", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseLabel": "冷阶段", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText": "冻结的索引在集群上有很少的开销,已被阻止进行写操作。您可以搜索冻结的索引,但查询应会较慢。", @@ -8036,7 +8034,6 @@ "xpack.indexLifecycleMgmt.editPolicy.maximumIndexSizeMissingError": "最大索引大小必填。", "xpack.indexLifecycleMgmt.editPolicy.nameLabel": "名称", "xpack.indexLifecycleMgmt.editPolicy.nodeAllocationLabel": "选择节点属性来控制分片分配", - "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingDescription": "没有节点属性,将无法控制分片分配。", "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel": "elasticsearch.yml 中未配置任何节点属性", "xpack.indexLifecycleMgmt.editPolicy.numberRequiredError": "数字必填。", "xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel": "冷阶段计时", @@ -8192,7 +8189,6 @@ "xpack.indexLifecycleMgmt.warmPhase.numberOfPrimaryShardsLabel": "主分片数目", "xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel": "副本分片数目", "xpack.indexLifecycleMgmt.warmPhase.numberOfSegmentsLabel": "段数目", - "xpack.indexLifecycleMgmt.warmPhase.replicaCountHelpText": "默认情况下,副本分片数目仍一样。", "xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel": "缩小索引", "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "无内容(未分组)", "xpack.infra.alerting.alertFlyout.groupByLabel": "分组依据", diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/nodes.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/nodes.js index bc8b2af401423..bb35f6fd96429 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/nodes.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/nodes.js @@ -29,7 +29,7 @@ export default function ({ getService }) { const nodesIds = Object.keys(nodeStats.nodes); const { body } = await loadNodes().expect(200); - expect(body[NODE_CUSTOM_ATTRIBUTE]).to.eql(nodesIds); + expect(body.nodesByAttributes[NODE_CUSTOM_ATTRIBUTE]).to.eql(nodesIds); }); }); From 6408fa54ed78ca03c82631aa03255e6e26f4f645 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Fri, 18 Sep 2020 12:59:45 +0200 Subject: [PATCH 16/25] [Security Solution] Adds "Creates timeline" Cypress test (#76836) * adds "Creates timeline" test * deletes timeline events spec * completes assertions * comments assertion * fixes typecheck error * waits for all the changes in the timeline to be performed before creating a new timeline and closing the toggle * fixes failing problem * fixes loop script * makes test realiable on visual mode * fixes merge issue * makes test more reliable * fixes typecheck issue * fixes typecheck * opens timeline from timeline settings Co-authored-by: Elastic Machine --- package.json | 1 + x-pack/package.json | 1 + .../security_solution/cypress/.eslintrc.json | 3 + .../integration/fields_browser.spec.ts | 6 +- .../cypress/integration/inspect.spec.ts | 4 +- .../integration/timeline_creation.spec.ts | 99 +++++++++++++++++++ .../timeline_data_providers.spec.ts | 4 +- .../integration/timeline_events.spec.ts | 39 -------- .../timeline_flyout_button.spec.ts | 4 +- .../timeline_search_or_filter.spec.ts | 4 +- .../timeline_toggle_column.spec.ts | 4 +- .../integration/timelines_export.spec.ts | 2 +- .../cypress/integration/url_state.spec.ts | 10 +- .../cypress/objects/timeline.ts | 24 +++++ .../cypress/screens/timeline.ts | 45 +++++++-- .../cypress/screens/timelines.ts | 29 ++++++ .../cypress/support/index.d.ts | 1 + .../cypress/support/index.js | 1 + .../cypress/tasks/create_new_rule.ts | 2 +- .../cypress/tasks/security_main.ts | 4 +- .../cypress/tasks/timeline.ts | 67 +++++++++---- .../cypress/tasks/timelines.ts | 27 +++++ x-pack/plugins/security_solution/package.json | 2 +- .../add_data_provider_popover.tsx | 4 +- .../timeline/properties/helpers.tsx | 6 +- .../scripts/loop_cypress_tests.js | 4 +- yarn.lock | 5 + 27 files changed, 307 insertions(+), 95 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts delete mode 100644 x-pack/plugins/security_solution/cypress/integration/timeline_events.spec.ts create mode 100644 x-pack/plugins/security_solution/cypress/screens/timelines.ts create mode 100644 x-pack/plugins/security_solution/cypress/tasks/timelines.ts diff --git a/package.json b/package.json index c88fbc0e5fd07..d258d21413fe4 100644 --- a/package.json +++ b/package.json @@ -154,6 +154,7 @@ "color": "1.0.3", "commander": "3.0.2", "core-js": "^3.6.4", + "cypress-promise": "^1.1.0", "deep-freeze-strict": "^1.1.1", "del": "^5.1.0", "elastic-apm-node": "^3.7.0", diff --git a/x-pack/package.json b/x-pack/package.json index de0bf0d922b42..9a1d424da4a1d 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -160,6 +160,7 @@ "cronstrue": "^1.51.0", "cypress": "5.0.0", "cypress-multi-reporters": "^1.2.3", + "cypress-promise": "^1.1.0", "d3": "3.5.17", "d3-scale": "1.0.7", "dragselect": "1.13.1", diff --git a/x-pack/plugins/security_solution/cypress/.eslintrc.json b/x-pack/plugins/security_solution/cypress/.eslintrc.json index 96a5a52f13e6c..a738652e2d27b 100644 --- a/x-pack/plugins/security_solution/cypress/.eslintrc.json +++ b/x-pack/plugins/security_solution/cypress/.eslintrc.json @@ -2,5 +2,8 @@ "plugins": ["cypress"], "env": { "cypress/globals": true + }, + "rules": { + "import/no-extraneous-dependencies": "off" } } diff --git a/x-pack/plugins/security_solution/cypress/integration/fields_browser.spec.ts b/x-pack/plugins/security_solution/cypress/integration/fields_browser.spec.ts index 6438a738580b7..e09d62d2a87d1 100644 --- a/x-pack/plugins/security_solution/cypress/integration/fields_browser.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/fields_browser.spec.ts @@ -28,7 +28,7 @@ import { resetFields, } from '../tasks/fields_browser'; import { loginAndWaitForPage } from '../tasks/login'; -import { openTimeline } from '../tasks/security_main'; +import { openTimelineUsingToggle } from '../tasks/security_main'; import { openTimelineFieldsBrowser, populateTimeline } from '../tasks/timeline'; import { HOSTS_URL } from '../urls/navigation'; @@ -48,7 +48,7 @@ describe('Fields Browser', () => { context('Fields Browser rendering', () => { before(() => { loginAndWaitForPage(HOSTS_URL); - openTimeline(); + openTimelineUsingToggle(); populateTimeline(); openTimelineFieldsBrowser(); }); @@ -111,7 +111,7 @@ describe('Fields Browser', () => { context('Editing the timeline', () => { before(() => { loginAndWaitForPage(HOSTS_URL); - openTimeline(); + openTimelineUsingToggle(); populateTimeline(); openTimelineFieldsBrowser(); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts b/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts index 53ddff501db82..c19e51c3ada40 100644 --- a/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts @@ -12,7 +12,7 @@ import { import { closesModal, openStatsAndTables } from '../tasks/inspect'; import { loginAndWaitForPage } from '../tasks/login'; -import { openTimeline } from '../tasks/security_main'; +import { openTimelineUsingToggle } from '../tasks/security_main'; import { executeTimelineKQL, openTimelineInspectButton, @@ -58,7 +58,7 @@ describe('Inspect', () => { it('inspects the timeline', () => { const hostExistsQuery = 'host.name: *'; loginAndWaitForPage(HOSTS_URL); - openTimeline(); + openTimelineUsingToggle(); executeTimelineKQL(hostExistsQuery); openTimelineSettings(); openTimelineInspectButton(); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts new file mode 100644 index 0000000000000..9f61d11b7ac0f --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts @@ -0,0 +1,99 @@ +/* + * 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 { timeline } from '../objects/timeline'; + +import { + FAVORITE_TIMELINE, + LOCKED_ICON, + NOTES, + NOTES_BUTTON, + NOTES_COUNT, + NOTES_TEXT_AREA, + PIN_EVENT, + TIMELINE_DESCRIPTION, + // TIMELINE_FILTER, + TIMELINE_QUERY, + TIMELINE_TITLE, +} from '../screens/timeline'; +import { + TIMELINES_DESCRIPTION, + TIMELINES_PINNED_EVENT_COUNT, + TIMELINES_NOTES_COUNT, + TIMELINES_FAVORITE, +} from '../screens/timelines'; + +import { loginAndWaitForPage } from '../tasks/login'; +import { openTimelineUsingToggle } from '../tasks/security_main'; +import { + addDescriptionToTimeline, + addFilter, + addNameToTimeline, + addNotesToTimeline, + closeNotes, + closeTimeline, + createNewTimeline, + markAsFavorite, + openTimelineFromSettings, + pinFirstEvent, + populateTimeline, + waitForTimelineChanges, +} from '../tasks/timeline'; +import { openTimeline } from '../tasks/timelines'; + +import { OVERVIEW_URL } from '../urls/navigation'; + +describe('Timelines', () => { + before(() => { + cy.server(); + cy.route('PATCH', '**/api/timeline').as('timeline'); + }); + + it('Creates a timeline', async () => { + loginAndWaitForPage(OVERVIEW_URL); + openTimelineUsingToggle(); + populateTimeline(); + addFilter(timeline.filter); + pinFirstEvent(); + + cy.get(PIN_EVENT).should('have.attr', 'aria-label', 'Pinned event'); + cy.get(LOCKED_ICON).should('be.visible'); + + addNameToTimeline(timeline.title); + + const response = await cy.wait('@timeline').promisify(); + const timelineId = JSON.parse(response.xhr.responseText).data.persistTimeline.timeline + .savedObjectId; + + addDescriptionToTimeline(timeline.description); + addNotesToTimeline(timeline.notes); + closeNotes(); + markAsFavorite(); + waitForTimelineChanges(); + createNewTimeline(); + closeTimeline(); + openTimelineFromSettings(); + + cy.contains(timeline.title).should('exist'); + cy.get(TIMELINES_DESCRIPTION).first().should('have.text', timeline.description); + cy.get(TIMELINES_PINNED_EVENT_COUNT).first().should('have.text', '1'); + cy.get(TIMELINES_NOTES_COUNT).first().should('have.text', '1'); + cy.get(TIMELINES_FAVORITE).first().should('exist'); + + openTimeline(timelineId); + + cy.get(FAVORITE_TIMELINE).should('exist'); + cy.get(TIMELINE_TITLE).should('have.attr', 'value', timeline.title); + cy.get(TIMELINE_DESCRIPTION).should('have.attr', 'value', timeline.description); + cy.get(TIMELINE_QUERY).should('have.text', timeline.query); + // Comments this assertion until we agreed what to do with the filters. + // cy.get(TIMELINE_FILTER(timeline.filter)).should('exist'); + cy.get(NOTES_COUNT).should('have.text', '1'); + cy.get(PIN_EVENT).should('have.attr', 'aria-label', 'Pinned event'); + cy.get(NOTES_BUTTON).click(); + cy.get(NOTES_TEXT_AREA).should('have.attr', 'placeholder', 'Add a Note'); + cy.get(NOTES).should('have.text', timeline.notes); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts index df0a26f3649c0..f62db083172a4 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts @@ -19,7 +19,7 @@ import { } from '../tasks/hosts/all_hosts'; import { loginAndWaitForPage } from '../tasks/login'; -import { openTimeline } from '../tasks/security_main'; +import { openTimelineUsingToggle } from '../tasks/security_main'; import { createNewTimeline } from '../tasks/timeline'; import { HOSTS_URL } from '../urls/navigation'; @@ -31,7 +31,7 @@ describe('timeline data providers', () => { }); beforeEach(() => { - openTimeline(); + openTimelineUsingToggle(); }); afterEach(() => { diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_events.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_events.spec.ts deleted file mode 100644 index 549cd134a04a4..0000000000000 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_events.spec.ts +++ /dev/null @@ -1,39 +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 { PIN_EVENT } from '../screens/timeline'; - -import { loginAndWaitForPage } from '../tasks/login'; -import { openTimeline } from '../tasks/security_main'; -import { pinFirstEvent, populateTimeline, unpinFirstEvent } from '../tasks/timeline'; - -import { HOSTS_URL } from '../urls/navigation'; - -describe('timeline events', () => { - before(() => { - loginAndWaitForPage(HOSTS_URL); - openTimeline(); - populateTimeline(); - }); - - after(() => { - unpinFirstEvent(); - }); - - it('pins the first event to the timeline', () => { - cy.server(); - cy.route('POST', '**/api/solutions/security/graphql').as('persistTimeline'); - - pinFirstEvent(); - - cy.wait('@persistTimeline', { timeout: 10000 }).then((response) => { - cy.wrap(response.status).should('eql', 200); - cy.wrap(response.xhr.responseText).should('include', 'persistPinnedEventOnTimeline'); - }); - - cy.get(PIN_EVENT).should('have.attr', 'aria-label', 'Pinned event'); - }); -}); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_flyout_button.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_flyout_button.spec.ts index 87639f41d4109..9b3434b5521d4 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_flyout_button.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_flyout_button.spec.ts @@ -8,7 +8,7 @@ import { TIMELINE_FLYOUT_HEADER, TIMELINE_NOT_READY_TO_DROP_BUTTON } from '../sc import { dragFirstHostToTimeline, waitForAllHostsToBeLoaded } from '../tasks/hosts/all_hosts'; import { loginAndWaitForPage } from '../tasks/login'; -import { openTimeline, openTimelineIfClosed } from '../tasks/security_main'; +import { openTimelineUsingToggle, openTimelineIfClosed } from '../tasks/security_main'; import { createNewTimeline } from '../tasks/timeline'; import { HOSTS_URL } from '../urls/navigation'; @@ -25,7 +25,7 @@ describe('timeline flyout button', () => { }); it('toggles open the timeline', () => { - openTimeline(); + openTimelineUsingToggle(); cy.get(TIMELINE_FLYOUT_HEADER).should('have.css', 'visibility', 'visible'); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_search_or_filter.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_search_or_filter.spec.ts index a2e2a72a17946..814fcee2b0c5f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_search_or_filter.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_search_or_filter.spec.ts @@ -7,7 +7,7 @@ import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline'; import { loginAndWaitForPage } from '../tasks/login'; -import { openTimeline } from '../tasks/security_main'; +import { openTimelineUsingToggle } from '../tasks/security_main'; import { executeTimelineKQL } from '../tasks/timeline'; import { HOSTS_URL } from '../urls/navigation'; @@ -19,7 +19,7 @@ describe('timeline search or filter KQL bar', () => { it('executes a KQL query', () => { const hostExistsQuery = 'host.name: *'; - openTimeline(); + openTimelineUsingToggle(); executeTimelineKQL(hostExistsQuery); cy.get(SERVER_SIDE_EVENT_COUNT) diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts index 12e6f3db9b61e..e4f303fb89fda 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts @@ -12,7 +12,7 @@ import { } from '../screens/timeline'; import { loginAndWaitForPage } from '../tasks/login'; -import { openTimeline } from '../tasks/security_main'; +import { openTimelineUsingToggle } from '../tasks/security_main'; import { checkIdToggleField, createNewTimeline, @@ -30,7 +30,7 @@ describe('toggle column in timeline', () => { }); beforeEach(() => { - openTimeline(); + openTimelineUsingToggle(); populateTimeline(); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines_export.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines_export.spec.ts index d8f96aaf5e563..103bbaad8f303 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines_export.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines_export.spec.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { exportTimeline, waitForTimelinesPanelToBeLoaded } from '../tasks/timeline'; +import { exportTimeline, waitForTimelinesPanelToBeLoaded } from '../tasks/timelines'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; diff --git a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts index 6d605e1d577a9..6c1d73492f30a 100644 --- a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts @@ -30,7 +30,7 @@ import { openAllHosts } from '../tasks/hosts/main'; import { waitForIpsTableToBeLoaded } from '../tasks/network/flows'; import { clearSearchBar, kqlSearch, navigateFromHeaderTo } from '../tasks/security_header'; -import { openTimeline } from '../tasks/security_main'; +import { openTimelineUsingToggle } from '../tasks/security_main'; import { addDescriptionToTimeline, addNameToTimeline, @@ -82,7 +82,7 @@ describe('url state', () => { it('sets the timeline start and end dates from the url when locked to global time', () => { loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.url); - openTimeline(); + openTimelineUsingToggle(); cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE).should( 'have.attr', @@ -105,7 +105,7 @@ describe('url state', () => { ); cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON).should('have.attr', 'title', ABSOLUTE_DATE.endTime); - openTimeline(); + openTimelineUsingToggle(); cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE).should( 'have.attr', @@ -121,7 +121,7 @@ describe('url state', () => { it('sets the url state when timeline/global date pickers are unlinked and timeline start and end date are set', () => { loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.urlUnlinked); - openTimeline(); + openTimelineUsingToggle(); setTimelineStartDate(ABSOLUTE_DATE.newStartTimeTyped); updateTimelineDates(); setTimelineEndDate(ABSOLUTE_DATE.newEndTimeTyped); @@ -220,7 +220,7 @@ describe('url state', () => { it('sets and reads the url state for timeline by id', () => { loginAndWaitForPage(HOSTS_URL); - openTimeline(); + openTimelineUsingToggle(); executeTimelineKQL('host.name: *'); cy.get(SERVER_SIDE_EVENT_COUNT) diff --git a/x-pack/plugins/security_solution/cypress/objects/timeline.ts b/x-pack/plugins/security_solution/cypress/objects/timeline.ts index ff7e80e5661ad..6121cb9a99b14 100644 --- a/x-pack/plugins/security_solution/cypress/objects/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/objects/timeline.ts @@ -10,6 +10,30 @@ export interface Timeline { query: string; } +export interface CompleteTimeline extends Timeline { + notes: string; + filter: TimelineFilter; +} + +export interface TimelineFilter { + field: string; + operator: string; + value?: string; +} + export interface TimelineWithId extends Timeline { id: string; } + +export const filter: TimelineFilter = { + field: 'host.name', + operator: 'exists', +}; + +export const timeline: CompleteTimeline = { + title: 'Security Timeline', + description: 'This is the best timeline', + query: 'host.name: * ', + notes: 'Yes, the best timeline', + filter, +}; diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index bcb64fc947feb..94255a2af8976 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -4,6 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { TimelineFilter } from '../objects/timeline'; + +export const ADD_NOTE_BUTTON = '[data-test-subj="add-note"]'; + +export const ADD_FILTER = '[data-test-subj="timeline"] [data-test-subj="addFilter"]'; + export const ATTACH_TIMELINE_TO_NEW_CASE_ICON = '[data-test-subj="attach-timeline-case"]'; export const ATTACH_TIMELINE_TO_EXISTING_CASE_ICON = @@ -15,14 +21,18 @@ export const CASE = (id: string) => { return `[data-test-subj="cases-table-row-${id}"]`; }; +export const CLOSE_NOTES_BTN = '[data-test-subj="notesModal"] .euiButtonIcon'; + export const CLOSE_TIMELINE_BTN = '[data-test-subj="close-timeline"]'; +export const COMBO_BOX = '.euiComboBoxOption__content'; + export const CREATE_NEW_TIMELINE = '[data-test-subj="timeline-new"]'; export const DRAGGABLE_HEADER = '[data-test-subj="events-viewer-panel"] [data-test-subj="headers-group"] [data-test-subj="draggable-header"]'; -export const EXPORT_TIMELINE_ACTION = '[data-test-subj="export-timeline-action"]'; +export const FAVORITE_TIMELINE = '[data-test-subj="timeline-favorite-filled-star"]'; export const HEADER = '[data-test-subj="header"]'; @@ -34,6 +44,16 @@ export const ID_FIELD = '[data-test-subj="timeline"] [data-test-subj="field-name export const ID_TOGGLE_FIELD = '[data-test-subj="toggle-field-_id"]'; +export const LOCKED_ICON = '[data-test-subj="timeline-date-picker-lock-button"]'; + +export const NOTES = '[data-test-subj="markdown-root"]'; + +export const NOTES_TEXT_AREA = '[data-test-subj="add-a-note"]'; + +export const NOTES_BUTTON = '[data-test-subj="timeline-notes-button-large"]'; + +export const NOTES_COUNT = '[data-test-subj="timeline-notes-count"]'; + export const OPEN_TIMELINE_ICON = '[data-test-subj="open-timeline-button"]'; export const PIN_EVENT = '[data-test-subj="pin"]'; @@ -45,21 +65,17 @@ export const REMOVE_COLUMN = '[data-test-subj="remove-column"]'; export const RESET_FIELDS = '[data-test-subj="events-viewer-panel"] [data-test-subj="reset-fields"]'; +export const SAVE_FILTER_BTN = '[data-test-subj="saveFilter"]'; + export const SEARCH_OR_FILTER_CONTAINER = '[data-test-subj="timeline-search-or-filter-search-container"]'; export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]'; -export const TIMELINE = (id: string) => { - return `[data-test-subj="title-${id}"]`; -}; +export const STAR_ICON = '[data-test-subj="timeline-favorite-empty-star"]'; export const TIMELINE_CHANGES_IN_PROGRESS = '[data-test-subj="timeline"] .euiProgress'; -export const TIMELINE_CHECKBOX = (id: string) => { - return `[data-test-subj="checkboxSelectRow-${id}"]`; -}; - export const TIMELINE_COLUMN_SPINNER = '[data-test-subj="timeline-loading-spinner"]'; export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]'; @@ -74,6 +90,17 @@ export const TIMELINE_DROPPED_DATA_PROVIDERS = '[data-test-subj="providerContain export const TIMELINE_FIELDS_BUTTON = '[data-test-subj="timeline"] [data-test-subj="show-field-browser"]'; +export const TIMELINE_FILTER = (filter: TimelineFilter) => { + return `[data-test-subj="filter filter-enabled filter-key-${filter.field} filter-value-${filter.value} filter-unpinned"]`; +}; + +export const TIMELINE_FILTER_FIELD = '[data-test-subj="filterFieldSuggestionList"]'; + +export const TIMELINE_FILTER_OPERATOR = '[data-test-subj="filterOperatorList"]'; + +export const TIMELINE_FILTER_VALUE = + '[data-test-subj="filterParamsComboBox phraseParamsComboxBox"]'; + export const TIMELINE_FLYOUT_HEADER = '[data-test-subj="eui-flyout-header"]'; export const TIMELINE_FLYOUT_BODY = '[data-test-subj="eui-flyout-body"]'; @@ -89,8 +116,6 @@ export const TIMELINE_SETTINGS_ICON = '[data-test-subj="settings-gear"]'; export const TIMELINE_TITLE = '[data-test-subj="timeline-title"]'; -export const TIMELINES_TABLE = '[data-test-subj="timelines-table"]'; - export const TIMESTAMP_HEADER_FIELD = '[data-test-subj="header-text-@timestamp"]'; export const TIMESTAMP_TOGGLE_FIELD = '[data-test-subj="toggle-field-@timestamp"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/timelines.ts b/x-pack/plugins/security_solution/cypress/screens/timelines.ts new file mode 100644 index 0000000000000..e87e3c4f72ca5 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/screens/timelines.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const BULK_ACTIONS = '[data-test-subj="utility-bar-action-button"]'; + +export const EXPORT_TIMELINE_ACTION = '[data-test-subj="export-timeline-action"]'; + +export const TIMELINE = (id: string) => { + return `[data-test-subj="title-${id}"]`; +}; + +export const TIMELINE_CHECKBOX = (id: string) => { + return `[data-test-subj="checkboxSelectRow-${id}"]`; +}; + +export const TIMELINES_FAVORITE = '[data-test-subj="favorite-starFilled-star"]'; + +export const TIMELINES_DESCRIPTION = '[data-test-subj="description"]'; + +export const TIMELINES_NOTES_COUNT = '[data-test-subj="notes-count"]'; + +export const TIMELINES_PINNED_EVENT_COUNT = '[data-test-subj="pinned-event-count"]'; + +export const TIMELINES_TABLE = '[data-test-subj="timelines-table"]'; + +export const TIMELINES_USERNAME = '[data-test-subj="username"]'; diff --git a/x-pack/plugins/security_solution/cypress/support/index.d.ts b/x-pack/plugins/security_solution/cypress/support/index.d.ts index f66aeff5d578d..f0b0b8c92c616 100644 --- a/x-pack/plugins/security_solution/cypress/support/index.d.ts +++ b/x-pack/plugins/security_solution/cypress/support/index.d.ts @@ -6,6 +6,7 @@ declare namespace Cypress { interface Chainable { + promisify(): Promise; stubSecurityApi(dataFileName: string): Chainable; stubSearchStrategyApi(dataFileName: string): Chainable; attachFile(fileName: string, fileType?: string): Chainable; diff --git a/x-pack/plugins/security_solution/cypress/support/index.js b/x-pack/plugins/security_solution/cypress/support/index.js index 244781e0ccd01..1328bea0e2918 100644 --- a/x-pack/plugins/security_solution/cypress/support/index.js +++ b/x-pack/plugins/security_solution/cypress/support/index.js @@ -21,6 +21,7 @@ // Import commands.js using ES2015 syntax: import './commands'; +import 'cypress-promise/register'; Cypress.Cookies.defaults({ preserve: 'sid', diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 0daff52de7063..dc89a39d082bc 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -55,7 +55,7 @@ import { EQL_TYPE, EQL_QUERY_INPUT, } from '../screens/create_new_rule'; -import { TIMELINE } from '../screens/timeline'; +import { TIMELINE } from '../screens/timelines'; export const createAndActivateRule = () => { cy.get(SCHEDULE_CONTINUE_BUTTON).click({ force: true }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/security_main.ts b/x-pack/plugins/security_solution/cypress/tasks/security_main.ts index 47b73db8b96df..6b1f3699d333a 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/security_main.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/security_main.ts @@ -6,14 +6,14 @@ import { MAIN_PAGE, TIMELINE_TOGGLE_BUTTON } from '../screens/security_main'; -export const openTimeline = () => { +export const openTimelineUsingToggle = () => { cy.get(TIMELINE_TOGGLE_BUTTON).click(); }; export const openTimelineIfClosed = () => { cy.get(MAIN_PAGE).then(($page) => { if ($page.find(TIMELINE_TOGGLE_BUTTON).length === 1) { - openTimeline(); + openTimelineUsingToggle(); } }); }; diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index cd8b197fc4dec..438700bdfca80 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -4,36 +4,47 @@ * you may not use this file except in compliance with the Elastic License. */ +import { TimelineFilter } from '../objects/timeline'; + import { ALL_CASES_CREATE_NEW_CASE_TABLE_BTN } from '../screens/all_cases'; + import { - BULK_ACTIONS, + ADD_FILTER, + ADD_NOTE_BUTTON, + ATTACH_TIMELINE_TO_EXISTING_CASE_ICON, + ATTACH_TIMELINE_TO_NEW_CASE_ICON, + CASE, CLOSE_TIMELINE_BTN, + CLOSE_NOTES_BTN, + COMBO_BOX, CREATE_NEW_TIMELINE, - EXPORT_TIMELINE_ACTION, - TIMELINE_CHECKBOX, HEADER, ID_FIELD, ID_HEADER_FIELD, ID_TOGGLE_FIELD, + NOTES_BUTTON, + NOTES_TEXT_AREA, + OPEN_TIMELINE_ICON, PIN_EVENT, + REMOVE_COLUMN, + RESET_FIELDS, + SAVE_FILTER_BTN, SEARCH_OR_FILTER_CONTAINER, SERVER_SIDE_EVENT_COUNT, + STAR_ICON, TIMELINE_CHANGES_IN_PROGRESS, TIMELINE_DESCRIPTION, TIMELINE_FIELDS_BUTTON, + TIMELINE_FILTER_FIELD, + TIMELINE_FILTER_OPERATOR, + TIMELINE_FILTER_VALUE, TIMELINE_INSPECT_BUTTON, TIMELINE_SETTINGS_ICON, TIMELINE_TITLE, - TIMELINES_TABLE, TIMESTAMP_TOGGLE_FIELD, TOGGLE_TIMELINE_EXPAND_EVENT, - REMOVE_COLUMN, - RESET_FIELDS, - ATTACH_TIMELINE_TO_NEW_CASE_ICON, - OPEN_TIMELINE_ICON, - ATTACH_TIMELINE_TO_EXISTING_CASE_ICON, - CASE, } from '../screens/timeline'; +import { TIMELINES_TABLE } from '../screens/timelines'; import { drag, drop } from '../tasks/common'; @@ -49,6 +60,24 @@ export const addNameToTimeline = (name: string) => { cy.get(TIMELINE_TITLE).should('have.attr', 'value', name); }; +export const addNotesToTimeline = (notes: string) => { + cy.get(NOTES_BUTTON).click(); + cy.get(NOTES_TEXT_AREA).type(notes); + cy.get(ADD_NOTE_BUTTON).click(); +}; + +export const addFilter = (filter: TimelineFilter) => { + cy.get(ADD_FILTER).click(); + cy.get(TIMELINE_FILTER_FIELD).type(filter.field); + cy.get(COMBO_BOX).contains(filter.field).click(); + cy.get(TIMELINE_FILTER_OPERATOR).type(filter.operator); + cy.get(COMBO_BOX).contains(filter.operator).click(); + if (filter.operator !== 'exists') { + cy.get(TIMELINE_FILTER_VALUE).type(`${filter.value}{enter}`); + } + cy.get(SAVE_FILTER_BTN).click(); +}; + export const addNewCase = () => { cy.get(ALL_CASES_CREATE_NEW_CASE_TABLE_BTN).click(); }; @@ -71,6 +100,10 @@ export const checkIdToggleField = () => { }); }; +export const closeNotes = () => { + cy.get(CLOSE_NOTES_BTN).click(); +}; + export const closeTimeline = () => { cy.get(CLOSE_TIMELINE_BTN).click({ force: true }); }; @@ -89,10 +122,8 @@ export const expandFirstTimelineEventDetails = () => { cy.get(TOGGLE_TIMELINE_EXPAND_EVENT).first().click({ force: true }); }; -export const exportTimeline = (timelineId: string) => { - cy.get(TIMELINE_CHECKBOX(timelineId)).click({ force: true }); - cy.get(BULK_ACTIONS).click({ force: true }); - cy.get(EXPORT_TIMELINE_ACTION).click(); +export const markAsFavorite = () => { + cy.get(STAR_ICON).click(); }; export const openTimelineFieldsBrowser = () => { @@ -160,11 +191,11 @@ export const selectCase = (caseId: string) => { cy.get(CASE(caseId)).click(); }; -export const waitForTimelinesPanelToBeLoaded = () => { - cy.get(TIMELINES_TABLE).should('exist'); -}; - export const waitForTimelineChanges = () => { cy.get(TIMELINE_CHANGES_IN_PROGRESS).should('exist'); cy.get(TIMELINE_CHANGES_IN_PROGRESS).should('not.exist'); }; + +export const waitForTimelinesPanelToBeLoaded = () => { + cy.get(TIMELINES_TABLE).should('exist'); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/timelines.ts b/x-pack/plugins/security_solution/cypress/tasks/timelines.ts new file mode 100644 index 0000000000000..1c5ce246a35b3 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/tasks/timelines.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 { + TIMELINE_CHECKBOX, + BULK_ACTIONS, + EXPORT_TIMELINE_ACTION, + TIMELINES_TABLE, + TIMELINE, +} from '../screens/timelines'; + +export const exportTimeline = (timelineId: string) => { + cy.get(TIMELINE_CHECKBOX(timelineId)).click({ force: true }); + cy.get(BULK_ACTIONS).click({ force: true }); + cy.get(EXPORT_TIMELINE_ACTION).click(); +}; + +export const openTimeline = (id: string) => { + cy.get(TIMELINE(id)).click(); +}; + +export const waitForTimelinesPanelToBeLoaded = () => { + cy.get(TIMELINES_TABLE).should('exist'); +}; diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index fd7941fb17cc5..6982c200a5afd 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -1,4 +1,4 @@ -{ + { "author": "Elastic", "name": "security_solution", "version": "8.0.0", diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx index 71cf81c00dc09..1bfeb482873c7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx @@ -146,7 +146,7 @@ const AddDataProviderPopoverComponent: React.FC = ( = ( {`+ ${ADD_FIELD_LABEL}`} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index 4ab05af5dd6d4..0f6535015c799 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -376,7 +376,11 @@ const NotesButtonComponent = React.memo( )} {size === 'l' && showNotes ? ( - + { const exitIfTimesToRunIsInvalid = (timesToRun) => { if (!timesToRun > 0) { console.error( - '\nERROR: You must specify a valid number of times to run the SIEM Cypress tests.' + '\nERROR: You must specify a valid number of times to run the Security Solution Cypress tests.' ); showUsage(); process.exit(1); @@ -44,7 +44,7 @@ const spawnChild = async () => { const child = spawn('node', [ 'scripts/functional_tests', '--config', - 'x-pack/test/security_solution_cypress/config.ts', + 'x-pack/test/security_solution_cypress/cli_config.ts', ]); for await (const chunk of child.stdout) { console.log(chunk.toString()); diff --git a/yarn.lock b/yarn.lock index 7fb61b10c1bf7..fa5f6ff135670 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9941,6 +9941,11 @@ cypress-multi-reporters@^1.2.3: debug "^4.1.1" lodash "^4.17.11" +cypress-promise@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/cypress-promise/-/cypress-promise-1.1.0.tgz#f2d66965945fe198431aaf692d5157cea9d47b25" + integrity sha512-DhIf5PJ/a0iY+Yii6n7Rbwq+9TJxU4pupXYzf9mZd8nPG0AzQrj9i+pqINv4xbI2EV1p+PKW3maCkR7oPG4GrA== + cypress@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cypress/-/cypress-5.0.0.tgz#6957e299b790af8b1cd7bea68261b8935646f72e" From b08594ad5c2cdbd04acfd3805f1a5d244b9f71ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 18 Sep 2020 12:53:42 +0100 Subject: [PATCH 17/25] [APM] Bug: Set minimum scale to 1% for the Transaction error rate chart (#77861) --- .../shared/charts/ErroneousTransactionsRateChart/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx index e806f556347f1..e64357c085209 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx @@ -6,6 +6,7 @@ import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; +import { max } from 'lodash'; import React, { useCallback } from 'react'; import { useParams } from 'react-router-dom'; import { asPercent } from '../../../../../common/utils/formatters'; @@ -56,6 +57,7 @@ export function ErroneousTransactionsRateChart() { ); const errorRates = data?.erroneousTransactionsRate || []; + const maxRate = max(errorRates.map((errorRate) => errorRate.y)); return ( @@ -70,7 +72,7 @@ export function ErroneousTransactionsRateChart() { Date: Fri, 18 Sep 2020 14:02:37 +0200 Subject: [PATCH 18/25] [APM] Align APM severity levels with ML (#77818) --- x-pack/plugins/apm/common/alert_types.ts | 37 ++++++ .../apm/common/anomaly_detection.test.ts | 39 ------ .../plugins/apm/common/anomaly_detection.ts | 84 ++----------- .../apm/common/service_health_status.ts | 79 ++++++++++++ x-pack/plugins/apm/kibana.json | 12 +- .../SelectAnomalySeverity.tsx | 119 ++++++------------ .../index.tsx | 15 ++- .../ServiceMap/Popover/AnomalyDetection.tsx | 13 +- .../app/ServiceMap/cytoscapeOptions.ts | 33 ++--- .../ServiceList/HealthBadge.tsx | 20 +-- .../ServiceList/__test__/List.test.js | 16 ++- .../__test__/__snapshots__/List.test.js.snap | 10 +- .../app/ServiceOverview/ServiceList/index.tsx | 43 +++---- .../__test__/ServiceOverview.test.tsx | 7 +- .../ServiceOverview.test.tsx.snap | 8 +- .../components/app/ServiceOverview/index.tsx | 3 - ...transaction_duration_anomaly_alert_type.ts | 28 ++++- .../lib/service_map/get_service_anomalies.ts | 10 +- .../transform_service_map_responses.test.ts | 3 + .../get_services/get_services_items_stats.ts | 4 +- x-pack/plugins/ml/common/index.ts | 2 + x-pack/plugins/ml/kibana.json | 3 + .../__snapshots__/service_maps.snap | 36 ++++++ .../trial/tests/service_maps/service_maps.ts | 2 + .../trial/tests/services/top_services.ts | 18 +-- 25 files changed, 362 insertions(+), 282 deletions(-) delete mode 100644 x-pack/plugins/apm/common/anomaly_detection.test.ts create mode 100644 x-pack/plugins/apm/common/service_health_status.ts diff --git a/x-pack/plugins/apm/common/alert_types.ts b/x-pack/plugins/apm/common/alert_types.ts index 15a3c642faf32..a234226d18034 100644 --- a/x-pack/plugins/apm/common/alert_types.ts +++ b/x-pack/plugins/apm/common/alert_types.ts @@ -5,6 +5,8 @@ */ import { i18n } from '@kbn/i18n'; +import { ValuesType } from 'utility-types'; +import { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from '../../ml/common'; export enum AlertType { ErrorCount = 'apm.error_rate', // ErrorRate was renamed to ErrorCount but the key is kept as `error_rate` for backwards-compat. @@ -55,6 +57,41 @@ export const ALERT_TYPES_CONFIG = { }, }; +export const ANOMALY_ALERT_SEVERITY_TYPES = [ + { + type: ANOMALY_SEVERITY.CRITICAL, + label: i18n.translate('xpack.apm.alerts.anomalySeverity.criticalLabel', { + defaultMessage: 'critical', + }), + threshold: ANOMALY_THRESHOLD.CRITICAL, + }, + { + type: ANOMALY_SEVERITY.MAJOR, + label: i18n.translate('xpack.apm.alerts.anomalySeverity.majorLabel', { + defaultMessage: 'major', + }), + threshold: ANOMALY_THRESHOLD.MAJOR, + }, + { + type: ANOMALY_SEVERITY.MINOR, + label: i18n.translate('xpack.apm.alerts.anomalySeverity.minor', { + defaultMessage: 'minor', + }), + threshold: ANOMALY_THRESHOLD.MINOR, + }, + { + type: ANOMALY_SEVERITY.WARNING, + label: i18n.translate('xpack.apm.alerts.anomalySeverity.warningLabel', { + defaultMessage: 'warning', + }), + threshold: ANOMALY_THRESHOLD.WARNING, + }, +] as const; + +export type AnomalyAlertSeverityType = ValuesType< + typeof ANOMALY_ALERT_SEVERITY_TYPES +>['type']; + // Server side registrations // x-pack/plugins/apm/server/lib/alerts/.ts // x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts diff --git a/x-pack/plugins/apm/common/anomaly_detection.test.ts b/x-pack/plugins/apm/common/anomaly_detection.test.ts deleted file mode 100644 index 21963b5300f83..0000000000000 --- a/x-pack/plugins/apm/common/anomaly_detection.test.ts +++ /dev/null @@ -1,39 +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 { getSeverity, Severity } from './anomaly_detection'; - -describe('getSeverity', () => { - describe('when score is undefined', () => { - it('returns undefined', () => { - expect(getSeverity(undefined)).toEqual(undefined); - }); - }); - - describe('when score < 25', () => { - it('returns warning', () => { - expect(getSeverity(10)).toEqual(Severity.warning); - }); - }); - - describe('when score is between 25 and 50', () => { - it('returns minor', () => { - expect(getSeverity(40)).toEqual(Severity.minor); - }); - }); - - describe('when score is between 50 and 75', () => { - it('returns major', () => { - expect(getSeverity(60)).toEqual(Severity.major); - }); - }); - - describe('when score is 75 or more', () => { - it('returns critical', () => { - expect(getSeverity(100)).toEqual(Severity.critical); - }); - }); -}); diff --git a/x-pack/plugins/apm/common/anomaly_detection.ts b/x-pack/plugins/apm/common/anomaly_detection.ts index 5d80ee6381267..dc5731e88083c 100644 --- a/x-pack/plugins/apm/common/anomaly_detection.ts +++ b/x-pack/plugins/apm/common/anomaly_detection.ts @@ -5,89 +5,31 @@ */ import { i18n } from '@kbn/i18n'; -import { EuiTheme } from '../../../legacy/common/eui_styled_components'; +import { ANOMALY_SEVERITY } from '../../ml/common'; +import { + getSeverityType, + getSeverityColor as mlGetSeverityColor, +} from '../../ml/common'; +import { ServiceHealthStatus } from './service_health_status'; export interface ServiceAnomalyStats { transactionType?: string; anomalyScore?: number; actualValue?: number; jobId?: string; + healthStatus: ServiceHealthStatus; } -export enum Severity { - critical = 'critical', - major = 'major', - minor = 'minor', - warning = 'warning', -} - -// TODO: Replace with `getSeverity` from: -// https://github.com/elastic/kibana/blob/0f964f66916480f2de1f4b633e5afafc08cf62a0/x-pack/plugins/ml/common/util/anomaly_utils.ts#L129 -export function getSeverity(score?: number) { - if (typeof score !== 'number') { - return undefined; - } else if (score < 25) { - return Severity.warning; - } else if (score >= 25 && score < 50) { - return Severity.minor; - } else if (score >= 50 && score < 75) { - return Severity.major; - } else if (score >= 75) { - return Severity.critical; - } else { - return undefined; +export function getSeverity(score: number | undefined) { + if (score === undefined) { + return ANOMALY_SEVERITY.UNKNOWN; } -} -export function getSeverityColor(theme: EuiTheme, severity?: Severity) { - switch (severity) { - case Severity.warning: - return theme.eui.euiColorVis0; - case Severity.minor: - case Severity.major: - return theme.eui.euiColorVis5; - case Severity.critical: - return theme.eui.euiColorVis9; - default: - return; - } + return getSeverityType(score); } -export function getSeverityLabel(severity?: Severity) { - switch (severity) { - case Severity.critical: - return i18n.translate( - 'xpack.apm.servicesTable.serviceHealthStatus.critical', - { - defaultMessage: 'Critical', - } - ); - - case Severity.major: - case Severity.minor: - return i18n.translate( - 'xpack.apm.servicesTable.serviceHealthStatus.warning', - { - defaultMessage: 'Warning', - } - ); - - case Severity.warning: - return i18n.translate( - 'xpack.apm.servicesTable.serviceHealthStatus.healthy', - { - defaultMessage: 'Healthy', - } - ); - - default: - return i18n.translate( - 'xpack.apm.servicesTable.serviceHealthStatus.unknown', - { - defaultMessage: 'Unknown', - } - ); - } +export function getSeverityColor(score: number) { + return mlGetSeverityColor(score); } export const ML_ERRORS = { diff --git a/x-pack/plugins/apm/common/service_health_status.ts b/x-pack/plugins/apm/common/service_health_status.ts new file mode 100644 index 0000000000000..468f06ab97af8 --- /dev/null +++ b/x-pack/plugins/apm/common/service_health_status.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { ANOMALY_SEVERITY } from '../../ml/common'; + +import { EuiTheme } from '../../../legacy/common/eui_styled_components'; + +export enum ServiceHealthStatus { + healthy = 'healthy', + critical = 'critical', + warning = 'warning', + unknown = 'unknown', +} + +export function getServiceHealthStatus({ + severity, +}: { + severity: ANOMALY_SEVERITY; +}) { + switch (severity) { + case ANOMALY_SEVERITY.CRITICAL: + case ANOMALY_SEVERITY.MAJOR: + return ServiceHealthStatus.critical; + + case ANOMALY_SEVERITY.MINOR: + case ANOMALY_SEVERITY.WARNING: + return ServiceHealthStatus.warning; + + case ANOMALY_SEVERITY.LOW: + return ServiceHealthStatus.healthy; + + case ANOMALY_SEVERITY.UNKNOWN: + return ServiceHealthStatus.unknown; + } +} + +export function getServiceHealthStatusColor( + theme: EuiTheme, + status: ServiceHealthStatus +) { + switch (status) { + case ServiceHealthStatus.healthy: + return theme.eui.euiColorVis0; + case ServiceHealthStatus.warning: + return theme.eui.euiColorVis5; + case ServiceHealthStatus.critical: + return theme.eui.euiColorVis9; + case ServiceHealthStatus.unknown: + return theme.eui.euiColorMediumShade; + } +} + +export function getServiceHealthStatusLabel(status: ServiceHealthStatus) { + switch (status) { + case ServiceHealthStatus.critical: + return i18n.translate('xpack.apm.serviceHealthStatus.critical', { + defaultMessage: 'Critical', + }); + + case ServiceHealthStatus.warning: + return i18n.translate('xpack.apm.serviceHealthStatus.warning', { + defaultMessage: 'Warning', + }); + + case ServiceHealthStatus.healthy: + return i18n.translate('xpack.apm.serviceHealthStatus.healthy', { + defaultMessage: 'Healthy', + }); + + case ServiceHealthStatus.unknown: + return i18n.translate('xpack.apm.serviceHealthStatus.unknown', { + defaultMessage: 'Unknown', + }); + } +} diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index 8aa4417580337..bdef0f9786a3f 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -23,13 +23,19 @@ ], "server": true, "ui": true, - "configPath": ["xpack", "apm"], - "extraPublicDirs": ["public/style/variables"], + "configPath": [ + "xpack", + "apm" + ], + "extraPublicDirs": [ + "public/style/variables" + ], "requiredBundles": [ "kibanaReact", "kibanaUtils", "observability", "home", - "maps" + "maps", + "ml" ] } diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx index 5bddfc67200b1..468d08339431c 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx @@ -5,105 +5,60 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiHealth, EuiSpacer, EuiSuperSelect, EuiText } from '@elastic/eui'; +import { getSeverityColor } from '../../../../common/anomaly_detection'; import { - getSeverityColor, - Severity, -} from '../../../../common/anomaly_detection'; -import { useTheme } from '../../../hooks/useTheme'; + AnomalyAlertSeverityType, + ANOMALY_ALERT_SEVERITY_TYPES, +} from '../../../../common/alert_types'; -type SeverityScore = 0 | 25 | 50 | 75; -const ANOMALY_SCORES: SeverityScore[] = [0, 25, 50, 75]; - -const anomalyScoreSeverityMap: { - [key in SeverityScore]: { label: string; severity: Severity }; -} = { - 0: { - label: i18n.translate('xpack.apm.alerts.anomalySeverity.warningLabel', { - defaultMessage: 'warning', - }), - severity: Severity.warning, - }, - 25: { - label: i18n.translate('xpack.apm.alerts.anomalySeverity.minorLabel', { - defaultMessage: 'minor', - }), - severity: Severity.minor, - }, - 50: { - label: i18n.translate('xpack.apm.alerts.anomalySeverity.majorLabel', { - defaultMessage: 'major', - }), - severity: Severity.major, - }, - 75: { - label: i18n.translate('xpack.apm.alerts.anomalySeverity.criticalLabel', { - defaultMessage: 'critical', - }), - severity: Severity.critical, - }, -}; - -export function AnomalySeverity({ - severityScore, -}: { - severityScore: SeverityScore; -}) { - const theme = useTheme(); - const { label, severity } = anomalyScoreSeverityMap[severityScore]; - const defaultColor = theme.eui.euiColorMediumShade; - const color = getSeverityColor(theme, severity) || defaultColor; +export function AnomalySeverity({ type }: { type: AnomalyAlertSeverityType }) { + const selectedOption = ANOMALY_ALERT_SEVERITY_TYPES.find( + (option) => option.type === type + )!; return ( - - {label} + + {selectedOption.label} ); } -const getOption = (value: SeverityScore) => { - return { - value: value.toString(10), - inputDisplay: , - dropdownDisplay: ( - <> - - - -

- -

-
- - ), - }; -}; - interface Props { - onChange: (value: SeverityScore) => void; - value: SeverityScore; + onChange: (value: AnomalyAlertSeverityType) => void; + value: AnomalyAlertSeverityType; } export function SelectAnomalySeverity({ onChange, value }: Props) { - const options = ANOMALY_SCORES.map((anomalyScore) => getOption(anomalyScore)); - return ( { - const selectedAnomalyScore = parseInt( - selectedValue, - 10 - ) as SeverityScore; - onChange(selectedAnomalyScore); + options={ANOMALY_ALERT_SEVERITY_TYPES.map((option) => ({ + value: option.type, + inputDisplay: , + dropdownDisplay: ( + <> + + + +

+ +

+
+ + ), + }))} + valueOfSelected={value} + onChange={(selectedValue: AnomalyAlertSeverityType) => { + onChange(selectedValue); }} /> ); diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx index fb4cda56fce04..ca1f55e9d391a 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx @@ -7,6 +7,7 @@ import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import React from 'react'; +import { ANOMALY_SEVERITY } from '../../../../../ml/common'; import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; import { useEnvironments } from '../../../hooks/useEnvironments'; import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; @@ -34,7 +35,11 @@ interface Params { serviceName: string; transactionType: string; environment: string; - anomalyScore: 0 | 25 | 50 | 75; + anomalySeverityType: + | ANOMALY_SEVERITY.CRITICAL + | ANOMALY_SEVERITY.MAJOR + | ANOMALY_SEVERITY.MINOR + | ANOMALY_SEVERITY.WARNING; } interface Props { @@ -67,7 +72,7 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) { transactionType, serviceName, environment: urlParams.environment || ENVIRONMENT_ALL.value, - anomalyScore: 75, + anomalySeverityType: ANOMALY_SEVERITY.CRITICAL, }; const params = { @@ -84,7 +89,7 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) { onChange={(e) => setAlertParams('environment', e.target.value)} />, } + value={} title={i18n.translate( 'xpack.apm.transactionDurationAnomalyAlertTrigger.anomalySeverity', { @@ -93,9 +98,9 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) { )} > { - setAlertParams('anomalyScore', value); + setAlertParams('anomalySeverityType', value); }} /> , diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx index 5699d0b56219b..c1192f5f18274 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx @@ -14,6 +14,10 @@ import { EuiIconTip, EuiHealth, } from '@elastic/eui'; +import { + getServiceHealthStatus, + getServiceHealthStatusColor, +} from '../../../../../common/service_health_status'; import { useTheme } from '../../../../hooks/useTheme'; import { fontSize, px } from '../../../../style/variables'; import { asInteger, asDuration } from '../../../../utils/formatters'; @@ -22,7 +26,6 @@ import { popoverWidth } from '../cytoscapeOptions'; import { TRANSACTION_REQUEST } from '../../../../../common/transaction_types'; import { getSeverity, - getSeverityColor, ServiceAnomalyStats, } from '../../../../../common/anomaly_detection'; @@ -59,13 +62,15 @@ export function AnomalyDetection({ serviceName, serviceAnomalyStats }: Props) { const theme = useTheme(); const anomalyScore = serviceAnomalyStats?.anomalyScore; - const anomalySeverity = getSeverity(anomalyScore); + const severity = getSeverity(anomalyScore); const actualValue = serviceAnomalyStats?.actualValue; const mlJobId = serviceAnomalyStats?.jobId; const transactionType = serviceAnomalyStats?.transactionType ?? TRANSACTION_REQUEST; const hasAnomalyDetectionScore = anomalyScore !== undefined; + const healthStatus = getServiceHealthStatus({ severity }); + return ( <>
@@ -81,7 +86,9 @@ export function AnomalyDetection({ serviceName, serviceAnomalyStats }: Props) { - + {ANOMALY_DETECTION_SCORE_METRIC} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts index 1ac7157cc2aad..61ac9bd7cd54c 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -5,26 +5,26 @@ */ import cytoscape from 'cytoscape'; import { CSSProperties } from 'react'; +import { + getServiceHealthStatusColor, + ServiceHealthStatus, +} from '../../../../common/service_health_status'; import { SERVICE_NAME, SPAN_DESTINATION_SERVICE_RESOURCE, } from '../../../../common/elasticsearch_fieldnames'; import { EuiTheme } from '../../../../../observability/public'; import { defaultIcon, iconForNode } from './icons'; -import { - getSeverity, - getSeverityColor, - ServiceAnomalyStats, - Severity, -} from '../../../../common/anomaly_detection'; +import { ServiceAnomalyStats } from '../../../../common/anomaly_detection'; export const popoverWidth = 280; -function getNodeSeverity(el: cytoscape.NodeSingular) { +function getServiceAnomalyStats(el: cytoscape.NodeSingular) { const serviceAnomalyStats: ServiceAnomalyStats | undefined = el.data( 'serviceAnomalyStats' ); - return getSeverity(serviceAnomalyStats?.anomalyScore); + + return serviceAnomalyStats; } function getBorderColorFn( @@ -32,10 +32,11 @@ function getBorderColorFn( ): cytoscape.Css.MapperFunction { return (el: cytoscape.NodeSingular) => { const hasAnomalyDetectionJob = el.data('serviceAnomalyStats') !== undefined; - const nodeSeverity = getNodeSeverity(el); + const anomalyStats = getServiceAnomalyStats(el); if (hasAnomalyDetectionJob) { - return ( - getSeverityColor(theme, nodeSeverity) || theme.eui.euiColorMediumShade + return getServiceHealthStatusColor( + theme, + anomalyStats?.healthStatus ?? ServiceHealthStatus.unknown ); } if (el.hasClass('primary') || el.selected()) { @@ -49,8 +50,8 @@ const getBorderStyle: cytoscape.Css.MapperFunction< cytoscape.NodeSingular, cytoscape.Css.LineStyle > = (el: cytoscape.NodeSingular) => { - const nodeSeverity = getNodeSeverity(el); - if (nodeSeverity === Severity.critical) { + const status = getServiceAnomalyStats(el)?.healthStatus; + if (status === ServiceHealthStatus.critical) { return 'double'; } else { return 'solid'; @@ -58,11 +59,11 @@ const getBorderStyle: cytoscape.Css.MapperFunction< }; function getBorderWidth(el: cytoscape.NodeSingular) { - const nodeSeverity = getNodeSeverity(el); + const status = getServiceAnomalyStats(el)?.healthStatus; - if (nodeSeverity === Severity.minor || nodeSeverity === Severity.major) { + if (status === ServiceHealthStatus.warning) { return 4; - } else if (nodeSeverity === Severity.critical) { + } else if (status === ServiceHealthStatus.critical) { return 8; } else { return 4; diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/HealthBadge.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/HealthBadge.tsx index 94353080bc7d5..c6be0a352ef66 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/HealthBadge.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/HealthBadge.tsx @@ -6,20 +6,22 @@ import React from 'react'; import { EuiBadge } from '@elastic/eui'; import { - getSeverityColor, - getSeverityLabel, - Severity, -} from '../../../../../common/anomaly_detection'; + getServiceHealthStatusColor, + getServiceHealthStatusLabel, + ServiceHealthStatus, +} from '../../../../../common/service_health_status'; import { useTheme } from '../../../../hooks/useTheme'; -export function HealthBadge({ severity }: { severity?: Severity }) { +export function HealthBadge({ + healthStatus, +}: { + healthStatus: ServiceHealthStatus; +}) { const theme = useTheme(); - const unknownColor = theme.eui.euiColorLightShade; - return ( - - {getSeverityLabel(severity)} + + {getServiceHealthStatusLabel(healthStatus)} ); } diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js index 519d74827097b..7c306c16cca1f 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js @@ -9,6 +9,7 @@ import { shallow } from 'enzyme'; import { ServiceList, SERVICE_COLUMNS } from '../index'; import props from './props.json'; import { mockMoment } from '../../../../../utils/testHelpers'; +import { ServiceHealthStatus } from '../../../../../../common/service_health_status'; describe('ServiceOverview -> List', () => { beforeAll(() => { @@ -52,25 +53,28 @@ describe('ServiceOverview -> List', () => { describe('without ML data', () => { it('does not render health column', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); const columns = wrapper.props().columns; - expect(columns[0].field).not.toBe('severity'); + expect(columns[0].field).not.toBe('healthStatus'); }); }); describe('with ML data', () => { it('renders health column', () => { const wrapper = shallow( - + ({ + ...item, + healthStatus: ServiceHealthStatus.warning, + }))} + /> ); const columns = wrapper.props().columns; - expect(columns[0].field).toBe('severity'); + expect(columns[0].field).toBe('healthStatus'); }); }); }); diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap index da3f6ae89940a..e6a9823f3ee28 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap @@ -1,6 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ServiceOverview -> List renders columns correctly 1`] = ``; +exports[`ServiceOverview -> List renders columns correctly 1`] = ` + +`; exports[`ServiceOverview -> List renders empty state 1`] = ` List renders empty state 1`] = ` } initialPageSize={50} initialSortDirection="desc" - initialSortField="severity" + initialSortField="healthStatus" items={Array []} sortFn={[Function]} /> @@ -106,7 +110,7 @@ exports[`ServiceOverview -> List renders with data 1`] = ` } initialPageSize={50} initialSortDirection="desc" - initialSortField="severity" + initialSortField="healthStatus" items={ Array [ Object { diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx index ce256137481cb..4c7c0824a7c49 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx @@ -10,6 +10,7 @@ import React from 'react'; import styled from 'styled-components'; import { ValuesType } from 'utility-types'; import { orderBy } from 'lodash'; +import { ServiceHealthStatus } from '../../../../../common/service_health_status'; import { asPercent } from '../../../../../common/utils/formatters'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_services'; @@ -20,14 +21,12 @@ import { ManagedTable, ITableColumn } from '../../../shared/ManagedTable'; import { EnvironmentBadge } from '../../../shared/EnvironmentBadge'; import { TransactionOverviewLink } from '../../../shared/Links/apm/TransactionOverviewLink'; import { AgentIcon } from '../../../shared/AgentIcon'; -import { Severity } from '../../../../../common/anomaly_detection'; import { HealthBadge } from './HealthBadge'; import { ServiceListMetric } from './ServiceListMetric'; interface Props { items: ServiceListAPIResponse['items']; noItemsMessage?: React.ReactNode; - displayHealthStatus: boolean; } type ServiceListItem = ValuesType; @@ -53,14 +52,18 @@ const AppLink = styled(TransactionOverviewLink)` export const SERVICE_COLUMNS: Array> = [ { - field: 'severity', + field: 'healthStatus', name: i18n.translate('xpack.apm.servicesTable.healthColumnLabel', { defaultMessage: 'Health', }), width: px(unit * 6), sortable: true, - render: (_, { severity }) => { - return ; + render: (_, { healthStatus }) => { + return ( + + ); }, }, { @@ -172,40 +175,38 @@ export const SERVICE_COLUMNS: Array> = [ }, ]; -const SEVERITY_ORDER = [ - Severity.warning, - Severity.minor, - Severity.major, - Severity.critical, +const SERVICE_HEALTH_STATUS_ORDER = [ + ServiceHealthStatus.unknown, + ServiceHealthStatus.healthy, + ServiceHealthStatus.warning, + ServiceHealthStatus.critical, ]; -export function ServiceList({ - items, - displayHealthStatus, - noItemsMessage, -}: Props) { +export function ServiceList({ items, noItemsMessage }: Props) { + const displayHealthStatus = items.some((item) => 'healthStatus' in item); + const columns = displayHealthStatus ? SERVICE_COLUMNS - : SERVICE_COLUMNS.filter((column) => column.field !== 'severity'); + : SERVICE_COLUMNS.filter((column) => column.field !== 'healthStatus'); return ( { - // For severity, sort items by severity first, then by TPM + // For healthStatus, sort items by healthStatus first, then by TPM - return sortField === 'severity' + return sortField === 'healthStatus' ? orderBy( itemsToSort, [ (item) => { - return item.severity - ? SEVERITY_ORDER.indexOf(item.severity) + return item.healthStatus + ? SERVICE_HEALTH_STATUS_ORDER.indexOf(item.healthStatus) : -1; }, (item) => item.transactionsPerMinute?.value ?? 0, diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx index e4ba1e36378d9..d8c8f25616560 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx @@ -10,6 +10,7 @@ import { merge } from 'lodash'; import React, { FunctionComponent, ReactChild } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; +import { ServiceHealthStatus } from '../../../../../common/service_health_status'; import { ServiceOverview } from '..'; import { EuiThemeProvider } from '../../../../../../observability/public'; import { ApmPluginContextValue } from '../../../../context/ApmPluginContext'; @@ -114,7 +115,7 @@ describe('Service Overview -> View', () => { errorsPerMinute: 200, avgResponseTime: 300, environments: ['test', 'dev'], - severity: 1, + healthStatus: ServiceHealthStatus.warning, }, { serviceName: 'My Go Service', @@ -123,7 +124,7 @@ describe('Service Overview -> View', () => { errorsPerMinute: 500, avgResponseTime: 600, environments: [], - severity: 10, + severity: ServiceHealthStatus.healthy, }, ], }); @@ -252,7 +253,7 @@ describe('Service Overview -> View', () => { errorsPerMinute: 200, avgResponseTime: 300, environments: ['test', 'dev'], - severity: 1, + healthStatus: ServiceHealthStatus.warning, }, ], }); diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap index b56f7d6820274..dfc0cc8637ff1 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap @@ -153,8 +153,8 @@ NodeList [ > - Unknown + Warning @@ -435,7 +435,7 @@ NodeList [ > 'severity' in item); - return ( <> @@ -134,7 +132,6 @@ export function ServiceOverview() { option.type === alertParams.anomalySeverityType + ); + + if (!selectedOption) { + throw new Error( + `Anomaly alert severity type ${alertParams.anomalySeverityType} is not supported.` + ); + } + + const threshold = selectedOption.threshold; + if (mlJobIds.length === 0) { return {}; } @@ -96,7 +118,7 @@ export function registerTransactionDurationAnomalyAlertType({ { range: { record_score: { - gte: alertParams.anomalyScore, + gte: threshold, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index ed8ae923e6e6c..da087b4c1911a 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import Boom from 'boom'; +import { getServiceHealthStatus } from '../../../common/service_health_status'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { PromiseReturnType } from '../../../typings/common'; import { @@ -12,6 +13,7 @@ import { } from '../../../common/transaction_types'; import { ServiceAnomalyStats, + getSeverity, ML_ERRORS, } from '../../../common/anomaly_detection'; import { getMlJobsWithAPMGroup } from '../anomaly_detection/get_ml_jobs_with_apm_group'; @@ -130,13 +132,19 @@ function transformResponseToServiceAnomalies( response.aggregations?.services.buckets ?? [] ).reduce( (statsByServiceName, { key: serviceName, top_score: topScoreAgg }) => { + const anomalyScore = topScoreAgg.hits.hits[0]?.sort?.[0]; + + const severity = getSeverity(anomalyScore); + const healthStatus = getServiceHealthStatus({ severity }); + return { ...statsByServiceName, [serviceName]: { transactionType: topScoreAgg.hits.hits[0]?._source?.by_field_value, - anomalyScore: topScoreAgg.hits.hits[0]?.sort?.[0], + anomalyScore, actualValue: topScoreAgg.hits.hits[0]?._source?.actual?.[0], jobId: topScoreAgg.hits.hits[0]?._source?.job_id, + healthStatus, }, }; }, diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts index e529198e717d3..f30b80feda302 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ServiceHealthStatus } from '../../../common/service_health_status'; + import { AGENT_NAME, SERVICE_ENVIRONMENT, @@ -43,6 +45,7 @@ const anomalies = { actualValue: 10000, anomalyScore: 50, jobId: 'apm-test-1234-ml-module-name', + healthStatus: ServiceHealthStatus.warning, }, }, }; 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 e7e18cbff1c15..17799203fe73b 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 @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getServiceHealthStatus } from '../../../../common/service_health_status'; import { EventOutcome } from '../../../../common/event_outcome'; import { getSeverity } from '../../../../common/anomaly_detection'; import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; @@ -413,10 +414,11 @@ export const getHealthStatuses = async ( const stats = anomalies.serviceAnomalies[serviceName]; const severity = getSeverity(stats.anomalyScore); + const healthStatus = getServiceHealthStatus({ severity }); return { serviceName, - severity, + healthStatus, }; }); }; diff --git a/x-pack/plugins/ml/common/index.ts b/x-pack/plugins/ml/common/index.ts index 791a7de48f36f..9a415ac0718b3 100644 --- a/x-pack/plugins/ml/common/index.ts +++ b/x-pack/plugins/ml/common/index.ts @@ -5,3 +5,5 @@ */ export { SearchResponse7 } from './types/es_client'; +export { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from './constants/anomalies'; +export { getSeverityColor, getSeverityType } from './util/anomaly_utils'; diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index 2c5dbe108ab1e..1cd52079b4e39 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -35,5 +35,8 @@ "dashboard", "savedObjects", "home" + ], + "extraPublicDirs": [ + "common" ] } diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap b/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap index 1424ca42539c0..199a49dce8f9e 100644 --- a/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap +++ b/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap @@ -77,6 +77,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -104,6 +105,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -130,6 +132,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -156,6 +159,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -209,6 +213,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -255,6 +260,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -301,6 +307,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -407,6 +414,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -465,6 +473,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -491,6 +500,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -504,6 +514,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -523,6 +534,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -548,6 +560,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -573,6 +586,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -605,6 +619,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -687,6 +702,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -734,6 +750,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -748,6 +765,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -806,6 +824,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -833,6 +852,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -859,6 +879,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -885,6 +906,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -938,6 +960,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -984,6 +1007,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -1030,6 +1054,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -1136,6 +1161,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -1194,6 +1220,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -1220,6 +1247,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -1233,6 +1261,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -1252,6 +1281,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -1277,6 +1307,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -1302,6 +1333,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -1334,6 +1366,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -1416,6 +1449,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -1463,6 +1497,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -1477,6 +1512,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts b/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts index be3301964bd3c..11b5ca71e64e7 100644 --- a/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts +++ b/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts @@ -165,6 +165,7 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -179,6 +180,7 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, diff --git a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts index d08064f6aa70e..c93816dfb48b9 100644 --- a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts @@ -45,24 +45,24 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.body.items.length).to.be.greaterThan(0); }); - it('some items have severity set', () => { + it('some items have a health status set', () => { // Under the assumption that the loaded archive has // at least one APM ML job, and the time range is longer - // than 15m, at least one items should have severity set. - // Note that we currently have a bug where healthy services - // report as unknown (so without any severity status): + // than 15m, at least one items should have a health status + // set. Note that we currently have a bug where healthy + // services report as unknown (so without any health status): // https://github.com/elastic/kibana/issues/77083 - const severityScores = response.body.items.map((item: any) => item.severity); + const healthStatuses = response.body.items.map((item: any) => item.healthStatus); - expect(severityScores.filter(Boolean).length).to.be.greaterThan(0); + expect(healthStatuses.filter(Boolean).length).to.be.greaterThan(0); - expectSnapshot(severityScores).toMatchInline(` + expectSnapshot(healthStatuses).toMatchInline(` Array [ undefined, undefined, - "warning", - "warning", + "healthy", + "healthy", undefined, undefined, undefined, From 5a31dce92d086bfe199334e6723a59c897feb14a Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Fri, 18 Sep 2020 08:16:57 -0400 Subject: [PATCH 19/25] Upgrade and consolidate prop-types and create-react-class (#77803) --- package.json | 4 +-- packages/kbn-i18n/package.json | 2 +- packages/kbn-ui-framework/package.json | 2 +- x-pack/package.json | 4 +-- yarn.lock | 34 ++++++-------------------- 5 files changed, 14 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index d258d21413fe4..fb1d7e50d7d22 100644 --- a/package.json +++ b/package.json @@ -308,7 +308,7 @@ "@types/pegjs": "^0.10.1", "@types/pngjs": "^3.3.2", "@types/podium": "^1.0.0", - "@types/prop-types": "^15.5.3", + "@types/prop-types": "^15.7.3", "@types/reach__router": "^1.2.6", "@types/react": "^16.9.36", "@types/react-dom": "^16.9.8", @@ -444,7 +444,7 @@ "pngjs": "^3.4.0", "postcss": "^7.0.32", "prettier": "^2.1.1", - "prop-types": "15.6.0", + "prop-types": "^15.7.2", "proxyquire": "1.8.0", "react-grid-layout": "^0.16.2", "react-markdown": "^4.3.1", diff --git a/packages/kbn-i18n/package.json b/packages/kbn-i18n/package.json index 680c789bc9d9d..eccdff9060cbe 100644 --- a/packages/kbn-i18n/package.json +++ b/packages/kbn-i18n/package.json @@ -27,7 +27,7 @@ "intl-format-cache": "^2.1.0", "intl-messageformat": "^2.2.0", "intl-relativeformat": "^2.1.0", - "prop-types": "^15.6.2", + "prop-types": "^15.7.2", "react": "^16.12.0", "react-intl": "^2.8.0" } diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index a2151ca3381bc..363f97522a901 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -18,7 +18,7 @@ "classnames": "2.2.6", "focus-trap-react": "^3.1.1", "lodash": "^4.17.15", - "prop-types": "15.6.0", + "prop-types": "^15.7.2", "react": "^16.12.0", "react-ace": "^5.9.0", "react-color": "^2.13.8", diff --git a/x-pack/package.json b/x-pack/package.json index 9a1d424da4a1d..86fb6bec6ba89 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -107,7 +107,7 @@ "@types/papaparse": "^5.0.3", "@types/pngjs": "^3.3.2", "@types/pretty-ms": "^5.0.0", - "@types/prop-types": "^15.5.3", + "@types/prop-types": "^15.7.3", "@types/proper-lockfile": "^3.0.1", "@types/puppeteer": "^1.20.1", "@types/react": "^16.9.36", @@ -354,7 +354,7 @@ "papaparse": "^5.2.0", "pdfmake": "^0.1.65", "pngjs": "3.4.0", - "prop-types": "^15.6.0", + "prop-types": "^15.7.2", "proper-lockfile": "^3.2.0", "puid": "1.0.7", "puppeteer-core": "^1.19.0", diff --git a/yarn.lock b/yarn.lock index fa5f6ff135670..ffca5adcc562f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4388,10 +4388,10 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.1.tgz#f1a11e7babb0c3cad68100be381d1e064c68f1f6" integrity sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg== -"@types/prop-types@^15.5.3": - version "15.5.9" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.9.tgz#f2d14df87b0739041bc53a7d75e3d77d726a3ec0" - integrity sha512-Nha5b+jmBI271jdTMwrHiNXM+DvThjHOfyZtMX9kj/c/LUj2xiLHsG/1L3tJ8DjAoQN48cHwUwtqBotjyXaSdQ== +"@types/prop-types@^15.7.3": + version "15.7.3" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" + integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== "@types/proper-lockfile@^3.0.1": version "3.0.1" @@ -9590,7 +9590,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" -create-react-class@^15.5.1: +create-react-class@^15.5.1, create-react-class@^15.5.2: version "15.6.3" resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.3.tgz#2d73237fb3f970ae6ebe011a9e66f46dbca80036" integrity sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg== @@ -9599,15 +9599,6 @@ create-react-class@^15.5.1: loose-envify "^1.3.1" object-assign "^4.1.1" -create-react-class@^15.5.2: - version "15.6.2" - resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.2.tgz#cf1ed15f12aad7f14ef5f2dfe05e6c42f91ef02a" - integrity sha1-zx7RXxKq1/FO9fLf4F5sQvke8Co= - dependencies: - fbjs "^0.8.9" - loose-envify "^1.3.1" - object-assign "^4.1.1" - create-react-context@^0.1.5: version "0.1.6" resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.1.6.tgz#0f425931d907741127acc6e31acb4f9015dd9fdc" @@ -22793,15 +22784,6 @@ prop-types-exact@^1.2.0: object.assign "^4.1.0" reflect.ownkeys "^0.2.0" -prop-types@15.6.0: - version "15.6.0" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856" - integrity sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY= - dependencies: - fbjs "^0.8.16" - loose-envify "^1.3.1" - object-assign "^4.1.1" - prop-types@15.7.2, prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" @@ -28338,9 +28320,9 @@ typescript@4.0.2, typescript@^3.0.1, typescript@^3.0.3, typescript@^3.2.2, types integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ== ua-parser-js@^0.7.18: - version "0.7.21" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777" - integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ== + version "0.7.22" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.22.tgz#960df60a5f911ea8f1c818f3747b99c6e177eae3" + integrity sha512-YUxzMjJ5T71w6a8WWVcMGM6YWOTX27rCoIQgLXiWaxqXSx9D7DNjiGWn1aJIRSQ5qr0xuhra77bSIh6voR/46Q== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.5" From 613509d81a8799c34fdd81df00d3019c48adec9d Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 18 Sep 2020 08:54:08 -0400 Subject: [PATCH 20/25] Improve home screen for limited-access users (#77665) --- .../__snapshots__/home.test.js.snap | 2 + .../public/application/components/home.js | 5 +- .../__snapshots__/manage_data.test.tsx.snap | 3 + .../manage_data/manage_data.test.tsx | 5 + .../components/manage_data/manage_data.tsx | 52 +++---- .../solution_panel.test.tsx.snap | 1 + .../solutions_section/solution_panel.tsx | 1 + .../feature_catalogue_registry.test.ts | 34 +++++ .../feature_catalogue_registry.ts | 7 +- src/plugins/management/public/plugin.ts | 7 +- test/functional/page_objects/home_page.ts | 8 ++ .../server/plugin.ts | 5 +- .../plugins/snapshot_restore/server/plugin.ts | 1 + .../home/feature_controls/home_security.ts | 130 ++++++++++++++++++ .../apps/home/feature_controls/index.ts | 14 ++ x-pack/test/functional/apps/home/index.ts | 14 ++ .../security_and_spaces/tests/catalogue.ts | 10 +- .../security_only/tests/catalogue.ts | 10 +- .../spaces_only/tests/catalogue.ts | 10 +- 19 files changed, 287 insertions(+), 32 deletions(-) create mode 100644 x-pack/test/functional/apps/home/feature_controls/home_security.ts create mode 100644 x-pack/test/functional/apps/home/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/home/index.ts diff --git a/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap b/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap index 1b10756c2975c..bf1e8c8f0b401 100644 --- a/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap +++ b/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap @@ -164,6 +164,7 @@ exports[`home directories should not render directory entry when showOnHomePage {stackManagement ? ( - + `; + +exports[`ManageData render empty without any features 1`] = ``; diff --git a/src/plugins/home/public/application/components/manage_data/manage_data.test.tsx b/src/plugins/home/public/application/components/manage_data/manage_data.test.tsx index 5d00370caf2cc..0e86bf7dd3d84 100644 --- a/src/plugins/home/public/application/components/manage_data/manage_data.test.tsx +++ b/src/plugins/home/public/application/components/manage_data/manage_data.test.tsx @@ -88,4 +88,9 @@ describe('ManageData', () => { ); expect(component).toMatchSnapshot(); }); + + test('render empty without any features', () => { + const component = shallowWithIntl(); + expect(component).toMatchSnapshot(); + }); }); diff --git a/src/plugins/home/public/application/components/manage_data/manage_data.tsx b/src/plugins/home/public/application/components/manage_data/manage_data.tsx index 0dfb4f949f0c7..85f1bc04f353b 100644 --- a/src/plugins/home/public/application/components/manage_data/manage_data.tsx +++ b/src/plugins/home/public/application/components/manage_data/manage_data.tsx @@ -36,31 +36,37 @@ export const ManageData: FC = ({ addBasePath, features }) => ( <> {features.length > 1 &&