From 0b5f1c0d9c48dcfd8dc2aa8635f93ad81908489f Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Fri, 3 Sep 2021 11:22:26 +0200 Subject: [PATCH 01/47] [Reporting] Unskip ILM migration tests (#110813) * added allow restricted indices to privileges check * updated comment -> TODO Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/reporting/server/routes/deprecations.ts | 6 ++++-- .../reporting_and_security/ilm_migration_apis.ts | 3 +-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/reporting/server/routes/deprecations.ts b/x-pack/plugins/reporting/server/routes/deprecations.ts index 874885e2258a..d1d8302e394c 100644 --- a/x-pack/plugins/reporting/server/routes/deprecations.ts +++ b/x-pack/plugins/reporting/server/routes/deprecations.ts @@ -5,6 +5,7 @@ * 2.0. */ import { errors } from '@elastic/elasticsearch'; +import { SecurityHasPrivilegesIndexPrivilegesCheck } from '@elastic/elasticsearch/api/types'; import { RequestHandler } from 'src/core/server'; import { API_MIGRATE_ILM_POLICY_URL, @@ -36,10 +37,11 @@ export const registerDeprecationsRoutes = (reporting: ReportingCore, logger: Log const { body } = await elasticsearch.client.asCurrentUser.security.hasPrivileges({ body: { index: [ - { + ({ privileges: ['manage'], // required to do anything with the reporting indices names: [store.getReportingIndexPattern()], - }, + allow_restricted_indices: true, + } as unknown) as SecurityHasPrivilegesIndexPrivilegesCheck, // TODO: Needed until `allow_restricted_indices` is added to the types. ], }, }); diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/ilm_migration_apis.ts b/x-pack/test/reporting_api_integration/reporting_and_security/ilm_migration_apis.ts index b312ba676927..fd49e2b23721 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/ilm_migration_apis.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/ilm_migration_apis.ts @@ -20,8 +20,7 @@ export default function ({ getService }: FtrProviderContext) { const reportingAPI = getService('reportingAPI'); const security = getService('security'); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/110483 - describe.skip('ILM policy migration APIs', () => { + describe('ILM policy migration APIs', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/reporting/logs'); await esArchiver.load('x-pack/test/functional/es_archives/logstash_functional'); From 23fa1b4c074f25e26a008245e04c2fb1ab2006e7 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Fri, 3 Sep 2021 11:30:51 +0200 Subject: [PATCH 02/47] [Reporting] Updated telemetry types for V2 export types (#110622) * updated telemetry types * update tests * telemtery JSON file Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../reporting_usage_collector.test.ts.snap | 302 +++++++++++++++ .../usage/reporting_usage_collector.test.ts | 44 +++ .../plugins/reporting/server/usage/schema.ts | 11 + .../plugins/reporting/server/usage/types.ts | 13 +- .../schema/xpack_plugins.json | 358 ++++++++++++++++++ 5 files changed, 727 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap b/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap index 12e89f19e624..4fab4ca72aba 100644 --- a/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap +++ b/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap @@ -16,6 +16,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "available": Object { + "type": "boolean", + }, + "deprecated": Object { + "type": "long", + }, + "total": Object { + "type": "long", + }, + }, "_all": Object { "type": "long", }, @@ -62,6 +73,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "available": Object { + "type": "boolean", + }, + "deprecated": Object { + "type": "long", + }, + "total": Object { + "type": "long", + }, + }, "_all": Object { "type": "long", }, @@ -117,6 +139,36 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "app": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, + "available": Object { + "type": "boolean", + }, + "deprecated": Object { + "type": "long", + }, + "layout": Object { + "preserve_layout": Object { + "type": "long", + }, + "print": Object { + "type": "long", + }, + }, + "total": Object { + "type": "long", + }, + }, "status": Object { "completed": Object { "type": "long", @@ -147,6 +199,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "csv": Object { "canvas workpad": Object { "type": "long", @@ -180,6 +243,17 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, }, "completed_with_warnings": Object { "PNG": Object { @@ -193,6 +267,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "csv": Object { "canvas workpad": Object { "type": "long", @@ -226,6 +311,17 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, }, "failed": Object { "PNG": Object { @@ -239,6 +335,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "csv": Object { "canvas workpad": Object { "type": "long", @@ -272,6 +379,17 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, }, "pending": Object { "PNG": Object { @@ -285,6 +403,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "csv": Object { "canvas workpad": Object { "type": "long", @@ -318,6 +447,17 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, }, "processing": Object { "PNG": Object { @@ -331,6 +471,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "csv": Object { "canvas workpad": Object { "type": "long", @@ -364,6 +515,17 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, }, }, }, @@ -397,6 +559,36 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "app": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, + "available": Object { + "type": "boolean", + }, + "deprecated": Object { + "type": "long", + }, + "layout": Object { + "preserve_layout": Object { + "type": "long", + }, + "print": Object { + "type": "long", + }, + }, + "total": Object { + "type": "long", + }, + }, "status": Object { "completed": Object { "type": "long", @@ -427,6 +619,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "csv": Object { "canvas workpad": Object { "type": "long", @@ -460,6 +663,17 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, }, "completed_with_warnings": Object { "PNG": Object { @@ -473,6 +687,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "csv": Object { "canvas workpad": Object { "type": "long", @@ -506,6 +731,17 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, }, "failed": Object { "PNG": Object { @@ -519,6 +755,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "csv": Object { "canvas workpad": Object { "type": "long", @@ -552,6 +799,17 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, }, "pending": Object { "PNG": Object { @@ -565,6 +823,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "csv": Object { "canvas workpad": Object { "type": "long", @@ -598,6 +867,17 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, }, "processing": Object { "PNG": Object { @@ -611,6 +891,17 @@ Object { "type": "long", }, }, + "PNGV2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "csv": Object { "canvas workpad": Object { "type": "long", @@ -644,6 +935,17 @@ Object { "type": "long", }, }, + "printable_pdf_v2": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, }, }, }, diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts index 72824f6aeeb3..31ce6581d7de 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts @@ -487,6 +487,7 @@ describe('data modeling', () => { // just check that the example objects can be cast to ReportingUsageType check({ PNG: { available: true, total: 7 }, + PNGV2: { available: true, total: 7 }, _all: 21, available: true, browser_type: 'chromium', @@ -495,6 +496,7 @@ describe('data modeling', () => { enabled: true, last7Days: { PNG: { available: true, total: 0 }, + PNGV2: { available: true, total: 0 }, _all: 0, csv: { available: true, total: 0 }, csv_searchsource: { available: true, total: 0 }, @@ -504,6 +506,12 @@ describe('data modeling', () => { layout: { preserve_layout: 0, print: 0 }, total: 0, }, + printable_pdf_v2: { + app: { dashboard: 0, visualization: 0 }, + available: true, + layout: { preserve_layout: 0, print: 0 }, + total: 0, + }, status: { completed: 0, failed: 0 }, statuses: {}, }, @@ -513,17 +521,26 @@ describe('data modeling', () => { layout: { preserve_layout: 7, print: 3 }, total: 10, }, + printable_pdf_v2: { + app: { 'canvas workpad': 3, dashboard: 3, visualization: 4 }, + available: true, + layout: { preserve_layout: 7, print: 3 }, + total: 10, + }, status: { completed: 21, failed: 0 }, statuses: { completed: { PNG: { dashboard: 3, visualization: 4 }, + PNGV2: { dashboard: 3, visualization: 4 }, csv: {}, printable_pdf: { 'canvas workpad': 3, dashboard: 3, visualization: 4 }, + printable_pdf_v2: { 'canvas workpad': 3, dashboard: 3, visualization: 4 }, }, }, }); check({ PNG: { available: true, total: 3 }, + PNGV2: { available: true, total: 3 }, _all: 4, available: true, browser_type: 'chromium', @@ -532,6 +549,7 @@ describe('data modeling', () => { enabled: true, last7Days: { PNG: { available: true, total: 3 }, + PNGV2: { available: true, total: 3 }, _all: 4, csv: { available: true, total: 0 }, csv_searchsource: { available: true, total: 0 }, @@ -541,6 +559,12 @@ describe('data modeling', () => { layout: { preserve_layout: 1, print: 0 }, total: 1, }, + printable_pdf_v2: { + app: { 'canvas workpad': 1, dashboard: 0, visualization: 0 }, + available: true, + layout: { preserve_layout: 1, print: 0 }, + total: 1, + }, status: { completed: 4, failed: 0 }, statuses: { completed: { PNG: { visualization: 3 }, printable_pdf: { 'canvas workpad': 1 } }, @@ -552,6 +576,12 @@ describe('data modeling', () => { layout: { preserve_layout: 1, print: 0 }, total: 1, }, + printable_pdf_v2: { + app: { 'canvas workpad': 1, dashboard: 0, visualization: 0 }, + available: true, + layout: { preserve_layout: 1, print: 0 }, + total: 1, + }, status: { completed: 4, failed: 0 }, statuses: { completed: { PNG: { visualization: 3 }, printable_pdf: { 'canvas workpad': 1 } }, @@ -571,9 +601,16 @@ describe('data modeling', () => { app: { dashboard: 0, visualization: 0 }, layout: { preserve_layout: 0, print: 0 }, }, + printable_pdf_v2: { + available: true, + total: 0, + app: { dashboard: 0, visualization: 0 }, + layout: { preserve_layout: 0, print: 0 }, + }, csv: { available: true, total: 0 }, csv_searchsource: { available: true, total: 0 }, PNG: { available: true, total: 0 }, + PNGV2: { available: true, total: 0 }, }, _all: 0, status: { completed: 0, failed: 0 }, @@ -584,9 +621,16 @@ describe('data modeling', () => { app: { dashboard: 0, visualization: 0 }, layout: { preserve_layout: 0, print: 0 }, }, + printable_pdf_v2: { + available: true, + total: 0, + app: { dashboard: 0, visualization: 0 }, + layout: { preserve_layout: 0, print: 0 }, + }, csv: { available: true, total: 0 }, csv_searchsource: { available: true, total: 0 }, PNG: { available: true, total: 0 }, + PNGV2: { available: true, total: 0 }, }); }); }); diff --git a/x-pack/plugins/reporting/server/usage/schema.ts b/x-pack/plugins/reporting/server/usage/schema.ts index 2060fdcb1f01..54545dd23509 100644 --- a/x-pack/plugins/reporting/server/usage/schema.ts +++ b/x-pack/plugins/reporting/server/usage/schema.ts @@ -25,7 +25,9 @@ const byAppCountsSchema: MakeSchemaFrom = { csv: appCountsSchema, csv_searchsource: appCountsSchema, PNG: appCountsSchema, + PNGV2: appCountsSchema, printable_pdf: appCountsSchema, + printable_pdf_v2: appCountsSchema, }; const availableTotalSchema: MakeSchemaFrom = { @@ -38,6 +40,7 @@ const jobTypesSchema: MakeSchemaFrom = { csv: availableTotalSchema, csv_searchsource: availableTotalSchema, PNG: availableTotalSchema, + PNGV2: availableTotalSchema, printable_pdf: { ...availableTotalSchema, app: appCountsSchema, @@ -46,6 +49,14 @@ const jobTypesSchema: MakeSchemaFrom = { preserve_layout: { type: 'long' }, }, }, + printable_pdf_v2: { + ...availableTotalSchema, + app: appCountsSchema, + layout: { + print: { type: 'long' }, + preserve_layout: { type: 'long' }, + }, + }, }; const rangeStatsSchema: MakeSchemaFrom = { diff --git a/x-pack/plugins/reporting/server/usage/types.ts b/x-pack/plugins/reporting/server/usage/types.ts index aae8c0ff4271..389dc27c46c6 100644 --- a/x-pack/plugins/reporting/server/usage/types.ts +++ b/x-pack/plugins/reporting/server/usage/types.ts @@ -63,7 +63,13 @@ export interface AvailableTotal { deprecated?: number; } -type BaseJobTypes = 'csv' | 'csv_searchsource' | 'PNG' | 'printable_pdf'; +type BaseJobTypes = + | 'csv' + | 'csv_searchsource' + | 'PNG' + | 'PNGV2' + | 'printable_pdf' + | 'printable_pdf_v2'; export interface LayoutCounts { print: number; @@ -80,6 +86,11 @@ export type JobTypes = { [K in BaseJobTypes]: AvailableTotal } & { app: AppCounts; layout: LayoutCounts; }; +} & { + printable_pdf_v2: AvailableTotal & { + app: AppCounts; + layout: LayoutCounts; + }; }; export type ByAppCounts = { [J in BaseJobTypes]?: AppCounts }; diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 38e74e15f7ae..5910de62271e 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -4079,6 +4079,19 @@ } } }, + "PNGV2": { + "properties": { + "available": { + "type": "boolean" + }, + "total": { + "type": "long" + }, + "deprecated": { + "type": "long" + } + } + }, "printable_pdf": { "properties": { "available": { @@ -4115,6 +4128,42 @@ } } }, + "printable_pdf_v2": { + "properties": { + "available": { + "type": "boolean" + }, + "total": { + "type": "long" + }, + "deprecated": { + "type": "long" + }, + "app": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, + "layout": { + "properties": { + "print": { + "type": "long" + }, + "preserve_layout": { + "type": "long" + } + } + } + } + }, "_all": { "type": "long" }, @@ -4180,6 +4229,19 @@ } } }, + "PNGV2": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, "printable_pdf": { "properties": { "canvas workpad": { @@ -4192,6 +4254,19 @@ "type": "long" } } + }, + "printable_pdf_v2": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } } } }, @@ -4236,6 +4311,19 @@ } } }, + "PNGV2": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, "printable_pdf": { "properties": { "canvas workpad": { @@ -4248,6 +4336,19 @@ "type": "long" } } + }, + "printable_pdf_v2": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } } } }, @@ -4292,6 +4393,19 @@ } } }, + "PNGV2": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, "printable_pdf": { "properties": { "canvas workpad": { @@ -4304,6 +4418,19 @@ "type": "long" } } + }, + "printable_pdf_v2": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } } } }, @@ -4348,6 +4475,19 @@ } } }, + "PNGV2": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, "printable_pdf": { "properties": { "canvas workpad": { @@ -4360,6 +4500,19 @@ "type": "long" } } + }, + "printable_pdf_v2": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } } } }, @@ -4404,6 +4557,19 @@ } } }, + "PNGV2": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, "printable_pdf": { "properties": { "canvas workpad": { @@ -4416,6 +4582,19 @@ "type": "long" } } + }, + "printable_pdf_v2": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } } } } @@ -4471,6 +4650,19 @@ } } }, + "PNGV2": { + "properties": { + "available": { + "type": "boolean" + }, + "total": { + "type": "long" + }, + "deprecated": { + "type": "long" + } + } + }, "printable_pdf": { "properties": { "available": { @@ -4507,6 +4699,42 @@ } } }, + "printable_pdf_v2": { + "properties": { + "available": { + "type": "boolean" + }, + "total": { + "type": "long" + }, + "deprecated": { + "type": "long" + }, + "app": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, + "layout": { + "properties": { + "print": { + "type": "long" + }, + "preserve_layout": { + "type": "long" + } + } + } + } + }, "_all": { "type": "long" }, @@ -4572,6 +4800,19 @@ } } }, + "PNGV2": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, "printable_pdf": { "properties": { "canvas workpad": { @@ -4584,6 +4825,19 @@ "type": "long" } } + }, + "printable_pdf_v2": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } } } }, @@ -4628,6 +4882,19 @@ } } }, + "PNGV2": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, "printable_pdf": { "properties": { "canvas workpad": { @@ -4640,6 +4907,19 @@ "type": "long" } } + }, + "printable_pdf_v2": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } } } }, @@ -4684,6 +4964,19 @@ } } }, + "PNGV2": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, "printable_pdf": { "properties": { "canvas workpad": { @@ -4696,6 +4989,19 @@ "type": "long" } } + }, + "printable_pdf_v2": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } } } }, @@ -4740,6 +5046,19 @@ } } }, + "PNGV2": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, "printable_pdf": { "properties": { "canvas workpad": { @@ -4752,6 +5071,19 @@ "type": "long" } } + }, + "printable_pdf_v2": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } } } }, @@ -4796,6 +5128,19 @@ } } }, + "PNGV2": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, "printable_pdf": { "properties": { "canvas workpad": { @@ -4808,6 +5153,19 @@ "type": "long" } } + }, + "printable_pdf_v2": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } } } } From c42391ed3a37b9e8b4569529ed925777a8c3a395 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Fri, 3 Sep 2021 11:42:12 +0200 Subject: [PATCH 03/47] Resurrect deprecated and removed authentication settings. (#110835) --- .../authentication_service.test.ts | 46 ++++ .../authentication/authentication_service.ts | 14 +- .../authentication/authenticator.test.ts | 1 + .../server/authentication/authenticator.ts | 2 + .../authentication/providers/base.mock.ts | 1 + .../server/authentication/providers/base.ts | 1 + .../authentication/providers/saml.test.ts | 218 ++++++++++++++---- .../server/authentication/providers/saml.ts | 74 ++++-- x-pack/plugins/security/server/config.test.ts | 148 +++++++++++- x-pack/plugins/security/server/config.ts | 7 +- .../server/config_deprecations.test.ts | 2 +- .../security/server/config_deprecations.ts | 3 +- .../security_api_integration/oidc.config.ts | 2 +- 13 files changed, 433 insertions(+), 86 deletions(-) diff --git a/x-pack/plugins/security/server/authentication/authentication_service.test.ts b/x-pack/plugins/security/server/authentication/authentication_service.test.ts index d38f963a60c3..a7ef6b34616c 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.test.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.test.ts @@ -318,6 +318,52 @@ describe('AuthenticationService', () => { }); }); + describe('getServerBaseURL()', () => { + let getServerBaseURL: () => string; + beforeEach(() => { + mockStartAuthenticationParams.http.getServerInfo.mockReturnValue({ + name: 'some-name', + protocol: 'socket', + hostname: 'test-hostname', + port: 1234, + }); + + service.setup(mockSetupAuthenticationParams); + service.start(mockStartAuthenticationParams); + + getServerBaseURL = jest.requireMock('./authenticator').Authenticator.mock.calls[0][0] + .getServerBaseURL; + }); + + it('falls back to legacy server config if `public` config is not specified', async () => { + expect(getServerBaseURL()).toBe('socket://test-hostname:1234'); + }); + + it('respects `public` config if it is specified', async () => { + mockStartAuthenticationParams.config.public = { + protocol: 'https', + } as ConfigType['public']; + expect(getServerBaseURL()).toBe('https://test-hostname:1234'); + + mockStartAuthenticationParams.config.public = { + hostname: 'elastic.co', + } as ConfigType['public']; + expect(getServerBaseURL()).toBe('socket://elastic.co:1234'); + + mockStartAuthenticationParams.config.public = { + port: 4321, + } as ConfigType['public']; + expect(getServerBaseURL()).toBe('socket://test-hostname:4321'); + + mockStartAuthenticationParams.config.public = { + protocol: 'https', + hostname: 'elastic.co', + port: 4321, + } as ConfigType['public']; + expect(getServerBaseURL()).toBe('https://elastic.co:4321'); + }); + }); + describe('getCurrentUser()', () => { let getCurrentUser: (r: KibanaRequest) => AuthenticatedUser | null; beforeEach(async () => { diff --git a/x-pack/plugins/security/server/authentication/authentication_service.ts b/x-pack/plugins/security/server/authentication/authentication_service.ts index 79dcfb8d804b..538bc26e6ffe 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.ts @@ -41,7 +41,7 @@ interface AuthenticationServiceSetupParams { } interface AuthenticationServiceStartParams { - http: Pick; + http: Pick; config: ConfigType; clusterClient: IClusterClient; legacyAuditLogger: SecurityAuditLogger; @@ -234,6 +234,17 @@ export class AuthenticationService { license: this.license, }); + /** + * Retrieves server protocol name/host name/port and merges it with `xpack.security.public` config + * to construct a server base URL (deprecated, used by the SAML provider only). + */ + const getServerBaseURL = () => { + const { protocol, hostname, port } = http.getServerInfo(); + const serverConfig = { protocol, hostname, port, ...config.public }; + + return `${serverConfig.protocol}://${serverConfig.hostname}:${serverConfig.port}`; + }; + const getCurrentUser = (request: KibanaRequest) => http.auth.get(request).state ?? null; @@ -247,6 +258,7 @@ export class AuthenticationService { config: { authc: config.authc }, getCurrentUser, featureUsageService, + getServerBaseURL, license: this.license, session, }); diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index ca33be92e9e9..27dfd89a3175 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -55,6 +55,7 @@ function getMockOptions({ basePath: httpServiceMock.createSetupContract().basePath, license: licenseMock.create(), loggers: loggingSystemMock.create(), + getServerBaseURL: jest.fn(), config: createConfig( ConfigSchema.validate({ authc: { selector, providers, http } }), loggingSystemMock.create().get(), diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index 4eeadf23c50b..5252f5c618f9 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -87,6 +87,7 @@ export interface AuthenticatorOptions { loggers: LoggerFactory; clusterClient: IClusterClient; session: PublicMethodsOf; + getServerBaseURL: () => string; } // Mapping between provider key defined in the config and authentication @@ -216,6 +217,7 @@ export class Authenticator { client: this.options.clusterClient.asInternalUser, logger: this.options.loggers.get('tokens'), }), + getServerBaseURL: this.options.getServerBaseURL, }; this.providers = new Map( diff --git a/x-pack/plugins/security/server/authentication/providers/base.mock.ts b/x-pack/plugins/security/server/authentication/providers/base.mock.ts index 5d3417ae9db1..6554b525fc9e 100644 --- a/x-pack/plugins/security/server/authentication/providers/base.mock.ts +++ b/x-pack/plugins/security/server/authentication/providers/base.mock.ts @@ -17,6 +17,7 @@ export type MockAuthenticationProviderOptions = ReturnType< export function mockAuthenticationProviderOptions(options?: { name: string }) { return { + getServerBaseURL: () => 'test-protocol://test-hostname:1234', client: elasticsearchServiceMock.createClusterClient(), logger: loggingSystemMock.create().get(), basePath: httpServiceMock.createBasePath(), diff --git a/x-pack/plugins/security/server/authentication/providers/base.ts b/x-pack/plugins/security/server/authentication/providers/base.ts index f6d9af24ee1a..d5b173fcfad8 100644 --- a/x-pack/plugins/security/server/authentication/providers/base.ts +++ b/x-pack/plugins/security/server/authentication/providers/base.ts @@ -26,6 +26,7 @@ import type { Tokens } from '../tokens'; */ export interface AuthenticationProviderOptions { name: string; + getServerBaseURL: () => string; basePath: HttpServiceSetup['basePath']; getRequestOriginalURL: ( request: KibanaRequest, diff --git a/x-pack/plugins/security/server/authentication/providers/saml.test.ts b/x-pack/plugins/security/server/authentication/providers/saml.test.ts index 4a32383d18de..251a59228fb0 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.test.ts @@ -39,23 +39,7 @@ describe('SAMLAuthenticationProvider', () => { ); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - provider = new SAMLAuthenticationProvider(mockOptions, { - realm: 'test-realm', - }); - }); - - it('throws if `realm` option is not specified', () => { - const providerOptions = mockAuthenticationProviderOptions(); - - expect(() => new SAMLAuthenticationProvider(providerOptions)).toThrowError( - 'Realm name must be specified' - ); - expect(() => new SAMLAuthenticationProvider(providerOptions, {})).toThrowError( - 'Realm name must be specified' - ); - expect(() => new SAMLAuthenticationProvider(providerOptions, { realm: '' })).toThrowError( - 'Realm name must be specified' - ); + provider = new SAMLAuthenticationProvider(mockOptions); }); describe('`login` method', () => { @@ -67,6 +51,7 @@ describe('SAMLAuthenticationProvider', () => { body: { access_token: 'some-token', refresh_token: 'some-refresh-token', + realm: 'test-realm', authentication: mockUser, }, }) @@ -108,13 +93,13 @@ describe('SAMLAuthenticationProvider', () => { body: { access_token: 'some-token', refresh_token: 'some-refresh-token', + realm: 'test-realm', authentication: mockUser, }, }) ); provider = new SAMLAuthenticationProvider(mockOptions, { - realm: 'test-realm', useRelayStateDeepLink: true, }); await expect( @@ -169,6 +154,10 @@ describe('SAMLAuthenticationProvider', () => { it('fails if realm from state is different from the realm provider is configured with.', async () => { const request = httpServerMock.createKibanaRequest(); + const customMockOptions = mockAuthenticationProviderOptions({ name: 'saml' }); + provider = new SAMLAuthenticationProvider(customMockOptions, { + realm: 'test-realm', + }); await expect( provider.login( @@ -184,7 +173,7 @@ describe('SAMLAuthenticationProvider', () => { ) ); - expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); + expect(customMockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled(); }); it('redirects to the default location if state contains empty redirect URL.', async () => { @@ -195,6 +184,7 @@ describe('SAMLAuthenticationProvider', () => { body: { access_token: 'user-initiated-login-token', refresh_token: 'user-initiated-login-refresh-token', + realm: 'test-realm', authentication: mockUser, }, }) @@ -232,13 +222,13 @@ describe('SAMLAuthenticationProvider', () => { body: { access_token: 'user-initiated-login-token', refresh_token: 'user-initiated-login-refresh-token', + realm: 'test-realm', authentication: mockUser, }, }) ); provider = new SAMLAuthenticationProvider(mockOptions, { - realm: 'test-realm', useRelayStateDeepLink: true, }); await expect( @@ -275,6 +265,7 @@ describe('SAMLAuthenticationProvider', () => { mockOptions.client.asInternalUser.transport.request.mockResolvedValue( securityMock.createApiResponse({ body: { + realm: 'test-realm', access_token: 'idp-initiated-login-token', refresh_token: 'idp-initiated-login-refresh-token', authentication: mockUser, @@ -301,7 +292,7 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/authenticate', - body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + body: { ids: [], content: 'saml-response-xml' }, }); }); @@ -342,20 +333,19 @@ describe('SAMLAuthenticationProvider', () => { username: 'user', access_token: 'valid-token', refresh_token: 'valid-refresh-token', + realm: 'test-realm', authentication: mockUser, }, }) ); provider = new SAMLAuthenticationProvider(mockOptions, { - realm: 'test-realm', useRelayStateDeepLink: true, }); }); it('redirects to the home page if `useRelayStateDeepLink` is set to `false`.', async () => { provider = new SAMLAuthenticationProvider(mockOptions, { - realm: 'test-realm', useRelayStateDeepLink: false, }); @@ -454,10 +444,39 @@ describe('SAMLAuthenticationProvider', () => { ) ); }); + + it('uses `realm` name instead of `acs` if it is specified for SAML authenticate request.', async () => { + // Create new provider instance with additional `realm` option. + provider = new SAMLAuthenticationProvider(mockOptions, { + realm: 'test-realm', + }); + + await expect( + provider.login(httpServerMock.createKibanaRequest({ headers: {} }), { + type: SAMLLogin.LoginWithSAMLResponse, + samlResponse: 'saml-response-xml', + }) + ).resolves.toEqual( + AuthenticationResult.redirectTo(`${mockOptions.basePath.serverBasePath}/`, { + state: { + accessToken: 'valid-token', + refreshToken: 'valid-refresh-token', + realm: 'test-realm', + }, + user: mockUser, + }) + ); + + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/authenticate', + body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + }); + }); }); describe('IdP initiated login with existing session', () => { - it('returns `notHandled` if new SAML Response is rejected.', async () => { + it('fails if new SAML Response is rejected and provider is not configured with specific realm.', async () => { const request = httpServerMock.createKibanaRequest({ headers: {} }); const authorization = 'Bearer some-valid-token'; @@ -466,6 +485,39 @@ describe('SAMLAuthenticationProvider', () => { ); mockOptions.client.asInternalUser.transport.request.mockRejectedValue(failureReason); + await expect( + provider.login( + request, + { type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' }, + { + accessToken: 'some-valid-token', + refreshToken: 'some-valid-refresh-token', + realm: 'test-realm', + } + ) + ).resolves.toEqual(AuthenticationResult.failed(failureReason)); + + expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ headers: { authorization } }); + expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/authenticate', + body: { ids: [], content: 'saml-response-xml' }, + }); + }); + + it('returns `notHandled` if new SAML Response is rejected and provider is configured with specific realm.', async () => { + const request = httpServerMock.createKibanaRequest({ headers: {} }); + const authorization = 'Bearer some-valid-token'; + + provider = new SAMLAuthenticationProvider(mockOptions, { + realm: 'test-realm', + }); + + const failureReason = new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 503, body: {} }) + ); + mockOptions.client.asInternalUser.transport.request.mockRejectedValue(failureReason); + await expect( provider.login( request, @@ -521,7 +573,7 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/authenticate', - body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + body: { ids: [], content: 'saml-response-xml' }, }); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); @@ -543,7 +595,7 @@ describe('SAMLAuthenticationProvider', () => { ), ], [ - 'current session is is expired', + 'current session is expired', Promise.reject( new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) ), @@ -568,6 +620,7 @@ describe('SAMLAuthenticationProvider', () => { username: 'user', access_token: 'new-valid-token', refresh_token: 'new-valid-refresh-token', + realm: 'test-realm', authentication: mockUser, }, }) @@ -595,7 +648,7 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/authenticate', - body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + body: { ids: [], content: 'saml-response-xml' }, }); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); @@ -624,6 +677,7 @@ describe('SAMLAuthenticationProvider', () => { username: 'user', access_token: 'new-valid-token', refresh_token: 'new-valid-refresh-token', + realm: 'test-realm', authentication: mockUser, }, }) @@ -632,7 +686,6 @@ describe('SAMLAuthenticationProvider', () => { mockOptions.tokens.invalidate.mockResolvedValue(undefined); provider = new SAMLAuthenticationProvider(mockOptions, { - realm: 'test-realm', useRelayStateDeepLink: true, }); @@ -661,7 +714,7 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/authenticate', - body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + body: { ids: [], content: 'saml-response-xml' }, }); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); @@ -699,19 +752,16 @@ describe('SAMLAuthenticationProvider', () => { body: { id: 'some-request-id', redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', + realm: 'test-realm', }, }) ); await expect( - provider.login( - request, - { - type: SAMLLogin.LoginInitiatedByUser, - redirectURL: '/test-base-path/some-path#some-fragment', - }, - { realm: 'test-realm' } - ) + provider.login(request, { + type: SAMLLogin.LoginInitiatedByUser, + redirectURL: '/test-base-path/some-path#some-fragment', + }) ).resolves.toEqual( AuthenticationResult.redirectTo( 'https://idp-host/path/login?SAMLRequest=some%20request%20', @@ -728,7 +778,9 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/prepare', - body: { realm: 'test-realm' }, + body: { + acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/v1/saml', + }, }); expect(mockOptions.logger.warn).not.toHaveBeenCalled(); @@ -742,6 +794,7 @@ describe('SAMLAuthenticationProvider', () => { body: { id: 'some-request-id', redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', + realm: 'test-realm', }, }) ); @@ -771,19 +824,32 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/prepare', - body: { realm: 'test-realm' }, + body: { + acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/v1/saml', + }, }); expect(mockOptions.logger.warn).not.toHaveBeenCalled(); }); - it('fails if SAML request preparation fails.', async () => { - const request = httpServerMock.createKibanaRequest(); + it('uses `realm` name instead of `acs` if it is specified for SAML prepare request.', async () => { + const request = httpServerMock.createKibanaRequest({ path: '/s/foo/some-path' }); - const failureReason = new errors.ResponseError( - securityMock.createApiResponse({ statusCode: 401, body: {} }) + // Create new provider instance with additional `realm` option. + const customMockOptions = mockAuthenticationProviderOptions(); + provider = new SAMLAuthenticationProvider(customMockOptions, { + realm: 'test-realm', + }); + + customMockOptions.client.asInternalUser.transport.request.mockResolvedValue( + securityMock.createApiResponse({ + body: { + id: 'some-request-id', + redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', + realm: 'test-realm', + }, + }) ); - mockOptions.client.asInternalUser.transport.request.mockRejectedValue(failureReason); await expect( provider.login( @@ -794,12 +860,47 @@ describe('SAMLAuthenticationProvider', () => { }, { realm: 'test-realm' } ) + ).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://idp-host/path/login?SAMLRequest=some%20request%20', + { + state: { + requestId: 'some-request-id', + redirectURL: '/test-base-path/some-path#some-fragment', + realm: 'test-realm', + }, + } + ) + ); + + expect(customMockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ + method: 'POST', + path: '/_security/saml/prepare', + body: { realm: 'test-realm' }, + }); + }); + + it('fails if SAML request preparation fails.', async () => { + const request = httpServerMock.createKibanaRequest(); + + const failureReason = new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 401, body: {} }) + ); + mockOptions.client.asInternalUser.transport.request.mockRejectedValue(failureReason); + + await expect( + provider.login(request, { + type: SAMLLogin.LoginInitiatedByUser, + redirectURL: '/test-base-path/some-path#some-fragment', + }) ).resolves.toEqual(AuthenticationResult.failed(failureReason)); expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/prepare', - body: { realm: 'test-realm' }, + body: { + acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/v1/saml', + }, }); }); }); @@ -893,7 +994,6 @@ describe('SAMLAuthenticationProvider', () => { state: { requestId: 'some-request-id', redirectURL: '/mock-server-basepath/s/foo/some-path#some-fragment', - realm: 'test-realm', }, } ) @@ -905,7 +1005,9 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/prepare', - body: { realm: 'test-realm' }, + body: { + acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/v1/saml', + }, }); }); @@ -1112,6 +1214,13 @@ describe('SAMLAuthenticationProvider', () => { it('fails if realm from state is different from the realm provider is configured with.', async () => { const request = httpServerMock.createKibanaRequest(); + + // Create new provider instance with additional `realm` option. + const customMockOptions = mockAuthenticationProviderOptions({ name: 'saml' }); + provider = new SAMLAuthenticationProvider(customMockOptions, { + realm: 'test-realm', + }); + await expect(provider.authenticate(request, { realm: 'other-realm' })).resolves.toEqual( AuthenticationResult.failed( Boom.unauthorized( @@ -1186,7 +1295,10 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/invalidate', - body: { query_string: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, + body: { + query_string: 'SAMLRequest=xxx%20yyy', + acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/v1/saml', + }, }); }); @@ -1305,7 +1417,10 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/invalidate', - body: { query_string: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, + body: { + query_string: 'SAMLRequest=xxx%20yyy', + acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/v1/saml', + }, }); }); @@ -1324,7 +1439,10 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({ method: 'POST', path: '/_security/saml/invalidate', - body: { query_string: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, + body: { + query_string: 'SAMLRequest=xxx%20yyy', + acs: 'test-protocol://test-hostname:1234/mock-server-basepath/api/security/v1/saml', + }, }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/saml.ts b/x-pack/plugins/security/server/authentication/providers/saml.ts index 37e7e868e4d3..6eab0c5dc487 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.ts @@ -42,9 +42,10 @@ interface ProviderState extends Partial { redirectURL?: string; /** - * The name of the SAML realm that was used to establish session. + * The name of the SAML realm that was used to establish session (may not be known during URL + * fragment capturing stage). */ - realm: string; + realm?: string; } /** @@ -105,9 +106,10 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { static readonly type = 'saml'; /** - * Specifies Elasticsearch SAML realm name that Kibana should use. + * Optionally specifies Elasticsearch SAML realm name that Kibana should use. If not specified + * Kibana ACS URL is used for realm matching instead. */ - private readonly realm: string; + private readonly realm?: string; /** * Indicates if we should treat non-empty `RelayState` as a deep link in Kibana we should redirect @@ -121,12 +123,8 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { ) { super(options); - if (!samlOptions || !samlOptions.realm) { - throw new Error('Realm name must be specified'); - } - - this.realm = samlOptions.realm; - this.useRelayStateDeepLink = samlOptions.useRelayStateDeepLink ?? false; + this.realm = samlOptions?.realm; + this.useRelayStateDeepLink = samlOptions?.useRelayStateDeepLink ?? false; } /** @@ -144,7 +142,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { // It may happen that Kibana is re-configured to use different realm for the same provider name, // we should clear such session an log user out. - if (state?.realm && state.realm !== this.realm) { + if (state && this.realm && state.realm !== this.realm) { const message = `State based on realm "${state.realm}", but provider with the name "${this.options.name}" is configured to use realm "${this.realm}".`; this.logger.debug(message); return AuthenticationResult.failed(Boom.unauthorized(message)); @@ -215,7 +213,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { // It may happen that Kibana is re-configured to use different realm for the same provider name, // we should clear such session an log user out. - if (state?.realm && state.realm !== this.realm) { + if (state && this.realm && state.realm !== this.realm) { const message = `State based on realm "${state.realm}", but provider with the name "${this.options.name}" is configured to use realm "${this.realm}".`; this.logger.debug(message); return AuthenticationResult.failed(Boom.unauthorized(message)); @@ -274,7 +272,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { // and state !== undefined). In this case case it'd be safer to trigger SP initiated logout // for the new session as well. const redirect = isIdPInitiatedSLORequest - ? await this.performIdPInitiatedSingleLogout(request) + ? await this.performIdPInitiatedSingleLogout(request, this.realm || state?.realm) : state ? await this.performUserInitiatedSingleLogout(state.accessToken!, state.refreshToken!) : // Once Elasticsearch can consume logout response we'll be sending it here. See https://github.com/elastic/elasticsearch/issues/40901 @@ -331,9 +329,14 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { // If we have a `SAMLResponse` and state, but state doesn't contain all the necessary information, // then something unexpected happened and we should fail. - const { requestId: stateRequestId, redirectURL: stateRedirectURL } = state || { + const { + requestId: stateRequestId, + redirectURL: stateRedirectURL, + realm: stateRealm, + } = state || { requestId: '', redirectURL: '', + realm: '', }; if (state && !stateRequestId) { const message = 'SAML response state does not have corresponding request id.'; @@ -349,7 +352,14 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { : 'Login has been initiated by Identity Provider.' ); - let result: { access_token: string; refresh_token: string; authentication: AuthenticationInfo }; + const providerRealm = this.realm || stateRealm; + + let result: { + access_token: string; + refresh_token: string; + realm: string; + authentication: AuthenticationInfo; + }; try { // This operation should be performed on behalf of the user with a privilege that normal // user usually doesn't have `cluster:admin/xpack/security/saml/authenticate`. @@ -362,7 +372,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { body: { ids: !isIdPInitiatedLogin ? [stateRequestId] : [], content: samlResponse, - realm: this.realm, + ...(providerRealm ? { realm: providerRealm } : {}), }, }) ).body as any; @@ -372,7 +382,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { // Since we don't know upfront what realm is targeted by the Identity Provider initiated login // there is a chance that it failed because of realm mismatch and hence we should return // `notHandled` and give other SAML providers a chance to properly handle it instead. - return isIdPInitiatedLogin + return isIdPInitiatedLogin && providerRealm ? AuthenticationResult.notHandled() : AuthenticationResult.failed(err); } @@ -404,7 +414,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { state: { accessToken: result.access_token, refreshToken: result.refresh_token, - realm: this.realm, + realm: result.realm, }, user: this.authenticationInfoToAuthenticatedUser(result.authentication), } @@ -545,7 +555,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { authHeaders: { authorization: new HTTPAuthorizationHeader('Bearer', accessToken).toString(), }, - state: { accessToken, refreshToken, realm: this.realm }, + state: { accessToken, refreshToken, realm: this.realm || state.realm }, } ); } @@ -559,15 +569,18 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { this.logger.debug('Trying to initiate SAML handshake.'); try { + // Prefer realm name if it's specified, otherwise fallback to ACS. + const preparePayload = this.realm ? { realm: this.realm } : { acs: this.getACS() }; + // This operation should be performed on behalf of the user with a privilege that normal // user usually doesn't have `cluster:admin/xpack/security/saml/prepare`. // We can replace generic `transport.request` with a dedicated API method call once // https://github.com/elastic/elasticsearch/issues/67189 is resolved. - const { id: requestId, redirect } = ( + const { id: requestId, redirect, realm } = ( await this.options.client.asInternalUser.transport.request({ method: 'POST', path: '/_security/saml/prepare', - body: { realm: this.realm }, + body: preparePayload, }) ).body as any; @@ -575,7 +588,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { // Store request id in the state so that we can reuse it once we receive `SAMLResponse`. return AuthenticationResult.redirectTo(redirect, { - state: { requestId, redirectURL, realm: this.realm }, + state: { requestId, redirectURL, realm }, }); } catch (err) { this.logger.debug(`Failed to initiate SAML handshake: ${getDetailedErrorMessage(err)}`); @@ -612,10 +625,14 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { * Calls `saml/invalidate` with the `SAMLRequest` query string parameter received from the Identity * Provider and redirects user back to the Identity Provider if needed. * @param request Request instance. + * @param realm Configured SAML realm name. */ - private async performIdPInitiatedSingleLogout(request: KibanaRequest) { + private async performIdPInitiatedSingleLogout(request: KibanaRequest, realm?: string) { this.logger.debug('Single logout has been initiated by the Identity Provider.'); + // Prefer realm name if it's specified, otherwise fallback to ACS. + const invalidatePayload = realm ? { realm } : { acs: this.getACS() }; + // This operation should be performed on behalf of the user with a privilege that normal // user usually doesn't have `cluster:admin/xpack/security/saml/invalidate`. // We can replace generic `transport.request` with a dedicated API method call once @@ -627,7 +644,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { // Elasticsearch expects `query_string` without leading `?`, so we should strip it with `slice`. body: { query_string: request.url.search ? request.url.search.slice(1) : '', - realm: this.realm, + ...invalidatePayload, }, }) ).body as any; @@ -637,6 +654,15 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { return redirect; } + /** + * Constructs and returns Kibana's Assertion consumer service URL. + */ + private getACS() { + return `${this.options.getServerBaseURL()}${ + this.options.basePath.serverBasePath + }/api/security/v1/saml`; + } + /** * Tries to initiate SAML authentication handshake. If the request already includes user URL hash fragment, we will * initiate handshake right away, otherwise we'll redirect user to a dedicated page where we capture URL hash fragment diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 75dfcb6151ea..3be565d59a11 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -58,6 +58,7 @@ describe('config schema', () => { "enabled": true, "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "loginAssistanceMessage": "", + "public": Object {}, "secureCookies": false, "session": Object { "cleanupInterval": "PT1H", @@ -109,6 +110,7 @@ describe('config schema', () => { "enabled": true, "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "loginAssistanceMessage": "", + "public": Object {}, "secureCookies": false, "session": Object { "cleanupInterval": "PT1H", @@ -159,6 +161,7 @@ describe('config schema', () => { "cookieName": "sid", "enabled": true, "loginAssistanceMessage": "", + "public": Object {}, "secureCookies": false, "session": Object { "cleanupInterval": "PT1H", @@ -179,6 +182,109 @@ describe('config schema', () => { ); }); + describe('public', () => { + it('properly validates `protocol`', async () => { + expect(ConfigSchema.validate({ public: { protocol: 'http' } }).public).toMatchInlineSnapshot(` + Object { + "protocol": "http", + } + `); + + expect(ConfigSchema.validate({ public: { protocol: 'https' } }).public) + .toMatchInlineSnapshot(` + Object { + "protocol": "https", + } + `); + + expect(() => ConfigSchema.validate({ public: { protocol: 'ftp' } })) + .toThrowErrorMatchingInlineSnapshot(` + "[public.protocol]: types that failed validation: + - [public.protocol.0]: expected value to equal [http] + - [public.protocol.1]: expected value to equal [https]" + `); + + expect(() => ConfigSchema.validate({ public: { protocol: 'some-protocol' } })) + .toThrowErrorMatchingInlineSnapshot(` + "[public.protocol]: types that failed validation: + - [public.protocol.0]: expected value to equal [http] + - [public.protocol.1]: expected value to equal [https]" + `); + }); + + it('properly validates `hostname`', async () => { + expect(ConfigSchema.validate({ public: { hostname: 'elastic.co' } }).public) + .toMatchInlineSnapshot(` + Object { + "hostname": "elastic.co", + } + `); + + expect(ConfigSchema.validate({ public: { hostname: '192.168.1.1' } }).public) + .toMatchInlineSnapshot(` + Object { + "hostname": "192.168.1.1", + } + `); + + expect(ConfigSchema.validate({ public: { hostname: '::1' } }).public).toMatchInlineSnapshot(` + Object { + "hostname": "::1", + } + `); + + expect(() => + ConfigSchema.validate({ public: { hostname: 'http://elastic.co' } }) + ).toThrowErrorMatchingInlineSnapshot( + `"[public.hostname]: value must be a valid hostname (see RFC 1123)."` + ); + + expect(() => + ConfigSchema.validate({ public: { hostname: 'localhost:5601' } }) + ).toThrowErrorMatchingInlineSnapshot( + `"[public.hostname]: value must be a valid hostname (see RFC 1123)."` + ); + }); + + it('properly validates `port`', async () => { + expect(ConfigSchema.validate({ public: { port: 1234 } }).public).toMatchInlineSnapshot(` + Object { + "port": 1234, + } + `); + + expect(ConfigSchema.validate({ public: { port: 0 } }).public).toMatchInlineSnapshot(` + Object { + "port": 0, + } + `); + + expect(ConfigSchema.validate({ public: { port: 65535 } }).public).toMatchInlineSnapshot(` + Object { + "port": 65535, + } + `); + + expect(() => + ConfigSchema.validate({ public: { port: -1 } }) + ).toThrowErrorMatchingInlineSnapshot( + `"[public.port]: Value must be equal to or greater than [0]."` + ); + + expect(() => + ConfigSchema.validate({ public: { port: 65536 } }) + ).toThrowErrorMatchingInlineSnapshot( + `"[public.port]: Value must be equal to or lower than [65535]."` + ); + + expect(() => + ConfigSchema.validate({ public: { port: '56x1' } }) + ).toThrowErrorMatchingInlineSnapshot( + `"[public.port]: expected value of type [number] but got [string]"` + ); + }); + }); + describe('authc.oidc', () => { it(`returns a validation error when authc.providers is "['oidc']" and realm is unspecified`, async () => { expect(() => ConfigSchema.validate({ authc: { providers: ['oidc'] } })).toThrow( @@ -255,14 +361,42 @@ describe('config schema', () => { }); describe('authc.saml', () => { - it('fails if authc.providers includes `saml`, but `saml.realm` is not specified', async () => { - expect(() => ConfigSchema.validate({ authc: { providers: ['saml'] } })).toThrow( - '[authc.saml.realm]: expected value of type [string] but got [undefined]' - ); + it('does not fail if authc.providers includes `saml`, but `saml.realm` is not specified', async () => { + expect(ConfigSchema.validate({ authc: { providers: ['saml'] } }).authc) + .toMatchInlineSnapshot(` + Object { + "http": Object { + "autoSchemesEnabled": true, + "enabled": true, + "schemes": Array [ + "apikey", + ], + }, + "providers": Array [ + "saml", + ], + "saml": Object {}, + "selector": Object {}, + } + `); - expect(() => ConfigSchema.validate({ authc: { providers: ['saml'], saml: {} } })).toThrow( - '[authc.saml.realm]: expected value of type [string] but got [undefined]' - ); + expect(ConfigSchema.validate({ authc: { providers: ['saml'], saml: {} } }).authc) + .toMatchInlineSnapshot(` + Object { + "http": Object { + "autoSchemesEnabled": true, + "enabled": true, + "schemes": Array [ + "apikey", + ], + }, + "providers": Array [ + "saml", + ], + "saml": Object {}, + "selector": Object {}, + } + `); expect( ConfigSchema.validate({ diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index 9daf0aff4c6c..90fccf4bc6c2 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -228,6 +228,11 @@ export const ConfigSchema = schema.object({ sameSiteCookies: schema.maybe( schema.oneOf([schema.literal('Strict'), schema.literal('Lax'), schema.literal('None')]) ), + public: schema.object({ + protocol: schema.maybe(schema.oneOf([schema.literal('http'), schema.literal('https')])), + hostname: schema.maybe(schema.string({ hostname: true })), + port: schema.maybe(schema.number({ min: 0, max: 65535 })), + }), authc: schema.object({ selector: schema.object({ enabled: schema.maybe(schema.boolean()) }), providers: schema.oneOf([schema.arrayOf(schema.string()), providersConfigSchema], { @@ -256,7 +261,7 @@ export const ConfigSchema = schema.object({ saml: providerOptionsSchema( 'saml', schema.object({ - realm: schema.string(), + realm: schema.maybe(schema.string()), maxRedirectURLSize: schema.maybe(schema.byteSize()), }) ), diff --git a/x-pack/plugins/security/server/config_deprecations.test.ts b/x-pack/plugins/security/server/config_deprecations.test.ts index ad6f81eaeeff..451867975515 100644 --- a/x-pack/plugins/security/server/config_deprecations.test.ts +++ b/x-pack/plugins/security/server/config_deprecations.test.ts @@ -286,7 +286,7 @@ describe('Config Deprecations', () => { const { messages } = applyConfigDeprecations(cloneDeep(config)); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"xpack.security.authc.providers.saml..maxRedirectURLSize\\" is is no longer used.", + "\\"xpack.security.authc.providers.saml..maxRedirectURLSize\\" is no longer used.", ] `); }); diff --git a/x-pack/plugins/security/server/config_deprecations.ts b/x-pack/plugins/security/server/config_deprecations.ts index f68112760632..169211184a32 100644 --- a/x-pack/plugins/security/server/config_deprecations.ts +++ b/x-pack/plugins/security/server/config_deprecations.ts @@ -13,6 +13,7 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ unused, }) => [ rename('sessionTimeout', 'session.idleTimeout'), + rename('authProviders', 'authc.providers'), rename('audit.appender.kind', 'audit.appender.type'), rename('audit.appender.layout.kind', 'audit.appender.layout.type'), @@ -121,7 +122,7 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ }), message: i18n.translate('xpack.security.deprecations.maxRedirectURLSizeMessage', { defaultMessage: - '"xpack.security.authc.providers.saml..maxRedirectURLSize" is is no longer used.', + '"xpack.security.authc.providers.saml..maxRedirectURLSize" is no longer used.', }), correctiveActions: { manualSteps: [ diff --git a/x-pack/test/security_api_integration/oidc.config.ts b/x-pack/test/security_api_integration/oidc.config.ts index a475d77aa568..b2822a49b204 100644 --- a/x-pack/test/security_api_integration/oidc.config.ts +++ b/x-pack/test/security_api_integration/oidc.config.ts @@ -50,7 +50,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { serverArgs: [ ...xPackAPITestsConfig.get('kbnTestServer.serverArgs'), `--plugin-path=${plugin}`, - `--xpack.security.authc.providers=${JSON.stringify(['oidc', 'basic'])}`, + `--xpack.security.authProviders=${JSON.stringify(['oidc', 'basic'])}`, '--xpack.security.authc.oidc.realm="oidc1"', ], }, From 641cef7ca6e7a61b831491c2f627eeb9ef331218 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Fri, 3 Sep 2021 12:48:32 +0300 Subject: [PATCH 04/47] Functional tests for execution context (#110299) * remove unnecessary ts-ignore * add context propagation to x-opaque-id header tests * run tests on CI * simplify logging. the action purpose follows from the context name * extend tests with the assertion against execution_context from the Kibana logs * split JSON log records only * apply suggestions proposed by Spencer --- scripts/functional_tests.js | 1 + .../execution_context_service.test.ts | 4 +- .../execution_context_service.ts | 4 +- test/common/services/es_archiver.ts | 2 - test/functional_execution_context/config.ts | 43 ++ .../ftr_provider_context.ts | 14 + test/functional_execution_context/services.ts | 11 + .../tests/execution_context.ts | 374 ++++++++++++++++++ .../tests/index.ts | 16 + 9 files changed, 463 insertions(+), 6 deletions(-) create mode 100644 test/functional_execution_context/config.ts create mode 100644 test/functional_execution_context/ftr_provider_context.ts create mode 100644 test/functional_execution_context/services.ts create mode 100644 test/functional_execution_context/tests/execution_context.ts create mode 100644 test/functional_execution_context/tests/index.ts diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index aa6e1831f5e7..c541b9faaecc 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -20,6 +20,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/api_integration/config.js'), require.resolve('../test/interpreter_functional/config.ts'), require.resolve('../test/examples/config.js'), + require.resolve('../test/functional_execution_context/config.ts'), ]; require('../src/setup_node_env'); diff --git a/src/core/server/execution_context/execution_context_service.test.ts b/src/core/server/execution_context/execution_context_service.test.ts index 3fa4de34ebda..9bb76ad78c49 100644 --- a/src/core/server/execution_context/execution_context_service.test.ts +++ b/src/core/server/execution_context/execution_context_service.test.ts @@ -109,7 +109,7 @@ describe('ExecutionContextService', () => { expect(loggingSystemMock.collect(core.logger).debug).toMatchInlineSnapshot(` Array [ Array [ - "set the execution context: {\\"type\\":\\"type-a\\",\\"name\\":\\"name-a\\",\\"id\\":\\"id-a\\",\\"description\\":\\"description-a\\"}", + "{\\"type\\":\\"type-a\\",\\"name\\":\\"name-a\\",\\"id\\":\\"id-a\\",\\"description\\":\\"description-a\\"}", ], ] `); @@ -351,7 +351,7 @@ describe('ExecutionContextService', () => { expect(loggingSystemMock.collect(core.logger).debug).toMatchInlineSnapshot(` Array [ Array [ - "stored the execution context: {\\"type\\":\\"type-a\\",\\"name\\":\\"name-a\\",\\"id\\":\\"id-a\\",\\"description\\":\\"description-a\\"}", + "{\\"type\\":\\"type-a\\",\\"name\\":\\"name-a\\",\\"id\\":\\"id-a\\",\\"description\\":\\"description-a\\"}", ], ] `); diff --git a/src/core/server/execution_context/execution_context_service.ts b/src/core/server/execution_context/execution_context_service.ts index 41b225cf1d0f..d8ff5fca7847 100644 --- a/src/core/server/execution_context/execution_context_service.ts +++ b/src/core/server/execution_context/execution_context_service.ts @@ -124,7 +124,7 @@ export class ExecutionContextService // we have to use enterWith since Hapi lifecycle model is built on event emitters. // therefore if we wrapped request handler in asyncLocalStorage.run(), we would lose context in other lifecycles. this.contextStore.enterWith(contextContainer); - this.log.debug(`set the execution context: ${JSON.stringify(contextContainer)}`); + this.log.debug(JSON.stringify(contextContainer)); } private withContext( @@ -136,7 +136,7 @@ export class ExecutionContextService } const parent = this.contextStore.getStore(); const contextContainer = new ExecutionContextContainer(context, parent); - this.log.debug(`stored the execution context: ${JSON.stringify(contextContainer)}`); + this.log.debug(JSON.stringify(contextContainer)); return this.contextStore.run(contextContainer, fn); } diff --git a/test/common/services/es_archiver.ts b/test/common/services/es_archiver.ts index d99a2e8d1023..2ea4b6ce3a43 100644 --- a/test/common/services/es_archiver.ts +++ b/test/common/services/es_archiver.ts @@ -8,8 +8,6 @@ import { EsArchiver } from '@kbn/es-archiver'; import { FtrProviderContext } from '../ftr_provider_context'; - -// @ts-ignore not TS yet import * as KibanaServer from './kibana_server'; export function EsArchiverProvider({ getService }: FtrProviderContext): EsArchiver { diff --git a/test/functional_execution_context/config.ts b/test/functional_execution_context/config.ts new file mode 100644 index 000000000000..6e4618907300 --- /dev/null +++ b/test/functional_execution_context/config.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; +import Path from 'path'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + + return { + ...functionalConfig.getAll(), + rootTags: ['skipCloud'], + testFiles: [require.resolve('./tests/')], + junit: { + reportName: 'Execution Context Functional Tests', + }, + kbnTestServer: { + ...functionalConfig.get('kbnTestServer'), + serverArgs: [ + ...functionalConfig.get('kbnTestServer.serverArgs'), + '--execution_context.enabled=true', + '--logging.appenders.file.type=file', + `--logging.appenders.file.fileName=${Path.resolve(__dirname, './kibana.log')}`, + '--logging.appenders.file.layout.type=json', + + '--logging.loggers[0].name=elasticsearch.query', + '--logging.loggers[0].level=all', + // eslint-disable-next-line prettier/prettier + '--logging.loggers[0].appenders=[\"file\"]', + + '--logging.loggers[1].name=execution_context', + '--logging.loggers[1].level=debug', + // eslint-disable-next-line prettier/prettier + '--logging.loggers[1].appenders=[\"file\"]', + ], + }, + }; +} diff --git a/test/functional_execution_context/ftr_provider_context.ts b/test/functional_execution_context/ftr_provider_context.ts new file mode 100644 index 000000000000..d4ac701735ef --- /dev/null +++ b/test/functional_execution_context/ftr_provider_context.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { GenericFtrProviderContext } from '@kbn/test'; +import { pageObjects } from '../functional/page_objects'; +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; +export { pageObjects }; diff --git a/test/functional_execution_context/services.ts b/test/functional_execution_context/services.ts new file mode 100644 index 000000000000..b0cf94fedd74 --- /dev/null +++ b/test/functional_execution_context/services.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { services as functionalServices } from '../functional/services'; + +export const services = functionalServices; diff --git a/test/functional_execution_context/tests/execution_context.ts b/test/functional_execution_context/tests/execution_context.ts new file mode 100644 index 000000000000..ad9b4332c9f0 --- /dev/null +++ b/test/functional_execution_context/tests/execution_context.ts @@ -0,0 +1,374 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import type { Ecs, KibanaExecutionContext } from 'kibana/server'; + +import Fs from 'fs/promises'; +import Path from 'path'; +import { isEqual } from 'lodash'; +import type { FtrProviderContext } from '../ftr_provider_context'; + +const logFilePath = Path.resolve(__dirname, '../kibana.log'); + +// to avoid splitting log record containing \n symbol +const endOfLine = /(?<=})\s*\n/; +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'home']); + const retry = getService('retry'); + + async function assertLogContains( + description: string, + predicate: (record: Ecs) => boolean + ): Promise { + // logs are written to disk asynchronously. I sacrificed performance to reduce flakiness. + await retry.waitFor(description, async () => { + const logsStr = await Fs.readFile(logFilePath, 'utf-8'); + const normalizedRecords = logsStr + .split(endOfLine) + .filter(Boolean) + .map((s) => JSON.parse(s)); + + return normalizedRecords.some(predicate); + }); + } + + function isExecutionContextLog( + record: string | undefined, + executionContext: KibanaExecutionContext + ) { + if (!record) return false; + try { + const object = JSON.parse(record); + return isEqual(object, executionContext); + } catch (e) { + return false; + } + } + + describe('Execution context service', () => { + before(async () => { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.home.addSampleDataSet('flights'); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + after(async () => { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.home.removeSampleDataSet('flights'); + }); + + describe('discover app', () => { + before(async () => { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + it('propagates context for Discover', async () => { + await assertLogContains( + 'execution context propagates to Elasticsearch via "x-opaque-id" header', + (record) => Boolean(record.http?.request?.id?.includes('kibana:application:discover')) + ); + + await assertLogContains('execution context propagates to Kibana logs', (record) => + isExecutionContextLog(record?.message, { + description: 'fetch documents', + id: '', + name: 'discover', + type: 'application', + // discovery doesn't have an URL since one of from the example dataset is not saved separately + url: '/app/discover', + }) + ); + + await assertLogContains('execution context propagates to Kibana logs', (record) => + isExecutionContextLog(record?.message, { + description: 'fetch chart data and total hits', + id: '', + name: 'discover', + type: 'application', + url: '/app/discover', + }) + ); + }); + }); + + describe('dashboard app', () => { + before(async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('[Flights] Global Flight Dashboard'); + await PageObjects.dashboard.waitForRenderComplete(); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + describe('propagates context for Lens visualizations', () => { + it('lnsXY', async () => { + await assertLogContains( + 'execution context propagates to Elasticsearch via "x-opaque-id" header', + (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsXY:086ac2e9-dd16-4b45-92b8-1e43ff7e3f65' + ) + ) + ); + + await assertLogContains('execution context propagates to Kibana logs', (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'lens', + name: 'lnsXY', + id: '086ac2e9-dd16-4b45-92b8-1e43ff7e3f65', + description: '[Flights] Flight count', + url: '/app/lens#/edit_by_value', + }) + ); + }); + + it('lnsMetric', async () => { + await assertLogContains( + 'execution context propagates to Elasticsearch via "x-opaque-id" header', + (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsMetric:b766e3b8-4544-46ed-99e6-9ecc4847e2a2' + ) + ) + ); + + await assertLogContains('execution context propagates to Kibana logs', (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'lens', + name: 'lnsMetric', + id: '2e33ade5-96e5-40b4-b460-493e5d4fa834', + description: '', + url: '/app/lens#/edit_by_value', + }) + ); + }); + + it('lnsDatatable', async () => { + await assertLogContains( + 'execution context propagates to Elasticsearch via "x-opaque-id" header', + (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsDatatable:fb86b32f-fb7a-45cf-9511-f366fef51bbd' + ) + ) + ); + + await assertLogContains('execution context propagates to Kibana logs', (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'lens', + name: 'lnsDatatable', + id: 'fb86b32f-fb7a-45cf-9511-f366fef51bbd', + description: 'Cities by delay, cancellation', + url: '/app/lens#/edit_by_value', + }) + ); + }); + + it('lnsPie', async () => { + await assertLogContains( + 'execution context propagates to Elasticsearch via "x-opaque-id" header', + (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsPie:5d53db36-2d5a-4adc-af7b-cec4c1a294e0' + ) + ) + ); + await assertLogContains('execution context propagates to Kibana logs', (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'lens', + name: 'lnsPie', + id: '5d53db36-2d5a-4adc-af7b-cec4c1a294e0', + description: '[Flights] Delay Type', + url: '/app/lens#/edit_by_value', + }) + ); + }); + }); + + it('propagates context for built-in Discover', async () => { + await assertLogContains( + 'execution context propagates to Elasticsearch via "x-opaque-id" header', + (record) => + Boolean( + record.http?.request?.id?.includes( + 'application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;search:discover:571aaf70-4c88-11e8-b3d7-01146121b73d' + ) + ) + ); + await assertLogContains('execution context propagates to Kibana logs', (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'search', + name: 'discover', + id: '571aaf70-4c88-11e8-b3d7-01146121b73d', + description: '[Flights] Flight Log', + url: '/app/discover#/view/571aaf70-4c88-11e8-b3d7-01146121b73d', + }) + ); + }); + + it('propagates context for TSVB visualizations', async () => { + await assertLogContains( + 'execution context propagates to Elasticsearch via "x-opaque-id" header', + (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:TSVB:bcb63b50-4c89-11e8-b3d7-01146121b73d' + ) + ) + ); + + await assertLogContains('execution context propagates to Kibana logs', (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'visualization', + name: 'TSVB', + id: 'bcb63b50-4c89-11e8-b3d7-01146121b73d', + description: '[Flights] Delays & Cancellations', + url: '/app/visualize#/edit/bcb63b50-4c89-11e8-b3d7-01146121b73d', + }) + ); + }); + + it('propagates context for Vega visualizations', async () => { + await assertLogContains( + 'execution context propagates to Elasticsearch via "x-opaque-id" header', + (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:Vega:ed78a660-53a0-11e8-acbd-0be0ad9d822b' + ) + ) + ); + + await assertLogContains('execution context propagates to Kibana logs', (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'visualization', + name: 'Vega', + id: 'ed78a660-53a0-11e8-acbd-0be0ad9d822b', + description: '[Flights] Airport Connections (Hover Over Airport)', + url: '/app/visualize#/edit/ed78a660-53a0-11e8-acbd-0be0ad9d822b', + }) + ); + }); + + it('propagates context for Tag Cloud visualization', async () => { + await assertLogContains( + 'execution context propagates to Elasticsearch via "x-opaque-id" header', + (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:Tag cloud:293b5a30-4c8f-11e8-b3d7-01146121b73d' + ) + ) + ); + + await assertLogContains('execution context propagates to Kibana logs', (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'visualization', + name: 'Tag cloud', + id: '293b5a30-4c8f-11e8-b3d7-01146121b73d', + description: '[Flights] Destination Weather', + url: '/app/visualize#/edit/293b5a30-4c8f-11e8-b3d7-01146121b73d', + }) + ); + }); + + it('propagates context for Vertical bar visualization', async () => { + await assertLogContains( + 'execution context propagates to Elasticsearch via "x-opaque-id" header', + (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:Vertical bar:9886b410-4c8b-11e8-b3d7-01146121b73d' + ) + ) + ); + + await assertLogContains('execution context propagates to Kibana logs', (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'visualization', + name: 'Vertical bar', + id: '9886b410-4c8b-11e8-b3d7-01146121b73d', + description: '[Flights] Delay Buckets', + url: '/app/visualize#/edit/9886b410-4c8b-11e8-b3d7-01146121b73d', + }) + ); + }); + }); + }); +} diff --git a/test/functional_execution_context/tests/index.ts b/test/functional_execution_context/tests/index.ts new file mode 100644 index 000000000000..6dc92f6fb3c8 --- /dev/null +++ b/test/functional_execution_context/tests/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Execution context', function () { + this.tags('ciGroup1'); + loadTestFile(require.resolve('./execution_context')); + }); +} From 4e9e7a867136090078b7464a323dbc16145e34fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?= Date: Fri, 3 Sep 2021 12:59:56 +0200 Subject: [PATCH 05/47] [RAC] Add loading and empty states to the alerts table - Take II (#110504) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/alerts_table/index.tsx | 7 +- ...on_product_no_results_magnifying_glass.svg | 1 + .../components/t_grid/integrated/index.tsx | 106 +++++++----------- .../public/components/t_grid/shared/index.tsx | 90 +++++++++++++++ .../components/t_grid/standalone/index.tsx | 48 +++----- .../public/components/t_grid/styles.tsx | 7 ++ .../timelines/public/methods/index.tsx | 11 +- .../applications/timelines_test/index.tsx | 69 ++++++------ 8 files changed, 195 insertions(+), 144 deletions(-) create mode 100644 x-pack/plugins/timelines/public/assets/illustration_product_no_results_magnifying_glass.svg create mode 100644 x-pack/plugins/timelines/public/components/t_grid/shared/index.tsx diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index e179c0298746..3c277d1d4019 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { EuiLoadingContent, EuiPanel } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { connect, ConnectedProps, useDispatch } from 'react-redux'; @@ -369,11 +368,7 @@ export const AlertsTableComponent: React.FC = ({ }, [dispatch, defaultTimelineModel, filterManager, tGridEnabled, timelineId]); if (loading || indexPatternsLoading || isEmpty(selectedPatterns)) { - return ( - - - - ); + return null; } return ( diff --git a/x-pack/plugins/timelines/public/assets/illustration_product_no_results_magnifying_glass.svg b/x-pack/plugins/timelines/public/assets/illustration_product_no_results_magnifying_glass.svg new file mode 100644 index 000000000000..b9a0df1630b2 --- /dev/null +++ b/x-pack/plugins/timelines/public/assets/illustration_product_no_results_magnifying_glass.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx index c3c83f6be72c..cdfca4e09eb1 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx @@ -8,19 +8,12 @@ import type { AlertConsumers as AlertConsumersTyped } from '@kbn/rule-data-utils'; // @ts-expect-error import { AlertConsumers as AlertConsumersNonTyped } from '@kbn/rule-data-utils/target_node/alerts_as_data_rbac'; -import { - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiLoadingContent, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; -import { FormattedMessage } from '@kbn/i18n/react'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { Direction, EntityType } from '../../../../common/search_strategy'; import type { DocValueFields } from '../../../../common/search_strategy'; @@ -53,6 +46,7 @@ import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexGroup, UpdatedFlexItem } import { Sort } from '../body/sort'; import { InspectButton, InspectButtonContainer } from '../../inspect'; import { SummaryViewSelector, ViewSelection } from '../event_rendered_view/selector'; +import { TGridLoading, TGridEmpty } from '../shared'; const AlertConsumers: typeof AlertConsumersTyped = AlertConsumersNonTyped; @@ -269,6 +263,8 @@ const TGridIntegratedComponent: React.FC = ({ [deletedEventIds.length, totalCount] ); + const hasAlerts = totalCountMinusDeleted > 0; + const nonDeletedEvents = useMemo(() => events.filter((e) => !deletedEventIds.includes(e._id)), [ deletedEventIds, events, @@ -300,7 +296,7 @@ const TGridIntegratedComponent: React.FC = ({ data-test-subj="events-viewer-panel" $isFullScreen={globalFullScreen} > - {isFirstUpdate.current && } + {isFirstUpdate.current && } {graphOverlay} @@ -325,61 +321,43 @@ const TGridIntegratedComponent: React.FC = ({ {!graphEventId && graphOverlay == null && ( - - - {totalCountMinusDeleted === 0 && loading === false && ( - - - - } - titleSize="s" - body={ -

- -

- } - /> - )} - {totalCountMinusDeleted > 0 && ( - - )} -
-
+ <> + {!hasAlerts && !loading && } + {hasAlerts && ( + + + + + + )} + )} )} diff --git a/x-pack/plugins/timelines/public/components/t_grid/shared/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/shared/index.tsx new file mode 100644 index 000000000000..563e8224058c --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/shared/index.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiImage, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import type { CoreStart } from '../../../../../../../src/core/public'; + +const heights = { + tall: 490, + short: 250, +}; + +export const TGridLoading: React.FC<{ height?: keyof typeof heights }> = ({ height = 'tall' }) => { + return ( + + + + + + + + ); +}; + +const panelStyle = { + maxWidth: 500, +}; + +export const TGridEmpty: React.FC<{ height?: keyof typeof heights }> = ({ height = 'tall' }) => { + const { http } = useKibana().services; + + return ( + + + + + + + + +

+ +

+
+

+ +

+
+
+ + + +
+
+
+
+
+ ); +}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx index ee9b7be48df6..74dd8c01295b 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx @@ -4,8 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiLoadingContent } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexItem } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useEffect, useMemo, useState, useRef } from 'react'; import styled from 'styled-components'; @@ -39,10 +38,16 @@ import type { State } from '../../../store/t_grid'; import { useTimelineEvents } from '../../../container'; import { StatefulBody } from '../body'; import { LastUpdatedAt } from '../..'; -import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexItem, UpdatedFlexGroup } from '../styles'; +import { + SELECTOR_TIMELINE_GLOBAL_CONTAINER, + UpdatedFlexItem, + UpdatedFlexGroup, + FullWidthFlexGroup, +} from '../styles'; import { InspectButton, InspectButtonContainer } from '../../inspect'; import { useFetchIndex } from '../../../container/source'; import { AddToCaseAction } from '../../actions/timeline/cases/add_to_case_action'; +import { TGridLoading, TGridEmpty } from '../shared'; export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px const STANDALONE_ID = 'standalone-t-grid'; @@ -68,12 +73,6 @@ const EventsContainerLoading = styled.div.attrs(({ className = '' }) => ({ flex-direction: column; `; -const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible: boolean }>` - overflow: hidden; - margin: 0; - display: ${({ $visible }) => ($visible ? 'flex' : 'none')}; -`; - const ScrollableFlexItem = styled(EuiFlexItem)` overflow: auto; `; @@ -255,6 +254,8 @@ const TGridStandaloneComponent: React.FC = ({ () => (totalCount > 0 ? totalCount - deletedEventIds.length : 0), [deletedEventIds.length, totalCount] ); + const hasAlerts = totalCountMinusDeleted > 0; + const activeCaseFlowId = useSelector((state: State) => tGridSelectors.activeCaseFlowId(state)); const selectedEvent = useMemo(() => { const matchedEvent = events.find((event) => event.ecs._id === activeCaseFlowId); @@ -338,14 +339,14 @@ const TGridStandaloneComponent: React.FC = ({ return ( - {isFirstUpdate.current && } + {isFirstUpdate.current && } {canQueryTimeline ? ( <> - + @@ -354,28 +355,9 @@ const TGridStandaloneComponent: React.FC = ({ - {totalCountMinusDeleted === 0 && loading === false && ( - - - - } - titleSize="s" - body={ -

- -

- } - /> - )} - {totalCountMinusDeleted > 0 && ( + {!hasAlerts && !loading && } + + {hasAlerts && ( ( }) )<{ $isVisible: boolean }>``; +export const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible?: boolean }>` + overflow: hidden; + margin: 0; + min-height: 490px; + display: ${({ $visible = true }) => ($visible ? 'flex' : 'none')}; +`; + export const UpdatedFlexGroup = styled(EuiFlexGroup)` position: absolute; z-index: ${({ theme }) => theme.eui.euiZLevel1}; diff --git a/x-pack/plugins/timelines/public/methods/index.tsx b/x-pack/plugins/timelines/public/methods/index.tsx index 91802c4eb10e..06bb1ae44321 100644 --- a/x-pack/plugins/timelines/public/methods/index.tsx +++ b/x-pack/plugins/timelines/public/methods/index.tsx @@ -6,7 +6,7 @@ */ import React, { lazy, Suspense } from 'react'; -import { EuiLoadingContent, EuiLoadingSpinner, EuiPanel } from '@elastic/eui'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { I18nProvider } from '@kbn/i18n/react'; import type { Store } from 'redux'; import { Provider } from 'react-redux'; @@ -17,6 +17,7 @@ import type { LastUpdatedAtProps, LoadingPanelProps, FieldBrowserProps } from '. import type { AddToCaseActionProps } from '../components/actions/timeline/cases/add_to_case_action'; import { initialTGridState } from '../store/t_grid/reducer'; import { createStore } from '../store/t_grid'; +import { TGridLoading } from '../components/t_grid/shared'; const initializeStore = ({ store, @@ -51,13 +52,7 @@ export const getTGridLazy = ( ) => { initializeStore({ store, storage, setStore }); return ( - - - - } - > + }> ); diff --git a/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx b/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx index adc10ae0a416..a37c00144504 100644 --- a/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx +++ b/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx @@ -11,6 +11,7 @@ import ReactDOM from 'react-dom'; import { AppMountParameters, CoreStart } from 'kibana/public'; import { I18nProvider } from '@kbn/i18n/react'; import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; +import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { TimelinesUIStart } from '../../../../../../../plugins/timelines/public'; import { DataPublicPluginStart } from '../../../../../../../../src/plugins/data/public'; @@ -60,39 +61,41 @@ const AppRoot = React.memo( - {(timelinesPluginSetup && - timelinesPluginSetup.getTGrid && - timelinesPluginSetup.getTGrid<'standalone'>({ - appId: 'securitySolution', - type: 'standalone', - casePermissions: { - read: true, - crud: true, - }, - columns: [], - indexNames: [], - deletedEventIds: [], - end: '', - footerText: 'Events', - filters: [], - hasAlertsCrudPermissions, - itemsPerPageOptions: [1, 2, 3], - loadingText: 'Loading events', - renderCellValue: () =>
test
, - sort: [], - leadingControlColumns: [], - trailingControlColumns: [], - query: { - query: '', - language: 'kuery', - }, - setRefetch, - start: '', - rowRenderers: [], - filterStatus: 'open', - unit: (n: number) => `${n}`, - })) ?? - null} + + {(timelinesPluginSetup && + timelinesPluginSetup.getTGrid && + timelinesPluginSetup.getTGrid<'standalone'>({ + appId: 'securitySolution', + type: 'standalone', + casePermissions: { + read: true, + crud: true, + }, + columns: [], + indexNames: [], + deletedEventIds: [], + end: '', + footerText: 'Events', + filters: [], + hasAlertsCrudPermissions, + itemsPerPageOptions: [1, 2, 3], + loadingText: 'Loading events', + renderCellValue: () =>
test
, + sort: [], + leadingControlColumns: [], + trailingControlColumns: [], + query: { + query: '', + language: 'kuery', + }, + setRefetch, + start: '', + rowRenderers: [], + filterStatus: 'open', + unit: (n: number) => `${n}`, + })) ?? + null} +
From dfea0fee21d93e73926092e32105d9e66fcc8c6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Fri, 3 Sep 2021 12:32:59 +0100 Subject: [PATCH 06/47] [GET /api/status] Default to v8format and allow v7format=true (#110830) --- ...in-core-server.deprecationsservicesetup.md | 4 +- .../src/kbn_client/kbn_client_plugins.ts | 14 +- .../src/kbn_client/kbn_client_status.ts | 17 +- .../public/core_app/status/lib/load_status.ts | 2 +- .../core_usage_data_service.mock.ts | 6 +- .../core_usage_data_service.test.ts | 44 +++ .../core_usage_data_service.ts | 26 +- src/core/server/core_usage_data/index.ts | 10 +- src/core/server/core_usage_data/types.ts | 49 ++- .../integration_tests/client.test.ts | 4 +- src/core/server/index.ts | 12 +- src/core/server/internal_types.ts | 3 +- src/core/server/mocks.ts | 4 + src/core/server/plugins/plugin_context.ts | 3 + .../saved_objects/routes/bulk_create.ts | 4 +- .../server/saved_objects/routes/bulk_get.ts | 4 +- .../saved_objects/routes/bulk_update.ts | 4 +- .../server/saved_objects/routes/create.ts | 4 +- .../server/saved_objects/routes/delete.ts | 4 +- .../server/saved_objects/routes/export.ts | 4 +- src/core/server/saved_objects/routes/find.ts | 4 +- src/core/server/saved_objects/routes/get.ts | 4 +- .../server/saved_objects/routes/import.ts | 4 +- src/core/server/saved_objects/routes/index.ts | 4 +- .../server/saved_objects/routes/resolve.ts | 4 +- .../routes/resolve_import_errors.ts | 4 +- .../server/saved_objects/routes/update.ts | 4 +- .../saved_objects/saved_objects_service.ts | 4 +- src/core/server/server.api.md | 23 ++ src/core/server/server.ts | 2 + .../routes/integration_tests/status.test.ts | 310 +++++++++--------- src/core/server/status/routes/status.ts | 31 +- src/core/server/status/status_service.test.ts | 2 + src/core/server/status/status_service.ts | 4 + .../server/plugin.test.ts | 2 + .../kibana_usage_collection/server/plugin.ts | 1 + test/api_integration/apis/status/status.js | 5 +- .../test_suites/core_plugins/status.ts | 2 +- .../http/platform/status.ts | 2 +- 39 files changed, 412 insertions(+), 226 deletions(-) diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md index 75732f59f1b3..eb0dbb59e6c1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md @@ -31,10 +31,10 @@ async function getDeprecations({ esClient, savedObjectsClient }: GetDeprecations // Example of a manual correctiveAction deprecations.push({ title: i18n.translate('xpack.timelion.deprecations.worksheetsTitle', { - defaultMessage: 'Found Timelion worksheets.' + defaultMessage: 'Timelion worksheets are deprecated' }), message: i18n.translate('xpack.timelion.deprecations.worksheetsMessage', { - defaultMessage: 'You have {count} Timelion worksheets. The Timelion app will be removed in 8.0. To continue using your Timelion worksheets, migrate them to a dashboard.', + defaultMessage: 'You have {count} Timelion worksheets. Migrate your Timelion worksheets to a dashboard to continue using them.', values: { count }, }), documentationUrl: diff --git a/packages/kbn-test/src/kbn_client/kbn_client_plugins.ts b/packages/kbn-test/src/kbn_client/kbn_client_plugins.ts index c730091c1478..25c3d7e156e9 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_plugins.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_plugins.ts @@ -8,26 +8,14 @@ import { KbnClientStatus } from './kbn_client_status'; -const PLUGIN_STATUS_ID = /^plugin:(.+?)@/; - export class KbnClientPlugins { constructor(private readonly status: KbnClientStatus) {} /** * Get a list of plugin ids that are enabled on the server */ public async getEnabledIds() { - const pluginIds: string[] = []; const apiResp = await this.status.get(); - for (const status of apiResp.status.statuses) { - if (status.id) { - const match = status.id.match(PLUGIN_STATUS_ID); - if (match) { - pluginIds.push(match[1]); - } - } - } - - return pluginIds; + return Object.keys(apiResp.status.plugins); } } diff --git a/packages/kbn-test/src/kbn_client/kbn_client_status.ts b/packages/kbn-test/src/kbn_client/kbn_client_status.ts index 26c46917ae8d..ed08b6b8cea1 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_status.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_status.ts @@ -9,13 +9,11 @@ import { KbnClientRequester } from './kbn_client_requester'; interface Status { - state: 'green' | 'red' | 'yellow'; - title?: string; - id?: string; - icon: string; - message: string; - uiColor: string; - since: string; + level: 'available' | 'degraded' | 'unavailable' | 'critical'; + summary: string; + detail?: string; + documentationUrl?: string; + meta?: Record; } interface ApiResponseStatus { @@ -29,7 +27,8 @@ interface ApiResponseStatus { }; status: { overall: Status; - statuses: Status[]; + core: Record; + plugins: Record; }; metrics: unknown; } @@ -55,6 +54,6 @@ export class KbnClientStatus { */ public async getOverallState() { const status = await this.get(); - return status.status.overall.state; + return status.status.overall.level; } } diff --git a/src/core/public/core_app/status/lib/load_status.ts b/src/core/public/core_app/status/lib/load_status.ts index a5cc18ffd6c1..e65764771f0f 100644 --- a/src/core/public/core_app/status/lib/load_status.ts +++ b/src/core/public/core_app/status/lib/load_status.ts @@ -145,7 +145,7 @@ export async function loadStatus({ let response: StatusResponse; try { - response = await http.get('/api/status', { query: { v8format: true } }); + response = await http.get('/api/status'); } catch (e) { // API returns a 503 response if not all services are available. // In this case, we want to treat this as a successful API call, so that we can diff --git a/src/core/server/core_usage_data/core_usage_data_service.mock.ts b/src/core/server/core_usage_data/core_usage_data_service.mock.ts index 941ac5afacb4..331a3bbb9c02 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.mock.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.mock.ts @@ -10,12 +10,14 @@ import { PublicMethodsOf } from '@kbn/utility-types'; import { BehaviorSubject } from 'rxjs'; import { CoreUsageDataService } from './core_usage_data_service'; import { coreUsageStatsClientMock } from './core_usage_stats_client.mock'; -import { CoreUsageData, CoreUsageDataSetup, CoreUsageDataStart } from './types'; +import { CoreUsageData, InternalCoreUsageDataSetup, CoreUsageDataStart } from './types'; const createSetupContractMock = (usageStatsClient = coreUsageStatsClientMock.create()) => { - const setupContract: jest.Mocked = { + const setupContract: jest.Mocked = { registerType: jest.fn(), getClient: jest.fn().mockReturnValue(usageStatsClient), + registerUsageCounter: jest.fn(), + incrementUsageCounter: jest.fn(), }; return setupContract; }; diff --git a/src/core/server/core_usage_data/core_usage_data_service.test.ts b/src/core/server/core_usage_data/core_usage_data_service.test.ts index 478cfe5daff4..3c05069d3cd0 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.test.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.test.ts @@ -150,6 +150,50 @@ describe('CoreUsageDataService', () => { expect(usageStatsClient).toBeInstanceOf(CoreUsageStatsClient); }); }); + + describe('Usage Counter', () => { + it('registers a usage counter and uses it to increment the counters', async () => { + const http = httpServiceMock.createInternalSetupContract(); + const metrics = metricsServiceMock.createInternalSetupContract(); + const savedObjectsStartPromise = Promise.resolve( + savedObjectsServiceMock.createStartContract() + ); + const changedDeprecatedConfigPath$ = configServiceMock.create().getDeprecatedConfigPath$(); + const coreUsageData = service.setup({ + http, + metrics, + savedObjectsStartPromise, + changedDeprecatedConfigPath$, + }); + const myUsageCounter = { incrementCounter: jest.fn() }; + coreUsageData.registerUsageCounter(myUsageCounter); + coreUsageData.incrementUsageCounter({ counterName: 'test' }); + expect(myUsageCounter.incrementCounter).toHaveBeenCalledWith({ counterName: 'test' }); + }); + + it('swallows errors when provided increment counter fails', async () => { + const http = httpServiceMock.createInternalSetupContract(); + const metrics = metricsServiceMock.createInternalSetupContract(); + const savedObjectsStartPromise = Promise.resolve( + savedObjectsServiceMock.createStartContract() + ); + const changedDeprecatedConfigPath$ = configServiceMock.create().getDeprecatedConfigPath$(); + const coreUsageData = service.setup({ + http, + metrics, + savedObjectsStartPromise, + changedDeprecatedConfigPath$, + }); + const myUsageCounter = { + incrementCounter: jest.fn(() => { + throw new Error('Something is really wrong'); + }), + }; + coreUsageData.registerUsageCounter(myUsageCounter); + expect(() => coreUsageData.incrementUsageCounter({ counterName: 'test' })).not.toThrow(); + expect(myUsageCounter.incrementCounter).toHaveBeenCalledWith({ counterName: 'test' }); + }); + }); }); describe('start', () => { diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts index 73f63d4d634d..ce9013d9437d 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.ts @@ -27,7 +27,7 @@ import type { CoreServicesUsageData, CoreUsageData, CoreUsageDataStart, - CoreUsageDataSetup, + InternalCoreUsageDataSetup, ConfigUsageData, CoreConfigUsageData, } from './types'; @@ -39,6 +39,7 @@ import { LEGACY_URL_ALIAS_TYPE } from '../saved_objects/object_types'; import { CORE_USAGE_STATS_TYPE } from './constants'; import { CoreUsageStatsClient } from './core_usage_stats_client'; import { MetricsServiceSetup, OpsMetrics } from '..'; +import { CoreIncrementUsageCounter } from './types'; export type ExposedConfigsToUsage = Map>; @@ -86,7 +87,8 @@ const isCustomIndex = (index: string) => { return index !== '.kibana'; }; -export class CoreUsageDataService implements CoreService { +export class CoreUsageDataService + implements CoreService { private logger: Logger; private elasticsearchConfig?: ElasticsearchConfigType; private configService: CoreContext['configService']; @@ -98,6 +100,7 @@ export class CoreUsageDataService implements CoreService {}; // Initially set to noop constructor(core: CoreContext) { this.logger = core.logger.get('core-usage-stats-service'); @@ -495,7 +498,24 @@ export class CoreUsageDataService implements CoreService { + this.incrementUsageCounter = (params) => usageCounter.incrementCounter(params); + }, + incrementUsageCounter: (params) => { + try { + this.incrementUsageCounter(params); + } catch (e) { + // Self-defense mechanism since the handler is externally registered + this.logger.debug('Failed to increase the usage counter'); + this.logger.debug(e); + } + }, + }; + + return contract; } start({ savedObjects, elasticsearch, exposedConfigsToUsage }: StartDeps) { diff --git a/src/core/server/core_usage_data/index.ts b/src/core/server/core_usage_data/index.ts index a5c62c75f62d..4687446bdb3a 100644 --- a/src/core/server/core_usage_data/index.ts +++ b/src/core/server/core_usage_data/index.ts @@ -7,7 +7,15 @@ */ export { CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID } from './constants'; -export type { CoreUsageDataSetup, ConfigUsageData, CoreUsageDataStart } from './types'; +export type { + InternalCoreUsageDataSetup, + ConfigUsageData, + CoreUsageDataStart, + CoreUsageDataSetup, + CoreUsageCounter, + CoreIncrementUsageCounter, + CoreIncrementCounterParams, +} from './types'; export { CoreUsageDataService } from './core_usage_data_service'; export { CoreUsageStatsClient, REPOSITORY_RESOLVE_OUTCOME_STATS } from './core_usage_stats_client'; diff --git a/src/core/server/core_usage_data/types.ts b/src/core/server/core_usage_data/types.ts index 563a2a337cc8..68e0b56c56db 100644 --- a/src/core/server/core_usage_data/types.ts +++ b/src/core/server/core_usage_data/types.ts @@ -280,12 +280,59 @@ export interface CoreConfigUsageData { }; } +/** + * @internal Details about the counter to be incremented + */ +export interface CoreIncrementCounterParams { + /** The name of the counter **/ + counterName: string; + /** The counter type ("count" by default) **/ + counterType?: string; + /** Increment the counter by this number (1 if not specified) **/ + incrementBy?: number; +} + +/** + * @internal + * Method to call whenever an event occurs, so the counter can be increased. + */ +export type CoreIncrementUsageCounter = (params: CoreIncrementCounterParams) => void; + +/** + * @internal + * API to track whenever an event occurs, so the core can report them. + */ +export interface CoreUsageCounter { + /** @internal {@link CoreIncrementUsageCounter} **/ + incrementCounter: CoreIncrementUsageCounter; +} + /** @internal */ -export interface CoreUsageDataSetup { +export interface InternalCoreUsageDataSetup extends CoreUsageDataSetup { registerType( typeRegistry: ISavedObjectTypeRegistry & Pick ): void; getClient(): CoreUsageStatsClient; + + /** @internal {@link CoreIncrementUsageCounter} **/ + incrementUsageCounter: CoreIncrementUsageCounter; +} + +/** + * Internal API for registering the Usage Tracker used for Core's usage data payload. + * + * @note This API should never be used to drive application logic and is only + * intended for telemetry purposes. + * + * @internal + */ +export interface CoreUsageDataSetup { + /** + * @internal + * API for a usage tracker plugin to inject the {@link CoreUsageCounter} to use + * when tracking events. + */ + registerUsageCounter: (usageCounter: CoreUsageCounter) => void; } /** diff --git a/src/core/server/elasticsearch/integration_tests/client.test.ts b/src/core/server/elasticsearch/integration_tests/client.test.ts index 6e40c638614b..83b20761df1a 100644 --- a/src/core/server/elasticsearch/integration_tests/client.test.ts +++ b/src/core/server/elasticsearch/integration_tests/client.test.ts @@ -96,8 +96,8 @@ describe('fake elasticsearch', () => { test('should return unknown product when it cannot perform the Product check (503 response)', async () => { const resp = await supertest(kibanaHttpServer).get('/api/status').expect(503); - expect(resp.body.status.overall.state).toBe('red'); - expect(resp.body.status.statuses[0].message).toBe( + expect(resp.body.status.overall.level).toBe('critical'); + expect(resp.body.status.core.elasticsearch.summary).toBe( 'Unable to retrieve version information from Elasticsearch nodes. The client noticed that the server is not Elasticsearch and we do not support this unknown product.' ); }); diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 1c3a0850d3b7..3a55d70109b8 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -55,7 +55,7 @@ import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; import { MetricsServiceSetup, MetricsServiceStart } from './metrics'; import { StatusServiceSetup } from './status'; import { AppenderConfigType, appendersSchema, LoggingServiceSetup } from './logging'; -import { CoreUsageDataStart } from './core_usage_data'; +import { CoreUsageDataStart, CoreUsageDataSetup } from './core_usage_data'; import { I18nServiceSetup } from './i18n'; import { DeprecationsServiceSetup, DeprecationsClient } from './deprecations'; // Because of #79265 we need to explicitly import, then export these types for @@ -410,7 +410,13 @@ export type { export { ServiceStatusLevels } from './status'; export type { CoreStatus, ServiceStatus, ServiceStatusLevel, StatusServiceSetup } from './status'; -export type { CoreUsageDataStart } from './core_usage_data'; +export type { + CoreUsageDataSetup, + CoreUsageDataStart, + CoreUsageCounter, + CoreIncrementUsageCounter, + CoreIncrementCounterParams, +} from './core_usage_data'; /** * Plugin specific context passed to a route handler. @@ -500,6 +506,8 @@ export interface CoreSetup; + /** @internal {@link CoreUsageDataSetup} */ + coreUsageData: CoreUsageDataSetup; } /** diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index 8fc76e8b9574..29187c3963ad 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -36,7 +36,7 @@ import { InternalRenderingServiceSetup } from './rendering'; import { InternalHttpResourcesPreboot, InternalHttpResourcesSetup } from './http_resources'; import { InternalStatusServiceSetup } from './status'; import { InternalLoggingServicePreboot, InternalLoggingServiceSetup } from './logging'; -import { CoreUsageDataStart } from './core_usage_data'; +import { CoreUsageDataStart, InternalCoreUsageDataSetup } from './core_usage_data'; import { I18nServiceSetup } from './i18n'; import { InternalDeprecationsServiceSetup, InternalDeprecationsServiceStart } from './deprecations'; import type { @@ -73,6 +73,7 @@ export interface InternalCoreSetup { logging: InternalLoggingServiceSetup; metrics: InternalMetricsServiceSetup; deprecations: InternalDeprecationsServiceSetup; + coreUsageData: InternalCoreUsageDataSetup; } /** diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index b53658b57493..f8b56e81ab18 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -169,6 +169,9 @@ function createCoreSetupMock({ metrics: metricsServiceMock.createSetupContract(), deprecations: deprecationsServiceMock.createSetupContract(), executionContext: executionContextServiceMock.createInternalSetupContract(), + coreUsageData: { + registerUsageCounter: coreUsageDataServiceMock.createSetupContract().registerUsageCounter, + }, getStartServices: jest .fn, object, any]>, []>() .mockResolvedValue([createCoreStartMock(), pluginStartDeps, pluginStartContract]), @@ -222,6 +225,7 @@ function createInternalCoreSetupMock() { metrics: metricsServiceMock.createInternalSetupContract(), deprecations: deprecationsServiceMock.createInternalSetupContract(), executionContext: executionContextServiceMock.createInternalSetupContract(), + coreUsageData: coreUsageDataServiceMock.createSetupContract(), }; return setupDeps; } diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index cbefdae52518..bdb4efde9b1f 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -211,6 +211,9 @@ export function createPluginSetupContext( }, getStartServices: () => plugin.startDependencies, deprecations: deps.deprecations.getRegistry(plugin.name), + coreUsageData: { + registerUsageCounter: deps.coreUsageData.registerUsageCounter, + }, }; } diff --git a/src/core/server/saved_objects/routes/bulk_create.ts b/src/core/server/saved_objects/routes/bulk_create.ts index 344a0d151cfb..f8438a70d041 100644 --- a/src/core/server/saved_objects/routes/bulk_create.ts +++ b/src/core/server/saved_objects/routes/bulk_create.ts @@ -8,11 +8,11 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } export const registerBulkCreateRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { diff --git a/src/core/server/saved_objects/routes/bulk_get.ts b/src/core/server/saved_objects/routes/bulk_get.ts index cf051d6cd25c..cffa69b06f4e 100644 --- a/src/core/server/saved_objects/routes/bulk_get.ts +++ b/src/core/server/saved_objects/routes/bulk_get.ts @@ -8,11 +8,11 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } export const registerBulkGetRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { diff --git a/src/core/server/saved_objects/routes/bulk_update.ts b/src/core/server/saved_objects/routes/bulk_update.ts index de47ab9c5961..277673971dab 100644 --- a/src/core/server/saved_objects/routes/bulk_update.ts +++ b/src/core/server/saved_objects/routes/bulk_update.ts @@ -8,11 +8,11 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } export const registerBulkUpdateRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { diff --git a/src/core/server/saved_objects/routes/create.ts b/src/core/server/saved_objects/routes/create.ts index 2fa7acfb6cab..0e321aa7031f 100644 --- a/src/core/server/saved_objects/routes/create.ts +++ b/src/core/server/saved_objects/routes/create.ts @@ -8,11 +8,11 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } export const registerCreateRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { diff --git a/src/core/server/saved_objects/routes/delete.ts b/src/core/server/saved_objects/routes/delete.ts index fe08acf23fd2..e8404ba7fc8c 100644 --- a/src/core/server/saved_objects/routes/delete.ts +++ b/src/core/server/saved_objects/routes/delete.ts @@ -8,11 +8,11 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } export const registerDeleteRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { diff --git a/src/core/server/saved_objects/routes/export.ts b/src/core/server/saved_objects/routes/export.ts index e0293a4522fc..e224f30a1bb0 100644 --- a/src/core/server/saved_objects/routes/export.ts +++ b/src/core/server/saved_objects/routes/export.ts @@ -11,7 +11,7 @@ import stringify from 'json-stable-stringify'; import { createPromiseFromStreams, createMapStream, createConcatStream } from '@kbn/utils'; import { IRouter, KibanaRequest } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import { SavedObjectConfig } from '../saved_objects_config'; import { SavedObjectsExportByTypeOptions, @@ -22,7 +22,7 @@ import { validateTypes, validateObjects, catchAndReturnBoomErrors } from './util interface RouteDependencies { config: SavedObjectConfig; - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } type EitherExportOptions = SavedObjectsExportByTypeOptions | SavedObjectsExportByObjectOptions; diff --git a/src/core/server/saved_objects/routes/find.ts b/src/core/server/saved_objects/routes/find.ts index d21039db30e5..6e009f80bda7 100644 --- a/src/core/server/saved_objects/routes/find.ts +++ b/src/core/server/saved_objects/routes/find.ts @@ -8,11 +8,11 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } export const registerFindRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { diff --git a/src/core/server/saved_objects/routes/get.ts b/src/core/server/saved_objects/routes/get.ts index f28822d95d81..ae0656599a1e 100644 --- a/src/core/server/saved_objects/routes/get.ts +++ b/src/core/server/saved_objects/routes/get.ts @@ -8,11 +8,11 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } export const registerGetRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { diff --git a/src/core/server/saved_objects/routes/import.ts b/src/core/server/saved_objects/routes/import.ts index 6f75bcf9fd5b..d373dd5e63bc 100644 --- a/src/core/server/saved_objects/routes/import.ts +++ b/src/core/server/saved_objects/routes/import.ts @@ -10,14 +10,14 @@ import { Readable } from 'stream'; import { extname } from 'path'; import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import { SavedObjectConfig } from '../saved_objects_config'; import { SavedObjectsImportError } from '../import'; import { catchAndReturnBoomErrors, createSavedObjectsStreamFromNdJson } from './utils'; interface RouteDependencies { config: SavedObjectConfig; - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } interface FileStream extends Readable { diff --git a/src/core/server/saved_objects/routes/index.ts b/src/core/server/saved_objects/routes/index.ts index 930e02de7657..889edfb66a20 100644 --- a/src/core/server/saved_objects/routes/index.ts +++ b/src/core/server/saved_objects/routes/index.ts @@ -7,7 +7,7 @@ */ import { InternalHttpServiceSetup } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import { Logger } from '../../logging'; import { SavedObjectConfig } from '../saved_objects_config'; import { IKibanaMigrator } from '../migrations'; @@ -34,7 +34,7 @@ export function registerRoutes({ migratorPromise, }: { http: InternalHttpServiceSetup; - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; logger: Logger; config: SavedObjectConfig; migratorPromise: Promise; diff --git a/src/core/server/saved_objects/routes/resolve.ts b/src/core/server/saved_objects/routes/resolve.ts index ba409f7db7b6..78e85d17fe1f 100644 --- a/src/core/server/saved_objects/routes/resolve.ts +++ b/src/core/server/saved_objects/routes/resolve.ts @@ -8,10 +8,10 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; interface RouteDependencies { - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } export const registerResolveRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { diff --git a/src/core/server/saved_objects/routes/resolve_import_errors.ts b/src/core/server/saved_objects/routes/resolve_import_errors.ts index a05c7d30b91f..f1fe2e9cfe43 100644 --- a/src/core/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts @@ -11,13 +11,13 @@ import { Readable } from 'stream'; import { schema } from '@kbn/config-schema'; import { chain } from 'lodash'; import { IRouter } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import { SavedObjectConfig } from '../saved_objects_config'; import { SavedObjectsImportError } from '../import'; import { catchAndReturnBoomErrors, createSavedObjectsStreamFromNdJson } from './utils'; interface RouteDependencies { config: SavedObjectConfig; - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } interface FileStream extends Readable { diff --git a/src/core/server/saved_objects/routes/update.ts b/src/core/server/saved_objects/routes/update.ts index b6dd9dc8e9ac..f21fc183cdad 100644 --- a/src/core/server/saved_objects/routes/update.ts +++ b/src/core/server/saved_objects/routes/update.ts @@ -8,12 +8,12 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; -import { CoreUsageDataSetup } from '../../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../../core_usage_data'; import type { SavedObjectsUpdateOptions } from '../service/saved_objects_client'; import { catchAndReturnBoomErrors } from './utils'; interface RouteDependencies { - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } export const registerUpdateRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index b25e51da3a74..074eae55acae 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -16,7 +16,7 @@ import { } from './'; import { KibanaMigrator, IKibanaMigrator } from './migrations'; import { CoreContext } from '../core_context'; -import { CoreUsageDataSetup } from '../core_usage_data'; +import { InternalCoreUsageDataSetup } from '../core_usage_data'; import { ElasticsearchClient, InternalElasticsearchServiceSetup, @@ -250,7 +250,7 @@ export interface SavedObjectsRepositoryFactory { export interface SavedObjectsSetupDeps { http: InternalHttpServiceSetup; elasticsearch: InternalElasticsearchServiceSetup; - coreUsageData: CoreUsageDataSetup; + coreUsageData: InternalCoreUsageDataSetup; } interface WrappedClientFactoryWrapper { diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index aa421fe39305..5abd1171a193 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -359,6 +359,16 @@ export interface CoreEnvironmentUsageData { // @internal (undocumented) export type CoreId = symbol; +// @internal +export interface CoreIncrementCounterParams { + counterName: string; + counterType?: string; + incrementBy?: number; +} + +// @internal +export type CoreIncrementUsageCounter = (params: CoreIncrementCounterParams) => void; + // @public export interface CorePreboot { // (undocumented) @@ -395,6 +405,8 @@ export interface CoreSetup void; +} + // @internal export interface CoreUsageDataStart { // (undocumented) diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 27c35031db46..865cc71a7e26 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -243,6 +243,7 @@ export class Server { environment: environmentSetup, http: httpSetup, metrics: metricsSetup, + coreUsageData: coreUsageDataSetup, }); const renderingSetup = await this.rendering.setup({ @@ -278,6 +279,7 @@ export class Server { logging: loggingSetup, metrics: metricsSetup, deprecations: deprecationsSetup, + coreUsageData: coreUsageDataSetup, }; const pluginsSetup = await this.plugins.setup(coreSetup); diff --git a/src/core/server/status/routes/integration_tests/status.test.ts b/src/core/server/status/routes/integration_tests/status.test.ts index 645ce0b24161..082be62f8dc0 100644 --- a/src/core/server/status/routes/integration_tests/status.test.ts +++ b/src/core/server/status/routes/integration_tests/status.test.ts @@ -29,6 +29,7 @@ describe('GET /api/status', () => { let server: HttpService; let httpSetup: InternalHttpServiceSetup; let metrics: jest.Mocked; + let incrementUsageCounter: jest.Mock; const setupServer = async ({ allowAnonymous = true }: { allowAnonymous?: boolean } = {}) => { const coreContext = createCoreContext({ coreId }); @@ -50,6 +51,8 @@ describe('GET /api/status', () => { d: { level: ServiceStatusLevels.critical, summary: 'd is critical' }, }); + incrementUsageCounter = jest.fn(); + const router = httpSetup.createRouter(''); registerStatusRoute({ router, @@ -71,6 +74,7 @@ describe('GET /api/status', () => { core$: status.core$, plugins$: pluginsStatus$, }, + incrementUsageCounter, }); // Register dummy auth provider for testing auth @@ -137,69 +141,75 @@ describe('GET /api/status', () => { }); describe('legacy status format', () => { - it('returns legacy status format when no query params provided', async () => { - await setupServer(); - const result = await supertest(httpSetup.server.listener).get('/api/status').expect(200); - expect(result.body.status).toEqual({ - overall: { + const legacyFormat = { + overall: { + icon: 'success', + nickname: 'Looking good', + since: expect.any(String), + state: 'green', + title: 'Green', + uiColor: 'secondary', + }, + statuses: [ + { + icon: 'success', + id: 'core:elasticsearch@9.9.9', + message: 'Service is working', + since: expect.any(String), + state: 'green', + uiColor: 'secondary', + }, + { icon: 'success', - nickname: 'Looking good', + id: 'core:savedObjects@9.9.9', + message: 'Service is working', since: expect.any(String), state: 'green', - title: 'Green', uiColor: 'secondary', }, - statuses: [ - { - icon: 'success', - id: 'core:elasticsearch@9.9.9', - message: 'Service is working', - since: expect.any(String), - state: 'green', - uiColor: 'secondary', - }, - { - icon: 'success', - id: 'core:savedObjects@9.9.9', - message: 'Service is working', - since: expect.any(String), - state: 'green', - uiColor: 'secondary', - }, - { - icon: 'success', - id: 'plugin:a@9.9.9', - message: 'a is available', - since: expect.any(String), - state: 'green', - uiColor: 'secondary', - }, - { - icon: 'warning', - id: 'plugin:b@9.9.9', - message: 'b is degraded', - since: expect.any(String), - state: 'yellow', - uiColor: 'warning', - }, - { - icon: 'danger', - id: 'plugin:c@9.9.9', - message: 'c is unavailable', - since: expect.any(String), - state: 'red', - uiColor: 'danger', - }, - { - icon: 'danger', - id: 'plugin:d@9.9.9', - message: 'd is critical', - since: expect.any(String), - state: 'red', - uiColor: 'danger', - }, - ], - }); + { + icon: 'success', + id: 'plugin:a@9.9.9', + message: 'a is available', + since: expect.any(String), + state: 'green', + uiColor: 'secondary', + }, + { + icon: 'warning', + id: 'plugin:b@9.9.9', + message: 'b is degraded', + since: expect.any(String), + state: 'yellow', + uiColor: 'warning', + }, + { + icon: 'danger', + id: 'plugin:c@9.9.9', + message: 'c is unavailable', + since: expect.any(String), + state: 'red', + uiColor: 'danger', + }, + { + icon: 'danger', + id: 'plugin:d@9.9.9', + message: 'd is critical', + since: expect.any(String), + state: 'red', + uiColor: 'danger', + }, + ], + }; + + it('returns legacy status format when v7format=true is provided', async () => { + await setupServer(); + const result = await supertest(httpSetup.server.listener) + .get('/api/status?v7format=true') + .expect(200); + expect(result.body.status).toEqual(legacyFormat); + expect(incrementUsageCounter).toHaveBeenCalledTimes(1); + expect(incrementUsageCounter).toHaveBeenCalledWith({ counterName: 'status_v7format' }); }); it('returns legacy status format when v8format=false is provided', async () => { @@ -207,109 +217,105 @@ describe('GET /api/status', () => { const result = await supertest(httpSetup.server.listener) .get('/api/status?v8format=false') .expect(200); - expect(result.body.status).toEqual({ - overall: { - icon: 'success', - nickname: 'Looking good', - since: expect.any(String), - state: 'green', - title: 'Green', - uiColor: 'secondary', - }, - statuses: [ - { - icon: 'success', - id: 'core:elasticsearch@9.9.9', - message: 'Service is working', - since: expect.any(String), - state: 'green', - uiColor: 'secondary', - }, - { - icon: 'success', - id: 'core:savedObjects@9.9.9', - message: 'Service is working', - since: expect.any(String), - state: 'green', - uiColor: 'secondary', - }, - { - icon: 'success', - id: 'plugin:a@9.9.9', - message: 'a is available', - since: expect.any(String), - state: 'green', - uiColor: 'secondary', - }, - { - icon: 'warning', - id: 'plugin:b@9.9.9', - message: 'b is degraded', - since: expect.any(String), - state: 'yellow', - uiColor: 'warning', - }, - { - icon: 'danger', - id: 'plugin:c@9.9.9', - message: 'c is unavailable', - since: expect.any(String), - state: 'red', - uiColor: 'danger', - }, - { - icon: 'danger', - id: 'plugin:d@9.9.9', - message: 'd is critical', - since: expect.any(String), - state: 'red', - uiColor: 'danger', - }, - ], - }); + expect(result.body.status).toEqual(legacyFormat); + expect(incrementUsageCounter).toHaveBeenCalledTimes(1); + expect(incrementUsageCounter).toHaveBeenCalledWith({ counterName: 'status_v7format' }); }); }); describe('v8format', () => { - it('returns new status format when v8format=true is provided', async () => { - await setupServer(); - const result = await supertest(httpSetup.server.listener) - .get('/api/status?v8format=true') - .expect(200); - expect(result.body.status).toEqual({ - core: { - elasticsearch: { - level: 'available', - summary: 'Service is working', - }, - savedObjects: { - level: 'available', - summary: 'Service is working', - }, + const newFormat = { + core: { + elasticsearch: { + level: 'available', + summary: 'Service is working', }, - overall: { + savedObjects: { level: 'available', summary: 'Service is working', }, - plugins: { - a: { - level: 'available', - summary: 'a is available', - }, - b: { - level: 'degraded', - summary: 'b is degraded', - }, - c: { - level: 'unavailable', - summary: 'c is unavailable', - }, - d: { - level: 'critical', - summary: 'd is critical', - }, + }, + overall: { + level: 'available', + summary: 'Service is working', + }, + plugins: { + a: { + level: 'available', + summary: 'a is available', + }, + b: { + level: 'degraded', + summary: 'b is degraded', + }, + c: { + level: 'unavailable', + summary: 'c is unavailable', }, - }); + d: { + level: 'critical', + summary: 'd is critical', + }, + }, + }; + + it('returns new status format when no query params are provided', async () => { + await setupServer(); + const result = await supertest(httpSetup.server.listener).get('/api/status').expect(200); + expect(result.body.status).toEqual(newFormat); + expect(incrementUsageCounter).not.toHaveBeenCalled(); + }); + + it('returns new status format when v8format=true is provided', async () => { + await setupServer(); + const result = await supertest(httpSetup.server.listener) + .get('/api/status?v8format=true') + .expect(200); + expect(result.body.status).toEqual(newFormat); + expect(incrementUsageCounter).not.toHaveBeenCalled(); + }); + + it('returns new status format when v7format=false is provided', async () => { + await setupServer(); + const result = await supertest(httpSetup.server.listener) + .get('/api/status?v7format=false') + .expect(200); + expect(result.body.status).toEqual(newFormat); + expect(incrementUsageCounter).not.toHaveBeenCalled(); + }); + }); + + describe('invalid query parameters', () => { + it('v8format=true and v7format=true', async () => { + await setupServer(); + await supertest(httpSetup.server.listener) + .get('/api/status?v8format=true&v7format=true') + .expect(400); + expect(incrementUsageCounter).not.toHaveBeenCalled(); + }); + + it('v8format=true and v7format=false', async () => { + await setupServer(); + await supertest(httpSetup.server.listener) + .get('/api/status?v8format=true&v7format=false') + .expect(400); + expect(incrementUsageCounter).not.toHaveBeenCalled(); + }); + + it('v8format=false and v7format=false', async () => { + await setupServer(); + await supertest(httpSetup.server.listener) + .get('/api/status?v8format=false&v7format=false') + .expect(400); + expect(incrementUsageCounter).not.toHaveBeenCalled(); + }); + + it('v8format=false and v7format=true', async () => { + await setupServer(); + await supertest(httpSetup.server.listener) + .get('/api/status?v8format=false&v7format=true') + .expect(400); + expect(incrementUsageCounter).not.toHaveBeenCalled(); }); }); }); diff --git a/src/core/server/status/routes/status.ts b/src/core/server/status/routes/status.ts index 43a596bd1e0e..861b41c58a89 100644 --- a/src/core/server/status/routes/status.ts +++ b/src/core/server/status/routes/status.ts @@ -12,6 +12,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; import { MetricsServiceSetup } from '../../metrics'; +import type { CoreIncrementUsageCounter } from '../../core_usage_data/types'; import { ServiceStatus, CoreStatus, ServiceStatusLevels } from '../types'; import { PluginName } from '../../plugins'; import { calculateLegacyStatus, LegacyStatusInfo } from '../legacy_status'; @@ -34,6 +35,7 @@ interface Deps { core$: Observable; plugins$: Observable>; }; + incrementUsageCounter: CoreIncrementUsageCounter; } interface StatusInfo { @@ -47,7 +49,13 @@ interface StatusHttpBody extends Omit { status: StatusInfo | LegacyStatusInfo; } -export const registerStatusRoute = ({ router, config, metrics, status }: Deps) => { +export const registerStatusRoute = ({ + router, + config, + metrics, + status, + incrementUsageCounter, +}: Deps) => { // Since the status.plugins$ observable is not subscribed to elsewhere, we need to subscribe it here to eagerly load // the plugins status when Kibana starts up so this endpoint responds quickly on first boot. const combinedStatus$ = new ReplaySubject< @@ -63,9 +71,19 @@ export const registerStatusRoute = ({ router, config, metrics, status }: Deps) = tags: ['api'], // ensures that unauthenticated calls receive a 401 rather than a 302 redirect to login page }, validate: { - query: schema.object({ - v8format: schema.boolean({ defaultValue: false }), - }), + query: schema.object( + { + v7format: schema.maybe(schema.boolean()), + v8format: schema.maybe(schema.boolean()), + }, + { + validate: ({ v7format, v8format }) => { + if (typeof v7format === 'boolean' && typeof v8format === 'boolean') { + return `provide only one format option: v7format or v8format`; + } + }, + } + ), }, }, async (context, req, res) => { @@ -73,14 +91,17 @@ export const registerStatusRoute = ({ router, config, metrics, status }: Deps) = const versionWithoutSnapshot = version.replace(SNAPSHOT_POSTFIX, ''); const [overall, core, plugins] = await combinedStatus$.pipe(first()).toPromise(); + const { v8format = true, v7format = false } = req.query ?? {}; + let statusInfo: StatusInfo | LegacyStatusInfo; - if (req.query?.v8format) { + if (!v7format && v8format) { statusInfo = { overall, core, plugins, }; } else { + incrementUsageCounter({ counterName: 'status_v7format' }); statusInfo = calculateLegacyStatus({ overall, core, diff --git a/src/core/server/status/status_service.test.ts b/src/core/server/status/status_service.test.ts index 4ead81a6638d..9148f69e079a 100644 --- a/src/core/server/status/status_service.test.ts +++ b/src/core/server/status/status_service.test.ts @@ -18,6 +18,7 @@ import { httpServiceMock } from '../http/http_service.mock'; import { mockRouter, RouterMock } from '../http/router/router.mock'; import { metricsServiceMock } from '../metrics/metrics_service.mock'; import { configServiceMock } from '../config/mocks'; +import { coreUsageDataServiceMock } from '../core_usage_data/core_usage_data_service.mock'; expect.addSnapshotSerializer(ServiceStatusLevelSnapshotSerializer); @@ -51,6 +52,7 @@ describe('StatusService', () => { environment: environmentServiceMock.createSetupContract(), http: httpServiceMock.createInternalSetupContract(), metrics: metricsServiceMock.createInternalSetupContract(), + coreUsageData: coreUsageDataServiceMock.createSetupContract(), ...overrides, }; }; diff --git a/src/core/server/status/status_service.ts b/src/core/server/status/status_service.ts index 8e9db30bbebd..107074bdb98b 100644 --- a/src/core/server/status/status_service.ts +++ b/src/core/server/status/status_service.ts @@ -20,6 +20,7 @@ import { PluginName } from '../plugins'; import { InternalMetricsServiceSetup } from '../metrics'; import { registerStatusRoute } from './routes'; import { InternalEnvironmentServiceSetup } from '../environment'; +import type { InternalCoreUsageDataSetup } from '../core_usage_data'; import { config, StatusConfigType } from './status_config'; import { ServiceStatus, CoreStatus, InternalStatusServiceSetup } from './types'; @@ -38,6 +39,7 @@ interface SetupDeps { http: InternalHttpServiceSetup; metrics: InternalMetricsServiceSetup; savedObjects: Pick; + coreUsageData: Pick; } export class StatusService implements CoreService { @@ -61,6 +63,7 @@ export class StatusService implements CoreService { metrics, savedObjects, environment, + coreUsageData, }: SetupDeps) { const statusConfig = await this.config$.pipe(take(1)).toPromise(); const core$ = this.setupCoreStatus({ elasticsearch, savedObjects }); @@ -101,6 +104,7 @@ export class StatusService implements CoreService { plugins$: this.pluginsStatus.getAll$(), core$, }, + incrementUsageCounter: coreUsageData.incrementUsageCounter, }; const router = http.createRouter(''); diff --git a/src/plugins/kibana_usage_collection/server/plugin.test.ts b/src/plugins/kibana_usage_collection/server/plugin.test.ts index 1584366a42dc..75c323ba0332 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.test.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.test.ts @@ -42,6 +42,8 @@ describe('kibana_usage_collection', () => { expect(pluginInstance.setup(coreSetup, { usageCollection })).toBe(undefined); + expect(coreSetup.coreUsageData.registerUsageCounter).toHaveBeenCalled(); + await expect( Promise.all( usageCollectors.map(async (usageCollector) => { diff --git a/src/plugins/kibana_usage_collection/server/plugin.ts b/src/plugins/kibana_usage_collection/server/plugin.ts index dadb4283e84a..275dcc761125 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.ts @@ -73,6 +73,7 @@ export class KibanaUsageCollectionPlugin implements Plugin { public setup(coreSetup: CoreSetup, { usageCollection }: KibanaUsageCollectionPluginsDepsSetup) { usageCollection.createUsageCounter('uiCounters'); this.eventLoopUsageCounter = usageCollection.createUsageCounter('eventLoop'); + coreSetup.coreUsageData.registerUsageCounter(usageCollection.createUsageCounter('core')); this.registerUsageCollectors( usageCollection, coreSetup, diff --git a/test/api_integration/apis/status/status.js b/test/api_integration/apis/status/status.js index 22076b2cddbc..e1545c448fce 100644 --- a/test/api_integration/apis/status/status.js +++ b/test/api_integration/apis/status/status.js @@ -25,9 +25,10 @@ export default function ({ getService }) { expect(body.version.build_number).to.be.a('number'); expect(body.status.overall).to.be.an('object'); - expect(body.status.overall.state).to.be('green'); + expect(body.status.overall.level).to.be('available'); - expect(body.status.statuses).to.be.an('array'); + expect(body.status.core).to.be.an('object'); + expect(body.status.plugins).to.be.an('object'); expect(body.metrics.collection_interval_in_millis).to.be.a('number'); diff --git a/test/plugin_functional/test_suites/core_plugins/status.ts b/test/plugin_functional/test_suites/core_plugins/status.ts index 2b0f15cb3927..10ca8c672204 100644 --- a/test/plugin_functional/test_suites/core_plugins/status.ts +++ b/test/plugin_functional/test_suites/core_plugins/status.ts @@ -16,7 +16,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); const getStatus = async (pluginName?: string) => { - const resp = await supertest.get('/api/status?v8format=true'); + const resp = await supertest.get('/api/status'); if (pluginName) { return resp.body.status.plugins[pluginName]; diff --git a/test/server_integration/http/platform/status.ts b/test/server_integration/http/platform/status.ts index 0dcf82c9bea9..e443ce3f31cb 100644 --- a/test/server_integration/http/platform/status.ts +++ b/test/server_integration/http/platform/status.ts @@ -18,7 +18,7 @@ export default function ({ getService }: FtrProviderContext) { const retry = getService('retry'); const getStatus = async (pluginName: string): Promise => { - const resp = await supertest.get('/api/status?v8format=true'); + const resp = await supertest.get('/api/status'); return resp.body.status.plugins[pluginName]; }; From df8ed81195f4772f203538ab1c5e4f5bd9d60871 Mon Sep 17 00:00:00 2001 From: ymao1 Date: Fri, 3 Sep 2021 07:35:17 -0400 Subject: [PATCH 07/47] Adding experimental to event log mentions in the docs (#110876) --- .../alerting/troubleshooting/alerting-common-issues.asciidoc | 2 +- docs/user/alerting/troubleshooting/event-log-index.asciidoc | 2 ++ .../alerting-production-considerations.asciidoc | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/user/alerting/troubleshooting/alerting-common-issues.asciidoc b/docs/user/alerting/troubleshooting/alerting-common-issues.asciidoc index c57e9876a411..408b18143f27 100644 --- a/docs/user/alerting/troubleshooting/alerting-common-issues.asciidoc +++ b/docs/user/alerting/troubleshooting/alerting-common-issues.asciidoc @@ -68,7 +68,7 @@ Rules are taking a long time to execute and are impacting the overall health of [IMPORTANT] ============================================== -By default, only users with a `superuser` role can query the {kib} event log because it is a system index. To enable additional users to execute this query, assign `read` privileges to the `.kibana-event-log*` index. +By default, only users with a `superuser` role can query the experimental[] {kib} event log because it is a system index. To enable additional users to execute this query, assign `read` privileges to the `.kibana-event-log*` index. ============================================== *Solution* diff --git a/docs/user/alerting/troubleshooting/event-log-index.asciidoc b/docs/user/alerting/troubleshooting/event-log-index.asciidoc index fa5b5831c04e..393b982b279f 100644 --- a/docs/user/alerting/troubleshooting/event-log-index.asciidoc +++ b/docs/user/alerting/troubleshooting/event-log-index.asciidoc @@ -2,6 +2,8 @@ [[event-log-index]] === Event log index +experimental[] + Use the event log index to determine: * Whether a rule successfully ran but its associated actions did not diff --git a/docs/user/production-considerations/alerting-production-considerations.asciidoc b/docs/user/production-considerations/alerting-production-considerations.asciidoc index 57cc2a72a889..cd8a60a1d5fe 100644 --- a/docs/user/production-considerations/alerting-production-considerations.asciidoc +++ b/docs/user/production-considerations/alerting-production-considerations.asciidoc @@ -54,6 +54,8 @@ Predicting the buffer required to account for actions depends heavily on the rul [[event-log-ilm]] === Event log index lifecycle managment +experimental[] + Alerts and actions log activity in a set of "event log" indices. These indices are configured with an index lifecycle management (ILM) policy, which you can customize. The default policy rolls over the index when it reaches 50GB, or after 30 days. Indices over 90 days old are deleted. The name of the index policy is `kibana-event-log-policy`. {kib} creates the index policy on startup, if it doesn't already exist. The index policy can be customized for your environment, but {kib} never modifies the index policy after creating it. From 5b4d26557179665130b9f1c61cc9a07138666325 Mon Sep 17 00:00:00 2001 From: Ashokaditya Date: Fri, 3 Sep 2021 13:47:36 +0200 Subject: [PATCH 08/47] [Security Solution][Endpoint] Use super date picker instead of date range picker (#108722) * Use super date picker instead of date range picker fixes elastic/security-team/issues/1571 * fix test target Super date picker's `data-test-subj` prop gets garbled and doesn't show up in rendered DOM. In other words, the component is entirely void of a data-test-subj attribute. * make auto refresh work!! fixes https://github.com/elastic/security-team/issues/1571 * set max width as per mock fixes elastic/security-team/issues/1571 * show a callout to inform users to select different date ranges fixes elastic/security-team/issues/1571 * persist recently used date ranges on the component only fixes elastic/security-team/issues/1571 * use commonly used ranges from default common security solution ranges fixes elastic/security-team/issues/1571 * Better align date picker * full width panel for date picker so content flows below it review comments * mock time picker settings for tests * use eui token for bg color review comment * persist recently used dates fixes elastic/security-team/issues/1571 * persist date range selection over new endpoint selection review comments * remove obsolete local state since update button is not visible. review comments * fix bg color for dark mode and relative path * update relative path review comments * cleanup - the action doesn't allow for undefined start and end dates anyway refs 28a859ab3a5fbbd9a14f4009ded671b969f2dc09 * fix types after sync * update test title * add a test for callout when empty data * fix lint * show update button when dates are changed Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/endpoint/schema/actions.ts | 4 +- .../common/endpoint/types/actions.ts | 4 +- .../management/pages/endpoint_hosts/mocks.ts | 2 + .../pages/endpoint_hosts/store/action.ts | 19 ++- .../pages/endpoint_hosts/store/builders.ts | 9 +- .../pages/endpoint_hosts/store/index.test.ts | 7 + .../endpoint_hosts/store/middleware.test.ts | 6 + .../pages/endpoint_hosts/store/middleware.ts | 4 +- .../pages/endpoint_hosts/store/reducer.ts | 63 +++++++- .../management/pages/endpoint_hosts/types.ts | 10 +- .../pages/endpoint_hosts/utils.test.ts | 14 +- .../management/pages/endpoint_hosts/utils.ts | 11 +- .../activity_log_date_range_picker/index.tsx | 153 +++++++++++------- .../view/details/endpoint_activity_log.tsx | 13 ++ .../view/details/endpoints.stories.tsx | 2 + .../pages/endpoint_hosts/view/index.test.tsx | 124 +++++++++++--- .../pages/endpoint_hosts/view/translations.ts | 21 +-- .../endpoint/routes/actions/audit_log.test.ts | 20 +-- .../server/endpoint/services/actions.ts | 19 +-- 19 files changed, 360 insertions(+), 145 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts b/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts index 98cb7729c944..69fce914cb1d 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts @@ -23,8 +23,8 @@ export const EndpointActionLogRequestSchema = { query: schema.object({ page: schema.number({ defaultValue: 1, min: 1 }), page_size: schema.number({ defaultValue: 10, min: 1, max: 100 }), - start_date: schema.maybe(schema.string()), - end_date: schema.maybe(schema.string()), + start_date: schema.string(), + end_date: schema.string(), }), params: schema.object({ agent_id: schema.string(), diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index d49868aae922..c6d30825c21c 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -65,8 +65,8 @@ export type ActivityLogEntry = ActivityLogAction | ActivityLogActionResponse; export interface ActivityLog { page: number; pageSize: number; - startDate?: string; - endDate?: string; + startDate: string; + endDate: string; data: ActivityLogEntry[]; } diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts index 0ac73df6704c..9c557f83012b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts @@ -126,6 +126,8 @@ export const endpointActivityLogHttpMock = httpHandlerMockFactory => { disabled: false, page: 1, pageSize: 50, - startDate: undefined, - endDate: undefined, + startDate: 'now-1d', + endDate: 'now', isInvalidDateRange: false, + autoRefreshOptions: { + enabled: false, + duration: DEFAULT_POLL_INTERVAL, + }, + recentlyUsedDateRanges: [], }, logData: createUninitialisedResourceState(), }, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts index 7fbe2dfc0a09..49ba88fd4771 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts @@ -48,7 +48,14 @@ describe('EndpointList store concerns', () => { disabled: false, page: 1, pageSize: 50, + startDate: 'now-1d', + endDate: 'now', isInvalidDateRange: false, + autoRefreshOptions: { + enabled: false, + duration: DEFAULT_POLL_INTERVAL, + }, + recentlyUsedDateRanges: [], }, logData: { type: 'UninitialisedResourceState' }, }, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index e51fe15e7130..83d3e62cf98f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -267,6 +267,8 @@ describe('endpoint list middleware', () => { payload: { page, pageSize: 50, + startDate: 'now-1d', + endDate: 'now', }, }); }; @@ -311,6 +313,8 @@ describe('endpoint list middleware', () => { expect(mockedApis.responseProvider.activityLogResponse).toHaveBeenCalledWith({ path: expect.any(String), query: { + end_date: 'now', + start_date: 'now-1d', page: 1, page_size: 50, }, @@ -396,6 +400,8 @@ describe('endpoint list middleware', () => { query: { page: 3, page_size: 50, + start_date: 'now-1d', + end_date: 'now', }, }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index df4361a6048a..6b88183db684 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -640,12 +640,12 @@ async function endpointDetailsActivityLogChangedMiddleware({ }); try { - const { page, pageSize } = getActivityLogDataPaging(getState()); + const { page, pageSize, startDate, endDate } = getActivityLogDataPaging(getState()); const route = resolvePathVariables(ENDPOINT_ACTION_LOG_ROUTE, { agent_id: selectedAgent(getState()), }); const activityLog = await coreStart.http.get(route, { - query: { page, page_size: pageSize }, + query: { page, page_size: pageSize, start_date: startDate, end_date: endDate }, }); dispatch({ type: 'endpointDetailsActivityLogChanged', diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index 02d2adce833c..b16caf00b4e2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -24,6 +24,7 @@ import { AppAction } from '../../../../common/store/actions'; import { ImmutableReducer } from '../../../../common/store'; import { Immutable } from '../../../../../common/endpoint/types'; import { createUninitialisedResourceState, isUninitialisedResourceState } from '../../../state'; +import { DEFAULT_POLL_INTERVAL } from '../../../common/constants'; type StateReducer = ImmutableReducer; type CaseReducer = ( @@ -172,7 +173,11 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta }, }, }; - } else if (action.type === 'endpointDetailsActivityLogUpdatePaging') { + } else if ( + action.type === 'endpointDetailsActivityLogUpdatePaging' || + action.type === 'endpointDetailsActivityLogUpdateIsInvalidDateRange' || + action.type === 'userUpdatedActivityLogRefreshOptions' + ) { return { ...state, endpointDetails: { @@ -186,7 +191,7 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta }, }, }; - } else if (action.type === 'endpointDetailsActivityLogUpdateIsInvalidDateRange') { + } else if (action.type === 'userUpdatedActivityLogRecentlyUsedDateRanges') { return { ...state, endpointDetails: { @@ -195,7 +200,7 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta ...state.endpointDetails.activityLog, paging: { ...state.endpointDetails.activityLog.paging, - ...action.payload, + recentlyUsedDateRanges: action.payload, }, }, }, @@ -315,9 +320,16 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta logData: createUninitialisedResourceState(), paging: { disabled: false, + isInvalidDateRange: false, page: 1, pageSize: 50, - isInvalidDateRange: false, + startDate: 'now-1d', + endDate: 'now', + autoRefreshOptions: { + enabled: false, + duration: DEFAULT_POLL_INTERVAL, + }, + recentlyUsedDateRanges: [], }, }; @@ -337,7 +349,16 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta ...stateUpdates, endpointDetails: { ...state.endpointDetails, - activityLog, + activityLog: { + ...activityLog, + paging: { + ...activityLog.paging, + startDate: state.endpointDetails.activityLog.paging.startDate, + endDate: state.endpointDetails.activityLog.paging.endDate, + recentlyUsedDateRanges: + state.endpointDetails.activityLog.paging.recentlyUsedDateRanges, + }, + }, hostDetails: { ...state.endpointDetails.hostDetails, detailsError: undefined, @@ -355,7 +376,16 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta ...stateUpdates, endpointDetails: { ...state.endpointDetails, - activityLog, + activityLog: { + ...activityLog, + paging: { + ...activityLog.paging, + startDate: state.endpointDetails.activityLog.paging.startDate, + endDate: state.endpointDetails.activityLog.paging.endDate, + recentlyUsedDateRanges: + state.endpointDetails.activityLog.paging.recentlyUsedDateRanges, + }, + }, hostDetails: { ...state.endpointDetails.hostDetails, detailsLoading: !isNotLoadingDetails, @@ -372,7 +402,16 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta ...stateUpdates, endpointDetails: { ...state.endpointDetails, - activityLog, + activityLog: { + ...activityLog, + paging: { + ...activityLog.paging, + startDate: state.endpointDetails.activityLog.paging.startDate, + endDate: state.endpointDetails.activityLog.paging.endDate, + recentlyUsedDateRanges: + state.endpointDetails.activityLog.paging.recentlyUsedDateRanges, + }, + }, hostDetails: { ...state.endpointDetails.hostDetails, detailsLoading: true, @@ -391,7 +430,15 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta ...stateUpdates, endpointDetails: { ...state.endpointDetails, - activityLog, + activityLog: { + ...activityLog, + paging: { + ...activityLog.paging, + startDate: state.endpointDetails.activityLog.paging.startDate, + endDate: state.endpointDetails.activityLog.paging.endDate, + recentlyUsedDateRanges: state.endpointDetails.activityLog.paging.recentlyUsedDateRanges, + }, + }, hostDetails: { ...state.endpointDetails.hostDetails, detailsError: undefined, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts index 82057af233e4..dd0bc79f1ba5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { EuiSuperDatePickerRecentRange } from '@elastic/eui'; import { ActivityLog, HostInfo, @@ -41,9 +42,14 @@ export interface EndpointState { disabled?: boolean; page: number; pageSize: number; - startDate?: string; - endDate?: string; + startDate: string; + endDate: string; isInvalidDateRange: boolean; + autoRefreshOptions: { + enabled: boolean; + duration: number; + }; + recentlyUsedDateRanges: EuiSuperDatePickerRecentRange[]; }; logData: AsyncResourceState; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.test.ts index fa2aaaa16ae3..ee723bd0bf0f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.test.ts @@ -10,12 +10,14 @@ import { getIsInvalidDateRange } from './utils'; describe('utils', () => { describe('getIsInvalidDateRange', () => { - it('should return FALSE when either dates are undefined', () => { - expect(getIsInvalidDateRange({})).toBe(false); - expect(getIsInvalidDateRange({ startDate: moment().subtract(1, 'd').toISOString() })).toBe( - false - ); - expect(getIsInvalidDateRange({ endDate: moment().toISOString() })).toBe(false); + it('should return FALSE when startDate is before endDate', () => { + expect(getIsInvalidDateRange({ startDate: 'now-1d', endDate: 'now' })).toBe(false); + expect( + getIsInvalidDateRange({ + startDate: moment().subtract(1, 'd').toISOString(), + endDate: moment().toISOString(), + }) + ).toBe(false); }); it('should return TRUE when startDate is after endDate', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.ts index e2d619743c83..1bfb99c68ef6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.ts @@ -5,6 +5,7 @@ * 2.0. */ +import dateMath from '@elastic/datemath'; import moment from 'moment'; import { HostInfo, HostMetadata } from '../../../../common/endpoint/types'; @@ -29,12 +30,12 @@ export const getIsInvalidDateRange = ({ startDate, endDate, }: { - startDate?: string; - endDate?: string; + startDate: string; + endDate: string; }) => { - if (startDate && endDate) { - const start = moment(startDate); - const end = moment(endDate); + const start = moment(dateMath.parse(startDate)); + const end = moment(dateMath.parse(endDate)); + if (start.isValid() && end.isValid()) { return start.isAfter(end); } return false; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx index e92107853930..30ab082559c7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx @@ -8,95 +8,140 @@ import { useDispatch } from 'react-redux'; import React, { memo, useCallback } from 'react'; import styled from 'styled-components'; -import moment from 'moment'; -import { EuiFlexGroup, EuiFlexItem, EuiDatePicker, EuiDatePickerRange } from '@elastic/eui'; +import dateMath from '@elastic/datemath'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSuperDatePicker, + EuiSuperDatePickerRecentRange, +} from '@elastic/eui'; -import * as i18 from '../../../translations'; import { useEndpointSelector } from '../../../hooks'; -import { getActivityLogDataPaging } from '../../../../store/selectors'; +import { + getActivityLogDataPaging, + getActivityLogRequestLoading, +} from '../../../../store/selectors'; +import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../../../../../../common/constants'; +import { useUiSetting$ } from '../../../../../../../common/lib/kibana'; + +interface Range { + from: string; + to: string; + display: string; +} const DatePickerWrapper = styled.div` width: ${(props) => props.theme.eui.fractions.single.percentage}; - background: white; + max-width: 350px; `; const StickyFlexItem = styled(EuiFlexItem)` - max-width: 350px; + background: ${(props) => `${props.theme.eui.euiHeaderBackgroundColor}`}; position: sticky; - top: ${(props) => props.theme.eui.euiSizeM}; + top: 0; z-index: 1; - padding: ${(props) => `0 ${props.theme.eui.paddingSizes.m}`}; + padding: ${(props) => `${props.theme.eui.paddingSizes.m}`}; `; export const DateRangePicker = memo(() => { const dispatch = useDispatch(); - const { page, pageSize, startDate, endDate, isInvalidDateRange } = useEndpointSelector( - getActivityLogDataPaging - ); + const { + page, + pageSize, + startDate, + endDate, + autoRefreshOptions, + recentlyUsedDateRanges, + } = useEndpointSelector(getActivityLogDataPaging); + + const activityLogLoading = useEndpointSelector(getActivityLogRequestLoading); - const onChangeStartDate = useCallback( - (date) => { + const dispatchActionUpdateActivityLogPaging = useCallback( + async ({ start, end }) => { dispatch({ type: 'endpointDetailsActivityLogUpdatePaging', payload: { disabled: false, page, pageSize, - startDate: date ? date?.toISOString() : undefined, - endDate: endDate ? endDate : undefined, + startDate: dateMath.parse(start)?.toISOString(), + endDate: dateMath.parse(end)?.toISOString(), }, }); }, - [dispatch, endDate, page, pageSize] + [dispatch, page, pageSize] ); - const onChangeEndDate = useCallback( - (date) => { + const onRefreshChange = useCallback( + (evt) => { dispatch({ - type: 'endpointDetailsActivityLogUpdatePaging', + type: 'userUpdatedActivityLogRefreshOptions', payload: { - disabled: false, - page, - pageSize, - startDate: startDate ? startDate : undefined, - endDate: date ? date.toISOString() : undefined, + autoRefreshOptions: { enabled: !evt.isPaused, duration: evt.refreshInterval }, }, }); }, - [dispatch, startDate, page, pageSize] + [dispatch] ); + const onRefresh = useCallback(() => { + dispatch({ + type: 'endpointDetailsActivityLogUpdatePaging', + payload: { + disabled: false, + page, + pageSize, + startDate, + endDate, + }, + }); + }, [dispatch, page, pageSize, startDate, endDate]); + + const onTimeChange = useCallback( + ({ start: newStart, end: newEnd }) => { + const newRecentlyUsedDateRanges = [ + { start: newStart, end: newEnd }, + ...recentlyUsedDateRanges + .filter( + (recentlyUsedRange) => + !(recentlyUsedRange.start === newStart && recentlyUsedRange.end === newEnd) + ) + .slice(0, 9), + ]; + dispatch({ + type: 'userUpdatedActivityLogRecentlyUsedDateRanges', + payload: newRecentlyUsedDateRanges, + }); + + dispatchActionUpdateActivityLogPaging({ start: newStart, end: newEnd }); + }, + [dispatch, recentlyUsedDateRanges, dispatchActionUpdateActivityLogPaging] + ); + + const [quickRanges] = useUiSetting$(DEFAULT_TIMEPICKER_QUICK_RANGES); + const commonlyUsedRanges = !quickRanges.length + ? [] + : quickRanges.map(({ from, to, display }) => ({ + start: from, + end: to, + label: display, + })); + return ( - - + + - - } - endDateControl={ - - } + diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx index 5172b59450e0..f0b6b5fbc896 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx @@ -9,11 +9,13 @@ import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react'; import styled from 'styled-components'; import { + EuiCallOut, EuiText, EuiFlexGroup, EuiFlexItem, EuiLoadingContent, EuiEmptyPrompt, + EuiSpacer, } from '@elastic/eui'; import { useDispatch } from 'react-redux'; import { LogEntry } from './components/log_entry'; @@ -114,6 +116,17 @@ export const EndpointActivityLog = memo( <> + {!isPagingDisabled && activityLogLoaded && !activityLogData.length && ( + <> + + + + )} {activityLogLoaded && activityLogData.map((logEntry) => ( diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx index 372bd4491d7d..123a51e5a52b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx @@ -22,6 +22,8 @@ export const dummyEndpointActivityLog = ( data: { page: 1, pageSize: 50, + startDate: moment().subtract(5, 'day').fromNow().toString(), + endDate: moment().toString(), data: [ { type: 'action', diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 996198568ad2..ea999334ee77 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -10,6 +10,8 @@ import * as reactTestingLibrary from '@testing-library/react'; import { EndpointList } from './index'; import '../../../../common/mock/match_media'; +import { createUseUiSetting$Mock } from '../../../../../public/common/lib/kibana/kibana_react.mock'; + import { mockEndpointDetailsApiResult, mockEndpointResultList, @@ -28,7 +30,7 @@ import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_da import { POLICY_STATUS_TO_HEALTH_COLOR, POLICY_STATUS_TO_TEXT } from './host_constants'; import { mockPolicyResultList } from '../../policy/store/test_mock_utils'; import { getEndpointDetailsPath } from '../../../common/routing'; -import { KibanaServices, useKibana, useToasts } from '../../../../common/lib/kibana'; +import { KibanaServices, useKibana, useToasts, useUiSetting$ } from '../../../../common/lib/kibana'; import { hostIsolationHttpMocks } from '../../../../common/lib/endpoint_isolation/mocks'; import { createFailedResourceState, @@ -40,7 +42,11 @@ import { import { getCurrentIsolationRequestState } from '../store/selectors'; import { licenseService } from '../../../../common/hooks/use_license'; import { FleetActionGenerator } from '../../../../../common/endpoint/data_generators/fleet_action_generator'; -import { APP_PATH, MANAGEMENT_PATH } from '../../../../../common/constants'; +import { + APP_PATH, + MANAGEMENT_PATH, + DEFAULT_TIMEPICKER_QUICK_RANGES, +} from '../../../../../common/constants'; import { TransformStats, TRANSFORM_STATE } from '../types'; import { metadataTransformPrefix } from '../../../../../common/endpoint/constants'; @@ -63,6 +69,59 @@ jest.mock('../../policy/store/services/ingest', () => { sendGetEndpointSecurityPackage: () => Promise.resolve({}), }; }); +const mockUseUiSetting$ = useUiSetting$ as jest.Mock; +const timepickerRanges = [ + { + from: 'now/d', + to: 'now/d', + display: 'Today', + }, + { + from: 'now/w', + to: 'now/w', + display: 'This week', + }, + { + from: 'now-15m', + to: 'now', + display: 'Last 15 minutes', + }, + { + from: 'now-30m', + to: 'now', + display: 'Last 30 minutes', + }, + { + from: 'now-1h', + to: 'now', + display: 'Last 1 hour', + }, + { + from: 'now-24h', + to: 'now', + display: 'Last 24 hours', + }, + { + from: 'now-7d', + to: 'now', + display: 'Last 7 days', + }, + { + from: 'now-30d', + to: 'now', + display: 'Last 30 days', + }, + { + from: 'now-90d', + to: 'now', + display: 'Last 90 days', + }, + { + from: 'now-1y', + to: 'now', + display: 'Last 1 year', + }, +]; jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../common/hooks/use_license'); @@ -759,6 +818,14 @@ describe('when on the endpoint list page', () => { disconnect: jest.fn(), })); + mockUseUiSetting$.mockImplementation((key, defaultValue) => { + const useUiSetting$Mock = createUseUiSetting$Mock(); + + return key === DEFAULT_TIMEPICKER_QUICK_RANGES + ? [timepickerRanges, jest.fn()] + : useUiSetting$Mock(key, defaultValue); + }); + const fleetActionGenerator = new FleetActionGenerator('seed'); const responseData = fleetActionGenerator.generateResponse({ agent_id: agentId, @@ -766,9 +833,12 @@ describe('when on the endpoint list page', () => { const actionData = fleetActionGenerator.generate({ agents: [agentId], }); + getMockData = () => ({ page: 1, pageSize: 50, + startDate: 'now-1d', + endDate: 'now', data: [ { type: 'response', @@ -838,7 +908,7 @@ describe('when on the endpoint list page', () => { expect(emptyState).not.toBe(null); }); - it('should display empty state when no log data', async () => { + it('should not display empty state when no log data', async () => { const activityLogTab = await renderResult.findByTestId('activity_log'); reactTestingLibrary.act(() => { reactTestingLibrary.fireEvent.click(activityLogTab); @@ -848,36 +918,39 @@ describe('when on the endpoint list page', () => { dispatchEndpointDetailsActivityLogChanged('success', { page: 1, pageSize: 50, + startDate: 'now-1d', + endDate: 'now', data: [], }); }); const emptyState = await renderResult.queryByTestId('activityLogEmpty'); - expect(emptyState).not.toBe(null); + expect(emptyState).toBe(null); + + const superDatePicker = await renderResult.queryByTestId('activityLogSuperDatePicker'); + expect(superDatePicker).not.toBe(null); }); - it('should not display empty state with no log data while date range filter is active', async () => { - const activityLogTab = await renderResult.findByTestId('activity_log'); + it('should display activity log when tab is loaded using the URL', async () => { + const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); reactTestingLibrary.act(() => { - reactTestingLibrary.fireEvent.click(activityLogTab); + history.push( + `${MANAGEMENT_PATH}/endpoints?page_index=0&page_size=10&selected_endpoint=1&show=activity_log` + ); }); + const changedUrlAction = await userChangedUrlChecker; + expect(changedUrlAction.payload.search).toEqual( + '?page_index=0&page_size=10&selected_endpoint=1&show=activity_log' + ); await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged'); reactTestingLibrary.act(() => { - dispatchEndpointDetailsActivityLogChanged('success', { - page: 1, - pageSize: 50, - startDate: new Date().toISOString(), - data: [], - }); + dispatchEndpointDetailsActivityLogChanged('success', getMockData()); }); - - const emptyState = await renderResult.queryByTestId('activityLogEmpty'); - const dateRangePicker = await renderResult.queryByTestId('activityLogDateRangePicker'); - expect(emptyState).toBe(null); - expect(dateRangePicker).not.toBe(null); + const logEntries = await renderResult.queryAllByTestId('timelineEntry'); + expect(logEntries.length).toEqual(2); }); - it('should display activity log when tab is loaded using the URL', async () => { + it('should display a callout message if no log data', async () => { const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); reactTestingLibrary.act(() => { history.push( @@ -890,10 +963,17 @@ describe('when on the endpoint list page', () => { ); await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged'); reactTestingLibrary.act(() => { - dispatchEndpointDetailsActivityLogChanged('success', getMockData()); + dispatchEndpointDetailsActivityLogChanged('success', { + page: 1, + pageSize: 50, + startDate: 'now-1d', + endDate: 'now', + data: [], + }); }); - const logEntries = await renderResult.queryAllByTestId('timelineEntry'); - expect(logEntries.length).toEqual(2); + + const activityLogCallout = await renderResult.findByTestId('activityLogNoDataCallout'); + expect(activityLogCallout).not.toBeNull(); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts index 57ad3e4808bd..c8a29eed3fda 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts @@ -15,20 +15,6 @@ export const ACTIVITY_LOG = { tabTitle: i18n.translate('xpack.securitySolution.endpointDetails.activityLog', { defaultMessage: 'Activity Log', }), - datePicker: { - startDate: i18n.translate( - 'xpack.securitySolution.endpointDetails.activityLog.datePicker.startDate', - { - defaultMessage: 'Pick a start date', - } - ), - endDate: i18n.translate( - 'xpack.securitySolution.endpointDetails.activityLog.datePicker.endDate', - { - defaultMessage: 'Pick an end date', - } - ), - }, LogEntry: { endOfLog: i18n.translate( 'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.endOfLog', @@ -36,6 +22,13 @@ export const ACTIVITY_LOG = { defaultMessage: 'Nothing more to show', } ), + dateRangeMessage: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.dateRangeMessage.title', + { + defaultMessage: + 'Nothing to show for selected date range, please select another and try again.', + } + ), emptyState: { title: i18n.translate( 'xpack.securitySolution.endpointDetails.activityLog.logEntry.emptyState.title', diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts index 83f38bc90457..4bd63c83169e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts @@ -48,19 +48,13 @@ describe('Action Log API', () => { }).not.toThrow(); }); - it('should work without query params', () => { + it('should not work when no params while requesting with query params', () => { expect(() => { EndpointActionLogRequestSchema.query.validate({}); - }).not.toThrow(); - }); - - it('should work with query params', () => { - expect(() => { - EndpointActionLogRequestSchema.query.validate({ page: 10, page_size: 100 }); - }).not.toThrow(); + }).toThrow(); }); - it('should work with all query params', () => { + it('should work with all required query params', () => { expect(() => { EndpointActionLogRequestSchema.query.validate({ page: 10, @@ -71,24 +65,24 @@ describe('Action Log API', () => { }).not.toThrow(); }); - it('should work with just startDate', () => { + it('should not work without endDate', () => { expect(() => { EndpointActionLogRequestSchema.query.validate({ page: 1, page_size: 100, start_date: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday }); - }).not.toThrow(); + }).toThrow(); }); - it('should work with just endDate', () => { + it('should not work without startDate', () => { expect(() => { EndpointActionLogRequestSchema.query.validate({ page: 1, page_size: 100, end_date: new Date().toISOString(), // today }); - }).not.toThrow(); + }).toThrow(); }); it('should not work without allowed page and page_size params', () => { diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions.ts index 80fb1c5d9c7b..a04a6eea5ab6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions.ts @@ -31,8 +31,8 @@ export const getAuditLogResponse = async ({ elasticAgentId: string; page: number; pageSize: number; - startDate?: string; - endDate?: string; + startDate: string; + endDate: string; context: SecuritySolutionRequestHandlerContext; logger: Logger; }): Promise => { @@ -71,8 +71,8 @@ const getActivityLog = async ({ elasticAgentId: string; size: number; from: number; - startDate?: string; - endDate?: string; + startDate: string; + endDate: string; logger: Logger; }) => { const options = { @@ -84,13 +84,10 @@ const getActivityLog = async ({ let actionsResult; let responsesResult; - const dateFilters = []; - if (startDate) { - dateFilters.push({ range: { '@timestamp': { gte: startDate } } }); - } - if (endDate) { - dateFilters.push({ range: { '@timestamp': { lte: endDate } } }); - } + const dateFilters = [ + { range: { '@timestamp': { gte: startDate } } }, + { range: { '@timestamp': { lte: endDate } } }, + ]; try { // fetch actions with matching agent_id From b4f5877ff8e1ac5a01053a5615526a9bdc202244 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Fri, 3 Sep 2021 14:38:19 +0200 Subject: [PATCH 09/47] catch errors from providers (#111093) --- .../public/services/search_service.test.ts | 64 +++++++++++++++++++ .../public/services/search_service.ts | 8 +-- .../server/services/search_service.test.ts | 38 +++++++++++ .../server/services/search_service.ts | 5 +- 4 files changed, 109 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/global_search/public/services/search_service.test.ts b/x-pack/plugins/global_search/public/services/search_service.test.ts index 4b3c06f03dcc..b0e6a7229043 100644 --- a/x-pack/plugins/global_search/public/services/search_service.test.ts +++ b/x-pack/plugins/global_search/public/services/search_service.test.ts @@ -272,6 +272,43 @@ describe('SearchService', () => { }); }); + it('catches errors from providers', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + }); + + getTestScheduler().run(({ expectObservable, hot }) => { + registerResultProvider( + createProvider('A', { + source: hot('a---c-|', { + a: [providerResult('A1'), providerResult('A2')], + c: [providerResult('A3')], + }), + }) + ); + registerResultProvider( + createProvider('B', { + source: hot( + '-b-# ', + { + b: [providerResult('B1')], + }, + new Error('something went bad') + ), + }) + ); + + const { find } = service.start(startDeps()); + const results = find({ term: 'foobar' }, {}); + + expectObservable(results).toBe('ab--c-|', { + a: expectedBatch('A1', 'A2'), + b: expectedBatch('B1'), + c: expectedBatch('A3'), + }); + }); + }); + it('return mixed server/client providers results', async () => { const { registerResultProvider } = service.setup({ config: createConfig(), @@ -304,6 +341,33 @@ describe('SearchService', () => { }); }); + it('catches errors from the server', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + }); + + getTestScheduler().run(({ expectObservable, hot }) => { + fetchServerResultsMock.mockReturnValue(hot('#', {}, new Error('fetch error'))); + + registerResultProvider( + createProvider('A', { + source: hot('a-b-|', { + a: [providerResult('P1')], + b: [providerResult('P2')], + }), + }) + ); + + const { find } = service.start(startDeps()); + const results = find({ term: 'foobar' }, {}); + + expectObservable(results).toBe('a-b-|', { + a: expectedBatch('P1'), + b: expectedBatch('P2'), + }); + }); + }); + it('handles the `aborted$` option', async () => { const { registerResultProvider } = service.setup({ config: createConfig(), diff --git a/x-pack/plugins/global_search/public/services/search_service.ts b/x-pack/plugins/global_search/public/services/search_service.ts index bf06aa04061e..85f4d4143a60 100644 --- a/x-pack/plugins/global_search/public/services/search_service.ts +++ b/x-pack/plugins/global_search/public/services/search_service.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { merge, Observable, timer, throwError } from 'rxjs'; -import { map, takeUntil } from 'rxjs/operators'; +import { merge, Observable, timer, throwError, EMPTY } from 'rxjs'; +import { map, takeUntil, catchError } from 'rxjs/operators'; import { uniq } from 'lodash'; import { duration } from 'moment'; import { i18n } from '@kbn/i18n'; @@ -177,16 +177,16 @@ export class SearchService { const serverResults$ = fetchServerResults(this.http!, params, { preference, aborted$, - }); + }).pipe(catchError(() => EMPTY)); const providersResults$ = [...this.providers.values()].map((provider) => provider.find(params, providerOptions).pipe( + catchError(() => EMPTY), takeInArray(this.maxProviderResults), takeUntil(aborted$), map((results) => results.map((r) => processResult(r))) ) ); - return merge(...providersResults$, serverResults$).pipe( map((results) => ({ results, diff --git a/x-pack/plugins/global_search/server/services/search_service.test.ts b/x-pack/plugins/global_search/server/services/search_service.test.ts index 246fbd675aba..45824fde26af 100644 --- a/x-pack/plugins/global_search/server/services/search_service.test.ts +++ b/x-pack/plugins/global_search/server/services/search_service.test.ts @@ -178,6 +178,44 @@ describe('SearchService', () => { }); }); + it('catches errors from providers', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + basePath, + }); + + getTestScheduler().run(({ expectObservable, hot }) => { + registerResultProvider( + createProvider('A', { + source: hot('a---c-|', { + a: [result('A1'), result('A2')], + c: [result('A3')], + }), + }) + ); + registerResultProvider( + createProvider('B', { + source: hot( + '-b-# ', + { + b: [result('B1')], + }, + new Error('something went bad') + ), + }) + ); + + const { find } = service.start({ core: coreStart, licenseChecker }); + const results = find({ term: 'foobar' }, {}, request); + + expectObservable(results).toBe('ab--c-|', { + a: expectedBatch('A1', 'A2'), + b: expectedBatch('B1'), + c: expectedBatch('A3'), + }); + }); + }); + it('handles the `aborted$` option', async () => { const { registerResultProvider } = service.setup({ config: createConfig(), diff --git a/x-pack/plugins/global_search/server/services/search_service.ts b/x-pack/plugins/global_search/server/services/search_service.ts index a6c2a7ee234d..22bac036544a 100644 --- a/x-pack/plugins/global_search/server/services/search_service.ts +++ b/x-pack/plugins/global_search/server/services/search_service.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { Observable, timer, merge, throwError } from 'rxjs'; -import { map, takeUntil } from 'rxjs/operators'; +import { Observable, timer, merge, throwError, EMPTY } from 'rxjs'; +import { map, takeUntil, catchError } from 'rxjs/operators'; import { uniq } from 'lodash'; import { i18n } from '@kbn/i18n'; import { KibanaRequest, CoreStart, IBasePath } from 'src/core/server'; @@ -174,6 +174,7 @@ export class SearchService { const providersResults$ = [...this.providers.values()].map((provider) => provider.find(params, findOptions, context).pipe( + catchError(() => EMPTY), takeInArray(this.maxProviderResults), takeUntil(aborted$), map((results) => results.map((r) => processResult(r))) From e2ee2637e2e8d18455bebc758333401a9a88f197 Mon Sep 17 00:00:00 2001 From: mgiota Date: Fri, 3 Sep 2021 14:39:52 +0200 Subject: [PATCH 10/47] Update alert documents when the write index changes (#110788) * first draft(work in progress) * add back missing await * disable require_alias flag only when we update * cleanup --- .../server/utils/create_lifecycle_executor.ts | 109 ++++++++++-------- 1 file changed, 63 insertions(+), 46 deletions(-) diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts index 259a9e9e8de3..48f3a81a00af 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts @@ -162,7 +162,6 @@ export const createLifecycleExecutor = ( > = { alertWithLifecycle: ({ id, fields }) => { currentAlerts[id] = fields; - return alertInstanceFactory(id); }, }; @@ -179,7 +178,6 @@ export const createLifecycleExecutor = ( const currentAlertIds = Object.keys(currentAlerts); const trackedAlertIds = Object.keys(state.trackedAlerts); const newAlertIds = currentAlertIds.filter((alertId) => !trackedAlertIds.includes(alertId)); - const allAlertIds = [...new Set(currentAlertIds.concat(trackedAlertIds))]; const trackedAlertStates = Object.values(state.trackedAlerts); @@ -188,9 +186,10 @@ export const createLifecycleExecutor = ( `Tracking ${allAlertIds.length} alerts (${newAlertIds.length} new, ${trackedAlertStates.length} previous)` ); - const alertsDataMap: Record> = { - ...currentAlerts, - }; + const trackedAlertsDataMap: Record< + string, + { indexName: string; fields: Partial } + > = {}; if (trackedAlertStates.length) { const { hits } = await ruleDataClient.getReader().search({ @@ -228,59 +227,77 @@ export const createLifecycleExecutor = ( hits.hits.forEach((hit) => { const fields = parseTechnicalFields(hit.fields); + const indexName = hit._index; const alertId = fields[ALERT_INSTANCE_ID]; - alertsDataMap[alertId] = { - ...commonRuleFields, - ...fields, + trackedAlertsDataMap[alertId] = { + indexName, + fields, }; }); } - const eventsToIndex = allAlertIds.map((alertId) => { - const alertData = alertsDataMap[alertId]; - - if (!alertData) { - logger.warn(`Could not find alert data for ${alertId}`); - } - - const isNew = !state.trackedAlerts[alertId]; - const isRecovered = !currentAlerts[alertId]; - const isActive = !isRecovered; - - const { alertUuid, started } = state.trackedAlerts[alertId] ?? { - alertUuid: v4(), - started: commonRuleFields[TIMESTAMP], - }; - const event: ParsedTechnicalFields = { - ...alertData, - ...commonRuleFields, - [ALERT_DURATION]: (options.startedAt.getTime() - new Date(started).getTime()) * 1000, - [ALERT_INSTANCE_ID]: alertId, - [ALERT_START]: started, - [ALERT_STATUS]: isActive ? ALERT_STATUS_ACTIVE : ALERT_STATUS_RECOVERED, - [ALERT_WORKFLOW_STATUS]: alertData[ALERT_WORKFLOW_STATUS] ?? 'open', - [ALERT_UUID]: alertUuid, - [EVENT_KIND]: 'signal', - [EVENT_ACTION]: isNew ? 'open' : isActive ? 'active' : 'close', - [VERSION]: ruleDataClient.kibanaVersion, - ...(isRecovered ? { [ALERT_END]: commonRuleFields[TIMESTAMP] } : {}), - }; - - return event; - }); + const makeEventsDataMapFor = (alertIds: string[]) => + alertIds.map((alertId) => { + const alertData = trackedAlertsDataMap[alertId]; + const currentAlertData = currentAlerts[alertId]; + + if (!alertData) { + logger.warn(`Could not find alert data for ${alertId}`); + } + + const isNew = !state.trackedAlerts[alertId]; + const isRecovered = !currentAlerts[alertId]; + const isActive = !isRecovered; + + const { alertUuid, started } = state.trackedAlerts[alertId] ?? { + alertUuid: v4(), + started: commonRuleFields[TIMESTAMP], + }; + + const event: ParsedTechnicalFields = { + ...alertData?.fields, + ...commonRuleFields, + ...currentAlertData, + [ALERT_DURATION]: (options.startedAt.getTime() - new Date(started).getTime()) * 1000, + + [ALERT_INSTANCE_ID]: alertId, + [ALERT_START]: started, + [ALERT_UUID]: alertUuid, + [ALERT_STATUS]: isRecovered ? ALERT_STATUS_RECOVERED : ALERT_STATUS_ACTIVE, + [ALERT_WORKFLOW_STATUS]: alertData?.fields[ALERT_WORKFLOW_STATUS] ?? 'open', + [EVENT_KIND]: 'signal', + [EVENT_ACTION]: isNew ? 'open' : isActive ? 'active' : 'close', + [VERSION]: ruleDataClient.kibanaVersion, + ...(isRecovered ? { [ALERT_END]: commonRuleFields[TIMESTAMP] } : {}), + }; + + return { + indexName: alertData?.indexName, + event, + }; + }); + + const trackedEventsToIndex = makeEventsDataMapFor(trackedAlertIds); + const newEventsToIndex = makeEventsDataMapFor(newAlertIds); + const allEventsToIndex = [...trackedEventsToIndex, ...newEventsToIndex]; - if (eventsToIndex.length > 0 && ruleDataClient.isWriteEnabled()) { - logger.debug(`Preparing to index ${eventsToIndex.length} alerts.`); + if (allEventsToIndex.length > 0 && ruleDataClient.isWriteEnabled()) { + logger.debug(`Preparing to index ${allEventsToIndex.length} alerts.`); await ruleDataClient.getWriter().bulk({ - body: eventsToIndex.flatMap((event) => [{ index: { _id: event[ALERT_UUID]! } }, event]), + body: allEventsToIndex.flatMap(({ event, indexName }) => [ + indexName + ? { index: { _id: event[ALERT_UUID]!, _index: indexName, require_alias: false } } + : { index: { _id: event[ALERT_UUID]! } }, + event, + ]), }); } const nextTrackedAlerts = Object.fromEntries( - eventsToIndex - .filter((event) => event[ALERT_STATUS] !== 'closed') - .map((event) => { + allEventsToIndex + .filter(({ event }) => event[ALERT_STATUS] !== 'closed') + .map(({ event }) => { const alertId = event[ALERT_INSTANCE_ID]!; const alertUuid = event[ALERT_UUID]!; const started = new Date(event[ALERT_START]!).toISOString(); From d83c8244a296a1397c87ccdbd63db66784bceeca Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Fri, 3 Sep 2021 08:47:26 -0400 Subject: [PATCH 11/47] [Uptime] [Synthetics Integration] fix content typo (#110088) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/components/fleet_package/browser/simple_fields.tsx | 2 +- .../public/components/fleet_package/http/simple_fields.tsx | 2 +- .../public/components/fleet_package/icmp/simple_fields.tsx | 2 +- .../public/components/fleet_package/tcp/simple_fields.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx index 34f56a65df3e..0e2f10b96fe6 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx @@ -168,7 +168,7 @@ export const BrowserSimpleFields = memo(({ validate }) => { helpText={ } > diff --git a/x-pack/plugins/uptime/public/components/fleet_package/http/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/http/simple_fields.tsx index 8eb81eb92f7b..c4de1d53fe99 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/http/simple_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/http/simple_fields.tsx @@ -186,7 +186,7 @@ export const HTTPSimpleFields = memo(({ validate }) => { helpText={ } > diff --git a/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx index 420f218429e4..92afe4c5072e 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx @@ -190,7 +190,7 @@ export const ICMPSimpleFields = memo(({ validate }) => { helpText={ } > diff --git a/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx index 8bc017a51cfa..37f0c82595e0 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx @@ -157,7 +157,7 @@ export const TCPSimpleFields = memo(({ validate }) => { helpText={ } > From 71571c5b60df320de9f36b2322c27bb96af41357 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Fri, 3 Sep 2021 14:05:53 +0100 Subject: [PATCH 12/47] [ML] Job import and export functional tests (#110578) * [ML] Job import export functional tests * adding title check * adding dfa tests * removing export file * adds bad data test * commented code * adding export job tests * adds version to file names * improving tests * removing comment Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../export_jobs_flyout/export_jobs_flyout.tsx | 140 +++++--- .../cannot_import_jobs_callout.tsx | 1 + .../cannot_read_file_callout.tsx | 10 +- .../import_jobs_flyout/import_jobs_flyout.tsx | 43 ++- .../ml/stack_management_jobs/export_jobs.ts | 314 ++++++++++++++++++ .../anomaly_detection_jobs_7.16.json | 213 ++++++++++++ .../files_to_import/bad_data.json | 1 + .../data_frame_analytics_jobs_7.16.json | 60 ++++ .../ml/stack_management_jobs/import_jobs.ts | 107 ++++++ .../apps/ml/stack_management_jobs/index.ts | 2 + .../services/ml/stack_management_jobs.ts | 224 ++++++++++++- 11 files changed, 1043 insertions(+), 72 deletions(-) create mode 100644 x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts create mode 100644 x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs_7.16.json create mode 100644 x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/bad_data.json create mode 100644 x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/data_frame_analytics_jobs_7.16.json create mode 100644 x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx index bd4b805baa18..509c74c35965 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx @@ -63,6 +63,7 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { const [exporting, setExporting] = useState(false); const [selectedJobType, setSelectedJobType] = useState(currentTab); const [switchTabConfirmVisible, setSwitchTabConfirmVisible] = useState(false); + const [switchTabNextTab, setSwitchTabNextTab] = useState(currentTab); const { displayErrorToast, displaySuccessToast } = useMemo( () => toastNotificationServiceProvider(toasts), [toasts] @@ -170,16 +171,23 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { } } - const attemptTabSwitch = useCallback(() => { - // if the user has already selected some jobs, open a confirm modal - // rather than changing tabs - if (selectedJobIds.length > 0) { - setSwitchTabConfirmVisible(true); - return; - } + const attemptTabSwitch = useCallback( + (jobType: JobType) => { + if (jobType === selectedJobType) { + return; + } + // if the user has already selected some jobs, open a confirm modal + // rather than changing tabs + if (selectedJobIds.length > 0) { + setSwitchTabNextTab(jobType); + setSwitchTabConfirmVisible(true); + return; + } - switchTab(); - }, [selectedJobIds]); + switchTab(jobType); + }, + [selectedJobIds] + ); useEffect(() => { setSelectedJobDependencies( @@ -187,10 +195,7 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { ); }, [selectedJobIds]); - function switchTab() { - const jobType = - selectedJobType === 'anomaly-detector' ? 'data-frame-analytics' : 'anomaly-detector'; - + function switchTab(jobType: JobType) { setSwitchTabConfirmVisible(false); setSelectedJobIds([]); setSelectedJobType(jobType); @@ -211,7 +216,12 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { {showFlyout === true && isDisabled === false && ( <> - setShowFlyout(false)} hideCloseButton size="s"> + setShowFlyout(false)} + hideCloseButton + size="s" + data-test-subj="mlJobMgmtExportJobsFlyout" + >

@@ -227,8 +237,9 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { attemptTabSwitch('anomaly-detector')} disabled={exporting} + data-test-subj="mlJobMgmtExportJobsADTab" > = ({ isDisabled, currentTab }) => { attemptTabSwitch('data-frame-analytics')} disabled={exporting} + data-test-subj="mlJobMgmtExportJobsDFATab" > = ({ isDisabled, currentTab }) => { ) : ( <> - - + + {selectedJobIds.length === adJobIds.length ? ( + + ) : ( + + )} - {adJobIds.map((id) => ( -
- toggleSelectedJob(e.target.checked, id)} - /> - -
- ))} +
+ {adJobIds.map((id) => ( +
+ toggleSelectedJob(e.target.checked, id)} + /> + +
+ ))} +
)} @@ -284,26 +310,39 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { ) : ( <> - - + + {selectedJobIds.length === dfaJobIds.length ? ( + + ) : ( + + )} - - {dfaJobIds.map((id) => ( -
- toggleSelectedJob(e.target.checked, id)} - /> - -
- ))} +
+ {dfaJobIds.map((id) => ( +
+ toggleSelectedJob(e.target.checked, id)} + /> + +
+ ))} +
)} @@ -329,6 +368,7 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { disabled={selectedJobIds.length === 0 || exporting === true} onClick={onExport} fill + data-test-subj="mlJobMgmtExportExportButton" > = ({ isDisabled, currentTab }) => { {switchTabConfirmVisible === true ? ( switchTab(switchTabNextTab)} /> ) : null} diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx index 732be345a1ee..565ded9c6f6c 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx @@ -30,6 +30,7 @@ export const CannotImportJobsCallout: FC = ({ jobs, autoExpand = false }) values: { num: jobs.length }, })} color="warning" + data-test-subj="mlJobMgmtImportJobsCannotBeImportedCallout" > {autoExpand ? ( diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_read_file_callout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_read_file_callout.tsx index 4c7a2471db9d..70f94d1e0315 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_read_file_callout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_read_file_callout.tsx @@ -21,10 +21,12 @@ export const CannotReadFileCallout: FC = () => { })} color="warning" > - +
+ +
); diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx index 68db42cdbf0e..dfe07b1984e1 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx @@ -341,7 +341,12 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => { {showFlyout === true && isDisabled === false && ( - +

@@ -373,22 +378,26 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => { {showFileReadError ? : null} {totalJobsRead > 0 && jobType !== null && ( - <> +
{jobType === 'anomaly-detector' && ( - +
+ +
)} {jobType === 'data-frame-analytics' && ( - +
+ +
)} @@ -426,6 +435,7 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => { value={jobId.jobId} onChange={(e) => renameJob(e.target.value, i)} isInvalid={jobId.jobIdValid === false} + data-test-subj="mlJobMgmtImportJobIdInput" /> @@ -465,7 +475,7 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => {
))} - + )} @@ -484,7 +494,12 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => { - + = [ + { + // @ts-expect-error not full interface + job: { + job_id: 'fq_single_1_smv', + groups: ['farequote', 'automated', 'single-metric'], + description: 'mean(responsetime) on farequote dataset with 15m bucket span', + analysis_config: { + bucket_span: '15m', + detectors: [ + { + detector_description: 'mean(responsetime)', + function: 'mean', + field_name: 'responsetime', + }, + ], + influencers: [], + }, + analysis_limits: { + model_memory_limit: '10mb', + categorization_examples_limit: 4, + }, + data_description: { + time_field: '@timestamp', + time_format: 'epoch_ms', + }, + model_plot_config: { + enabled: true, + annotations_enabled: true, + }, + model_snapshot_retention_days: 10, + daily_model_snapshot_retention_after_days: 1, + results_index_name: 'shared', + allow_lazy_open: false, + }, + datafeed: { + datafeed_id: 'datafeed-fq_single_1_smv', + job_id: 'fq_single_1_smv', + query: { + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }, + indices: ['ft_farequote'], + scroll_size: 1000, + delayed_data_check_config: { + enabled: true, + }, + }, + }, + { + // @ts-expect-error not full interface + job: { + job_id: 'fq_single_2_smv', + groups: ['farequote', 'automated', 'single-metric'], + description: 'low_mean(responsetime) on farequote dataset with 15m bucket span', + analysis_config: { + bucket_span: '15m', + detectors: [ + { + detector_description: 'low_mean(responsetime)', + function: 'low_mean', + field_name: 'responsetime', + }, + ], + influencers: ['responsetime'], + }, + analysis_limits: { + model_memory_limit: '11mb', + categorization_examples_limit: 4, + }, + data_description: { + time_field: '@timestamp', + time_format: 'epoch_ms', + }, + model_plot_config: { + enabled: true, + annotations_enabled: true, + }, + model_snapshot_retention_days: 10, + daily_model_snapshot_retention_after_days: 1, + results_index_name: 'shared', + allow_lazy_open: false, + }, + datafeed: { + datafeed_id: 'datafeed-fq_single_2_smv', + job_id: 'fq_single_2_smv', + query: { + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }, + indices: ['ft_farequote'], + scroll_size: 1000, + delayed_data_check_config: { + enabled: true, + }, + }, + }, + { + // @ts-expect-error not full interface + job: { + job_id: 'fq_single_3_smv', + groups: ['farequote', 'automated', 'single-metric'], + description: 'high_mean(responsetime) on farequote dataset with 15m bucket span', + analysis_config: { + bucket_span: '15m', + detectors: [ + { + detector_description: 'high_mean(responsetime)', + function: 'high_mean', + field_name: 'responsetime', + }, + ], + influencers: ['responsetime'], + }, + analysis_limits: { + model_memory_limit: '11mb', + categorization_examples_limit: 4, + }, + data_description: { + time_field: '@timestamp', + time_format: 'epoch_ms', + }, + model_plot_config: { + enabled: true, + annotations_enabled: true, + }, + model_snapshot_retention_days: 10, + daily_model_snapshot_retention_after_days: 1, + results_index_name: 'shared', + allow_lazy_open: false, + }, + datafeed: { + datafeed_id: 'datafeed-fq_single_3_smv', + job_id: 'fq_single_3_smv', + query: { + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }, + indices: ['ft_farequote'], + scroll_size: 1000, + delayed_data_check_config: { + enabled: true, + }, + }, + }, +]; + +const testDFAJobs: DataFrameAnalyticsConfig[] = [ + // @ts-expect-error not full interface + { + id: `bm_1_1`, + description: + "Classification job based on 'ft_bank_marketing' dataset with dependentVariable 'y' and trainingPercent '20'", + source: { + index: ['ft_bank_marketing'], + query: { + match_all: {}, + }, + }, + dest: { + index: 'user-bm_1_1', + results_field: 'ml', + }, + analysis: { + classification: { + prediction_field_name: 'test', + dependent_variable: 'y', + training_percent: 20, + }, + }, + analyzed_fields: { + includes: [], + excludes: [], + }, + model_memory_limit: '60mb', + allow_lazy_start: false, + }, + // @ts-expect-error not full interface + { + id: `ihp_1_2`, + description: 'This is the job description', + source: { + index: ['ft_ihp_outlier'], + query: { + match_all: {}, + }, + }, + dest: { + index: 'user-ihp_1_2', + results_field: 'ml', + }, + analysis: { + outlier_detection: {}, + }, + analyzed_fields: { + includes: [], + excludes: [], + }, + model_memory_limit: '5mb', + }, + // @ts-expect-error not full interface + { + id: `egs_1_3`, + description: 'This is the job description', + source: { + index: ['ft_egs_regression'], + query: { + match_all: {}, + }, + }, + dest: { + index: 'user-egs_1_3', + results_field: 'ml', + }, + analysis: { + regression: { + prediction_field_name: 'test', + dependent_variable: 'stab', + training_percent: 20, + }, + }, + analyzed_fields: { + includes: [], + excludes: [], + }, + model_memory_limit: '20mb', + }, +]; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('export jobs', function () { + this.tags(['mlqa']); + before(async () => { + await ml.api.cleanMlIndices(); + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/bm_classification'); + await ml.testResources.createIndexPatternIfNeeded('ft_bank_marketing', '@timestamp'); + + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ihp_outlier'); + await ml.testResources.createIndexPatternIfNeeded('ft_ihp_outlier', '@timestamp'); + + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/egs_regression'); + await ml.testResources.createIndexPatternIfNeeded('ft_egs_regression', '@timestamp'); + + await ml.testResources.setKibanaTimeZoneToUTC(); + + for (const { job, datafeed } of testADJobs) { + await ml.api.createAnomalyDetectionJob(job); + await ml.api.createDatafeed(datafeed); + } + for (const job of testDFAJobs) { + await ml.api.createDataFrameAnalyticsJob(job); + } + + await ml.securityUI.loginAsMlPowerUser(); + await ml.navigation.navigateToStackManagement(); + await ml.navigation.navigateToStackManagementJobsListPage(); + }); + after(async () => { + await ml.api.cleanMlIndices(); + ml.stackManagementJobs.deleteExportedFiles([ + 'anomaly_detection_jobs', + 'data_frame_analytics_jobs', + ]); + }); + + it('opens export flyout and exports anomaly detector jobs', async () => { + await ml.stackManagementJobs.openExportFlyout(); + await ml.stackManagementJobs.selectExportJobType('anomaly-detector'); + await ml.stackManagementJobs.selectExportJobSelectAll('anomaly-detector'); + await ml.stackManagementJobs.selectExportJobs(); + await ml.stackManagementJobs.assertExportedADJobsAreCorrect(testADJobs); + }); + + it('opens export flyout and exports data frame analytics jobs', async () => { + await ml.stackManagementJobs.openExportFlyout(); + await ml.stackManagementJobs.selectExportJobType('data-frame-analytics'); + await ml.stackManagementJobs.selectExportJobSelectAll('data-frame-analytics'); + await ml.stackManagementJobs.selectExportJobs(); + await ml.stackManagementJobs.assertExportedDFAJobsAreCorrect(testDFAJobs); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs_7.16.json b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs_7.16.json new file mode 100644 index 000000000000..1bc51d433858 --- /dev/null +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs_7.16.json @@ -0,0 +1,213 @@ +[ + { + "job": { + "job_id": "ad-test1", + "description": "", + "analysis_config": { + "bucket_span": "15m", + "summary_count_field_name": "doc_count", + "detectors": [ + { + "detector_description": "mean(responsetime)", + "function": "mean", + "field_name": "responsetime", + "detector_index": 0 + } + ], + "influencers": [] + }, + "analysis_limits": { + "model_memory_limit": "11mb", + "categorization_examples_limit": 4 + }, + "data_description": { + "time_field": "@timestamp", + "time_format": "epoch_ms" + }, + "model_plot_config": { + "enabled": true, + "annotations_enabled": true + }, + "model_snapshot_retention_days": 10, + "daily_model_snapshot_retention_after_days": 1, + "results_index_name": "shared", + "allow_lazy_open": false + }, + "datafeed": { + "datafeed_id": "datafeed-ad-test1", + "job_id": "ad-test1", + "query": { + "bool": { + "must": [ + { + "match_all": {} + } + ] + } + }, + "indices": [ + "ft_farequote" + ], + "aggregations": { + "buckets": { + "date_histogram": { + "field": "@timestamp", + "fixed_interval": "90000ms" + }, + "aggregations": { + "responsetime": { + "avg": { + "field": "responsetime" + } + }, + "@timestamp": { + "max": { + "field": "@timestamp" + } + } + } + } + }, + "scroll_size": 1000, + "delayed_data_check_config": { + "enabled": true + } + } + }, + { + "job": { + "job_id": "ad-test2", + "groups": [ + "newgroup" + ], + "description": "", + "analysis_config": { + "bucket_span": "15m", + "summary_count_field_name": "doc_count", + "detectors": [ + { + "detector_description": "mean(responsetime)", + "function": "mean", + "field_name": "responsetime", + "detector_index": 0 + } + ], + "influencers": [] + }, + "analysis_limits": { + "model_memory_limit": "11mb", + "categorization_examples_limit": 4 + }, + "data_description": { + "time_field": "@timestamp", + "time_format": "epoch_ms" + }, + "model_plot_config": { + "enabled": true, + "annotations_enabled": true + }, + "model_snapshot_retention_days": 10, + "daily_model_snapshot_retention_after_days": 1, + "results_index_name": "shared", + "allow_lazy_open": false + }, + "datafeed": { + "datafeed_id": "datafeed-ad-test2", + "job_id": "ad-test2", + "query": { + "bool": { + "must": [ + { + "match_all": {} + } + ] + } + }, + "indices": [ + "missing" + ], + "aggregations": { + "buckets": { + "date_histogram": { + "field": "@timestamp", + "fixed_interval": "90000ms" + }, + "aggregations": { + "responsetime": { + "avg": { + "field": "responsetime" + } + }, + "@timestamp": { + "max": { + "field": "@timestamp" + } + } + } + } + }, + "scroll_size": 1000, + "delayed_data_check_config": { + "enabled": true + } + } + }, + { + "job": { + "job_id": "ad-test3", + "custom_settings": {}, + "description": "", + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "mean(responsetime) partitionfield=airline", + "function": "mean", + "field_name": "responsetime", + "partition_field_name": "airline", + "detector_index": 0 + } + ], + "influencers": [ + "airline" + ] + }, + "analysis_limits": { + "model_memory_limit": "11mb", + "categorization_examples_limit": 4 + }, + "data_description": { + "time_field": "@timestamp", + "time_format": "epoch_ms" + }, + "model_plot_config": { + "enabled": false, + "annotations_enabled": false + }, + "model_snapshot_retention_days": 10, + "daily_model_snapshot_retention_after_days": 1, + "results_index_name": "shared", + "allow_lazy_open": false + }, + "datafeed": { + "datafeed_id": "datafeed-ad-test3", + "job_id": "ad-test3", + "query": { + "bool": { + "must": [ + { + "match_all": {} + } + ] + } + }, + "indices": [ + "ft_farequote" + ], + "scroll_size": 1000, + "delayed_data_check_config": { + "enabled": true + } + } + } +] diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/bad_data.json b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/bad_data.json new file mode 100644 index 000000000000..5c40480832c0 --- /dev/null +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/bad_data.json @@ -0,0 +1 @@ +Hey! this isn't JSON. diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/data_frame_analytics_jobs_7.16.json b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/data_frame_analytics_jobs_7.16.json new file mode 100644 index 000000000000..cb93aa9e24c5 --- /dev/null +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/data_frame_analytics_jobs_7.16.json @@ -0,0 +1,60 @@ +[ + { + "id": "dfa-test1", + "description": "Classification job based on 'ft_bank_marketing' dataset with dependentVariable 'y' and trainingPercent '20'", + "source": { + "index": [ + "ft_bank_marketing" + ], + "query": { + "match_all": {} + } + }, + "dest": { + "index": "user-dfa-test1", + "results_field": "ml" + }, + "analysis": { + "classification": { + "prediction_field_name": "user-test", + "dependent_variable": "y", + "training_percent": 20 + } + }, + "analyzed_fields": { + "includes": [], + "excludes": [] + }, + "model_memory_limit": "60mb", + "allow_lazy_start": false + }, + { + "id": "dfa-test2", + "description": "Classification job based on 'ft_bank_marketing' dataset with dependentVariable 'y' and trainingPercent '20'", + "source": { + "index": [ + "missing-index" + ], + "query": { + "match_all": {} + } + }, + "dest": { + "index": "user-dfa-test2", + "results_field": "ml" + }, + "analysis": { + "classification": { + "prediction_field_name": "test", + "dependent_variable": "y", + "training_percent": 20 + } + }, + "analyzed_fields": { + "includes": [], + "excludes": [] + }, + "model_memory_limit": "60mb", + "allow_lazy_start": false + } +] diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts new file mode 100644 index 000000000000..6211885af0a2 --- /dev/null +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import path from 'path'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { JobType } from '../../../../../plugins/ml/common/types/saved_objects'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + const testDataListPositive = [ + { + filePath: path.join(__dirname, 'files_to_import', 'anomaly_detection_jobs_7.16.json'), + expected: { + jobType: 'anomaly-detector' as JobType, + jobIds: ['ad-test1', 'ad-test3'], + skippedJobIds: ['ad-test2'], + }, + }, + { + filePath: path.join(__dirname, 'files_to_import', 'data_frame_analytics_jobs_7.16.json'), + expected: { + jobType: 'data-frame-analytics' as JobType, + jobIds: ['dfa-test1'], + skippedJobIds: ['dfa-test2'], + }, + }, + ]; + + describe('import jobs', function () { + this.tags(['mlqa']); + before(async () => { + await ml.api.cleanMlIndices(); + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/bm_classification'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.createIndexPatternIfNeeded('ft_bank_marketing', '@timestamp'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + await ml.securityUI.loginAsMlPowerUser(); + await ml.navigation.navigateToStackManagement(); + await ml.navigation.navigateToStackManagementJobsListPage(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + for (const testData of testDataListPositive) { + it('selects and reads file', async () => { + await ml.testExecution.logTestStep('selects job import'); + await ml.stackManagementJobs.openImportFlyout(); + await ml.stackManagementJobs.selectFileToImport(testData.filePath); + }); + it('has the correct importable jobs', async () => { + await ml.stackManagementJobs.assertCorrectTitle( + [...testData.expected.jobIds, ...testData.expected.skippedJobIds].length, + testData.expected.jobType + ); + await ml.stackManagementJobs.assertJobIdsExist(testData.expected.jobIds); + await ml.stackManagementJobs.assertJobIdsSkipped(testData.expected.skippedJobIds); + }); + + it('imports jobs', async () => { + await ml.stackManagementJobs.importJobs(); + }); + + it('ensures jobs have been imported', async () => { + if (testData.expected.jobType === 'anomaly-detector') { + await ml.navigation.navigateToStackManagementJobsListPageAnomalyDetectionTab(); + await ml.jobTable.refreshJobList(); + for (const id of testData.expected.jobIds) { + await ml.jobTable.filterWithSearchString(id); + } + for (const id of testData.expected.skippedJobIds) { + await ml.jobTable.filterWithSearchString(id, 0); + } + } else { + await ml.navigation.navigateToStackManagementJobsListPageAnalyticsTab(); + await ml.dataFrameAnalyticsTable.refreshAnalyticsTable(); + for (const id of testData.expected.jobIds) { + await ml.dataFrameAnalyticsTable.assertAnalyticsJobDisplayedInTable(id, true); + } + for (const id of testData.expected.skippedJobIds) { + await ml.dataFrameAnalyticsTable.assertAnalyticsJobDisplayedInTable(id, false); + } + } + }); + } + + describe('correctly fails to import bad data', async () => { + it('selects and reads file', async () => { + await ml.testExecution.logTestStep('selects job import'); + await ml.stackManagementJobs.openImportFlyout(); + await ml.stackManagementJobs.selectFileToImport( + path.join(__dirname, 'files_to_import', 'bad_data.json'), + true + ); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/index.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/index.ts index f120ab0b450d..c5e0728266ba 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/index.ts +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/index.ts @@ -13,5 +13,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./synchronize')); loadTestFile(require.resolve('./manage_spaces')); + loadTestFile(require.resolve('./import_jobs')); + loadTestFile(require.resolve('./export_jobs')); }); } diff --git a/x-pack/test/functional/services/ml/stack_management_jobs.ts b/x-pack/test/functional/services/ml/stack_management_jobs.ts index 48fb89e51ff1..45b9fa2f29cc 100644 --- a/x-pack/test/functional/services/ml/stack_management_jobs.ts +++ b/x-pack/test/functional/services/ml/stack_management_jobs.ts @@ -6,10 +6,16 @@ */ import expect from '@kbn/expect'; +import { REPO_ROOT } from '@kbn/utils'; +import fs from 'fs'; +import path from 'path'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { MlADJobTable } from './job_table'; -import { MlDFAJobTable } from './data_frame_analytics_table'; +import type { FtrProviderContext } from '../../ftr_provider_context'; +import type { MlADJobTable } from './job_table'; +import type { MlDFAJobTable } from './data_frame_analytics_table'; +import type { JobType } from '../../../../plugins/ml/common/types/saved_objects'; +import type { Job, Datafeed } from '../../../../plugins/ml/common/types/anomaly_detection_jobs'; +import type { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common'; type SyncFlyoutObjectType = | 'MissingObjects' @@ -18,7 +24,7 @@ type SyncFlyoutObjectType = | 'ObjectsUnmatchedDatafeed'; export function MachineLearningStackManagementJobsProvider( - { getService }: FtrProviderContext, + { getService, getPageObjects }: FtrProviderContext, mlADJobTable: MlADJobTable, mlDFAJobTable: MlDFAJobTable ) { @@ -26,6 +32,9 @@ export function MachineLearningStackManagementJobsProvider( const retry = getService('retry'); const testSubjects = getService('testSubjects'); const toasts = getService('toasts'); + const log = getService('log'); + + const PageObjects = getPageObjects(['common']); return { async openSyncFlyout() { @@ -194,5 +203,212 @@ export function MachineLearningStackManagementJobsProvider( } await this.assertSpaceSelectionRowSelected(spaceId, shouldSelect); }, + + async openImportFlyout() { + await retry.tryForTime(5000, async () => { + await testSubjects.click('mlJobsImportButton', 1000); + await testSubjects.existOrFail('mlJobMgmtImportJobsFlyout'); + }); + }, + + async openExportFlyout() { + await retry.tryForTime(5000, async () => { + await testSubjects.click('mlJobsExportButton', 1000); + await testSubjects.existOrFail('mlJobMgmtExportJobsFlyout'); + }); + }, + + async selectFileToImport(filePath: string, expectError: boolean = false) { + log.debug(`Importing file '${filePath}' ...`); + await PageObjects.common.setFileInputPath(filePath); + + if (expectError) { + await testSubjects.existOrFail('~mlJobMgmtImportJobsFileReadErrorCallout'); + } else { + await testSubjects.missingOrFail('~mlJobMgmtImportJobsFileReadErrorCallout'); + await testSubjects.existOrFail('mlJobMgmtImportJobsFileRead'); + } + }, + + async assertJobIdsExist(expectedJobIds: string[]) { + const inputs = await testSubjects.findAll('mlJobMgmtImportJobIdInput'); + const actualJobIds = await Promise.all(inputs.map((i) => i.getAttribute('value'))); + + expect(actualJobIds.sort()).to.eql( + expectedJobIds.sort(), + `Expected job ids to be '${JSON.stringify(expectedJobIds)}' (got '${JSON.stringify( + actualJobIds + )}')` + ); + }, + + async assertCorrectTitle(jobCount: number, jobType: JobType) { + const dataTestSubj = + jobType === 'anomaly-detector' + ? 'mlJobMgmtImportJobsADTitle' + : 'mlJobMgmtImportJobsDFATitle'; + const subj = await testSubjects.find(dataTestSubj); + const title = (await subj.parseDomContent()).html(); + + const jobTypeString = + jobType === 'anomaly-detector' ? 'anomaly detection' : 'data frame analytics'; + + const results = title.match( + /(\d) (anomaly detection|data frame analytics) job[s]? read from file$/ + ); + expect(results).to.not.eql(null, `Expected regex results to not be null`); + const foundCount = results![1]; + const foundJobTypeString = results![2]; + expect(foundCount).to.eql( + jobCount, + `Expected job count to be '${jobCount}' (got '${foundCount}')` + ); + expect(foundJobTypeString).to.eql( + jobTypeString, + `Expected job count to be '${jobTypeString}' (got '${foundJobTypeString}')` + ); + }, + + async assertJobIdsSkipped(expectedJobIds: string[]) { + const subj = await testSubjects.find('mlJobMgmtImportJobsCannotBeImportedCallout'); + const skippedJobTitles = await subj.findAllByTagName('h5'); + const actualJobIds = ( + await Promise.all(skippedJobTitles.map((i) => i.parseDomContent())) + ).map((t) => t.html()); + + expect(actualJobIds.sort()).to.eql( + expectedJobIds.sort(), + `Expected job ids to be '${JSON.stringify(expectedJobIds)}' (got '${JSON.stringify( + actualJobIds + )}')` + ); + }, + + async importJobs() { + await testSubjects.click('mlJobMgmtImportImportButton', 1000); + await testSubjects.missingOrFail('mlJobMgmtImportJobsFlyout', { timeout: 60 * 1000 }); + }, + + async assertReadErrorCalloutExists() { + await testSubjects.existOrFail('~mlJobMgmtImportJobsFileReadErrorCallout'); + }, + + async selectExportJobType(jobType: JobType) { + if (jobType === 'anomaly-detector') { + await testSubjects.click('mlJobMgmtExportJobsADTab'); + await testSubjects.existOrFail('mlJobMgmtExportJobsADJobList'); + } else { + await testSubjects.click('mlJobMgmtExportJobsDFATab'); + await testSubjects.existOrFail('mlJobMgmtExportJobsDFAJobList'); + } + }, + + async selectExportJobSelectAll(jobType: JobType) { + await testSubjects.click('mlJobMgmtExportJobsSelectAllButton'); + const subjLabel = + jobType === 'anomaly-detector' + ? 'mlJobMgmtExportJobsADJobList' + : 'mlJobMgmtExportJobsDFAJobList'; + const subj = await testSubjects.find(subjLabel); + const inputs = await subj.findAllByTagName('input'); + const allInputValues = await Promise.all(inputs.map((input) => input.getAttribute('value'))); + expect(allInputValues.every((i) => i === 'on')).to.eql( + true, + `Expected all inputs to be checked` + ); + }, + + async getDownload(filePath: string) { + return retry.tryForTime(5000, async () => { + expect(fs.existsSync(filePath)).to.be(true); + return fs.readFileSync(filePath).toString(); + }); + }, + + getExportedFile(fileName: string) { + return path.resolve(REPO_ROOT, `target/functional-tests/downloads/${fileName}.json`); + }, + + deleteExportedFiles(fileNames: string[]) { + fileNames.forEach((file) => { + try { + fs.unlinkSync(this.getExportedFile(file)); + } catch (e) { + // it might not have been there to begin with + } + }); + }, + + async selectExportJobs() { + await testSubjects.click('mlJobMgmtExportExportButton'); + await testSubjects.missingOrFail('mlJobMgmtExportJobsFlyout', { timeout: 60 * 1000 }); + }, + + async assertExportedADJobsAreCorrect(expectedJobs: Array<{ job: Job; datafeed: Datafeed }>) { + const file = JSON.parse( + await this.getDownload(this.getExportedFile('anomaly_detection_jobs')) + ); + const loadedFile = Array.isArray(file) ? file : [file]; + const sortedActualJobs = loadedFile.sort((a, b) => a.job.job_id.localeCompare(b.job.job_id)); + + const sortedExpectedJobs = expectedJobs.sort((a, b) => + a.job.job_id.localeCompare(b.job.job_id) + ); + expect(sortedActualJobs.length).to.eql( + sortedExpectedJobs.length, + `Expected length of exported jobs to be '${sortedExpectedJobs.length}' (got '${sortedActualJobs.length}')` + ); + + sortedExpectedJobs.forEach((expectedJob, i) => { + expect(sortedActualJobs[i].job.job_id).to.eql( + expectedJob.job.job_id, + `Expected job id to be '${expectedJob.job.job_id}' (got '${sortedActualJobs[i].job.job_id}')` + ); + expect(sortedActualJobs[i].job.analysis_config.detectors.length).to.eql( + expectedJob.job.analysis_config.detectors.length, + `Expected detectors length to be '${expectedJob.job.analysis_config.detectors.length}' (got '${sortedActualJobs[i].job.analysis_config.detectors.length}')` + ); + expect(sortedActualJobs[i].job.analysis_config.detectors[0].function).to.eql( + expectedJob.job.analysis_config.detectors[0].function, + `Expected first detector function to be '${expectedJob.job.analysis_config.detectors[0].function}' (got '${sortedActualJobs[i].job.analysis_config.detectors[0].function}')` + ); + expect(sortedActualJobs[i].datafeed.datafeed_id).to.eql( + expectedJob.datafeed.datafeed_id, + `Expected job id to be '${expectedJob.datafeed.datafeed_id}' (got '${sortedActualJobs[i].datafeed.datafeed_id}')` + ); + }); + }, + + async assertExportedDFAJobsAreCorrect(expectedJobs: DataFrameAnalyticsConfig[]) { + const file = JSON.parse( + await this.getDownload(this.getExportedFile('data_frame_analytics_jobs')) + ); + const loadedFile = Array.isArray(file) ? file : [file]; + const sortedActualJobs = loadedFile.sort((a, b) => a.id.localeCompare(b.id)); + + const sortedExpectedJobs = expectedJobs.sort((a, b) => a.id.localeCompare(b.id)); + + expect(sortedActualJobs.length).to.eql( + sortedExpectedJobs.length, + `Expected length of exported jobs to be '${sortedExpectedJobs.length}' (got '${sortedActualJobs.length}')` + ); + + sortedExpectedJobs.forEach((expectedJob, i) => { + expect(sortedActualJobs[i].id).to.eql( + expectedJob.id, + `Expected job id to be '${expectedJob.id}' (got '${sortedActualJobs[i].id}')` + ); + const expectedType = Object.keys(expectedJob.analysis)[0]; + const actualType = Object.keys(sortedActualJobs[i].analysis)[0]; + expect(actualType).to.eql( + expectedType, + `Expected job type to be '${expectedType}' (got '${actualType}')` + ); + expect(sortedActualJobs[i].dest.index).to.eql( + expectedJob.dest.index, + `Expected destination index to be '${expectedJob.dest.index}' (got '${sortedActualJobs[i].dest.index}')` + ); + }); + }, }; } From 6f357e043331e0291f16750dc1cb907c2d4ce86a Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 3 Sep 2021 16:10:29 +0300 Subject: [PATCH 13/47] [Cases] Do not show status dropdown on modal cases selector (#111101) --- .../all_cases/all_cases_generic.tsx | 3 +- .../public/components/all_cases/columns.tsx | 68 ++++++++++--------- .../components/all_cases/index.test.tsx | 27 +++++++- 3 files changed, 63 insertions(+), 35 deletions(-) diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx index 72491a2bc1e3..9cbb13f7227a 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx @@ -203,7 +203,8 @@ export const AllCasesGeneric = React.memo( handleIsLoading, isLoadingCases: loading, refreshCases, - showActions, + // isSelectorView is boolean | undefined. We need to convert it to a boolean. + isSelectorView: !!isSelectorView, userCanCrud, connectors, }); diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.tsx index 8b755b0c6096..c0bd6536f1b7 100644 --- a/x-pack/plugins/cases/public/components/all_cases/columns.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/columns.tsx @@ -72,7 +72,7 @@ export interface GetCasesColumn { handleIsLoading: (a: boolean) => void; isLoadingCases: string[]; refreshCases?: (a?: boolean) => void; - showActions: boolean; + isSelectorView: boolean; userCanCrud: boolean; connectors?: ActionConnector[]; } @@ -84,7 +84,7 @@ export const useCasesColumns = ({ handleIsLoading, isLoadingCases, refreshCases, - showActions, + isSelectorView, userCanCrud, connectors = [], }: GetCasesColumn): CasesColumns[] => { @@ -281,38 +281,42 @@ export const useCasesColumns = ({ return getEmptyTagValue(); }, }, - { - name: i18n.STATUS, - render: (theCase: Case) => { - if (theCase?.subCases == null || theCase.subCases.length === 0) { - if (theCase.status == null || theCase.type === CaseType.collection) { - return getEmptyTagValue(); - } - return ( - 0} - onStatusChanged={(status) => - handleDispatchUpdate({ - updateKey: 'status', - updateValue: status, - caseId: theCase.id, - version: theCase.version, - }) + ...(!isSelectorView + ? [ + { + name: i18n.STATUS, + render: (theCase: Case) => { + if (theCase?.subCases == null || theCase.subCases.length === 0) { + if (theCase.status == null || theCase.type === CaseType.collection) { + return getEmptyTagValue(); + } + return ( + 0} + onStatusChanged={(status) => + handleDispatchUpdate({ + updateKey: 'status', + updateValue: status, + caseId: theCase.id, + version: theCase.version, + }) + } + /> + ); } - /> - ); - } - const badges = getSubCasesStatusCountsBadges(theCase.subCases); - return badges.map(({ color, count }, index) => ( - - {count} - - )); - }, - }, - ...(showActions + const badges = getSubCasesStatusCountsBadges(theCase.subCases); + return badges.map(({ color, count }, index) => ( + + {count} + + )); + }, + }, + ] + : []), + ...(userCanCrud && !isSelectorView ? [ { name: ( diff --git a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx index 9e6928d43c86..3fff43108772 100644 --- a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx @@ -144,7 +144,7 @@ describe('AllCasesGeneric', () => { filterStatus: CaseStatuses.open, handleIsLoading: jest.fn(), isLoadingCases: [], - showActions: true, + isSelectorView: false, userCanCrud: true, }; @@ -377,7 +377,7 @@ describe('AllCasesGeneric', () => { isLoadingCases: [], filterStatus: CaseStatuses.open, handleIsLoading: jest.fn(), - showActions: false, + isSelectorView: true, userCanCrud: true, }) ); @@ -926,4 +926,27 @@ describe('AllCasesGeneric', () => { ).toBeFalsy(); }); }); + + it('should not render status when isSelectorView=true', async () => { + const wrapper = mount( + + + + ); + + const { result } = renderHook(() => + useCasesColumns({ + ...defaultColumnArgs, + isSelectorView: true, + }) + ); + + expect(result.current.find((i) => i.name === 'Status')).toBeFalsy(); + + await waitFor(() => { + expect(wrapper.find('[data-test-subj="cases-table"]').exists()).toBeTruthy(); + }); + + expect(wrapper.find('[data-test-subj="case-view-status-dropdown"]').exists()).toBeFalsy(); + }); }); From 75486ecd1228825cafca280dde998220b24fbb64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ester=20Mart=C3=AD=20Vilaseca?= Date: Fri, 3 Sep 2021 15:15:53 +0200 Subject: [PATCH 14/47] [Stack Monitoring] Add setup mode to react app (#110670) * Show setup mode button and setup bottom bar * Adapt setup mode in react components to work without angular * Add setup mode data update to react app * Add missing functions from setup mode * Revert setup mode changes from react components * remove some empty lines * Add setup button to monitoring toolbar * Fix types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../application/global_state_context.tsx | 4 +- .../pages/cluster/overview_page.tsx | 26 ++- .../application/pages/page_template.tsx | 31 +-- .../application/setup_mode/setup_mode.tsx | 200 ++++++++++++++++ .../setup_mode/setup_mode_renderer.d.ts | 8 + .../setup_mode/setup_mode_renderer.js | 217 ++++++++++++++++++ .../public/components/shared/toolbar.tsx | 54 +++-- .../monitoring/public/external_config.ts | 16 ++ .../monitoring/public/lib/setup_mode.tsx | 4 + x-pack/plugins/monitoring/public/plugin.ts | 2 + 10 files changed, 514 insertions(+), 48 deletions(-) create mode 100644 x-pack/plugins/monitoring/public/application/setup_mode/setup_mode.tsx create mode 100644 x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts create mode 100644 x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js create mode 100644 x-pack/plugins/monitoring/public/external_config.ts diff --git a/x-pack/plugins/monitoring/public/application/global_state_context.tsx b/x-pack/plugins/monitoring/public/application/global_state_context.tsx index dc33316dbd9d..57bb638651d0 100644 --- a/x-pack/plugins/monitoring/public/application/global_state_context.tsx +++ b/x-pack/plugins/monitoring/public/application/global_state_context.tsx @@ -13,9 +13,11 @@ interface GlobalStateProviderProps { toasts: MonitoringStartPluginDependencies['core']['notifications']['toasts']; } -interface State { +export interface State { cluster_uuid?: string; ccs?: any; + inSetupMode?: boolean; + save?: () => void; } export const GlobalStateContext = createContext({} as State); diff --git a/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx b/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx index ddc097caea57..f329323bafda 100644 --- a/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx @@ -15,8 +15,15 @@ import { TabMenuItem } from '../page_template'; import { PageLoading } from '../../../components'; import { Overview } from '../../../components/cluster/overview'; import { ExternalConfigContext } from '../../external_config_context'; +import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; const CODE_PATHS = [CODE_PATH_ALL]; +interface SetupModeProps { + setupMode: any; + flyoutComponent: any; + bottomBarComponent: any; +} export const ClusterOverview: React.FC<{}> = () => { // TODO: check how many requests with useClusters @@ -49,11 +56,20 @@ export const ClusterOverview: React.FC<{}> = () => { return ( {loaded ? ( - ( + + {flyoutComponent} + + {/* */} + {bottomBarComponent} + + )} /> ) : ( diff --git a/x-pack/plugins/monitoring/public/application/pages/page_template.tsx b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx index f40c2d3ec5e5..29aafa09814f 100644 --- a/x-pack/plugins/monitoring/public/application/pages/page_template.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiTab, EuiTabs, EuiTitle } from '@elastic/eui'; +import { EuiTab, EuiTabs } from '@elastic/eui'; import React from 'react'; import { useTitle } from '../hooks/use_title'; import { MonitoringToolbar } from '../../components/shared/toolbar'; @@ -29,34 +29,7 @@ export const PageTemplate: React.FC = ({ title, pageTitle, ta return (
- - - - -
{/* HERE GOES THE SETUP BUTTON */}
-
- - {pageTitle && ( -
- -

{pageTitle}

-
-
- )} -
-
-
- - - - -
- + {tabs && ( {tabs.map((item, idx) => { diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode.tsx b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode.tsx new file mode 100644 index 000000000000..70932e517733 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode.tsx @@ -0,0 +1,200 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from 'react-dom'; +import { get, includes } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { HttpStart } from 'kibana/public'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import { Legacy } from '../../legacy_shims'; +import { SetupModeEnterButton } from '../../components/setup_mode/enter_button'; +import { SetupModeFeature } from '../../../common/enums'; +import { ISetupModeContext } from '../../components/setup_mode/setup_mode_context'; +import { State as GlobalState } from '../../application/global_state_context'; + +function isOnPage(hash: string) { + return includes(window.location.hash, hash); +} + +let globalState: GlobalState; +let httpService: HttpStart; + +interface ISetupModeState { + enabled: boolean; + data: any; + callback?: (() => void) | null; + hideBottomBar: boolean; +} +const setupModeState: ISetupModeState = { + enabled: false, + data: null, + callback: null, + hideBottomBar: false, +}; + +export const getSetupModeState = () => setupModeState; + +export const setNewlyDiscoveredClusterUuid = (clusterUuid: string) => { + globalState.cluster_uuid = clusterUuid; + globalState.save?.(); +}; + +export const fetchCollectionData = async (uuid?: string, fetchWithoutClusterUuid = false) => { + const clusterUuid = globalState.cluster_uuid; + const ccs = globalState.ccs; + + let url = '../api/monitoring/v1/setup/collection'; + if (uuid) { + url += `/node/${uuid}`; + } else if (!fetchWithoutClusterUuid && clusterUuid) { + url += `/cluster/${clusterUuid}`; + } else { + url += '/cluster'; + } + + try { + const response = await httpService.post(url, { + body: JSON.stringify({ + ccs, + }), + }); + return response; + } catch (err) { + // TODO: handle errors + throw new Error(err); + } +}; + +const notifySetupModeDataChange = () => setupModeState.callback && setupModeState.callback(); + +export const updateSetupModeData = async (uuid?: string, fetchWithoutClusterUuid = false) => { + const data = await fetchCollectionData(uuid, fetchWithoutClusterUuid); + setupModeState.data = data; + const hasPermissions = get(data, '_meta.hasPermissions', false); + if (!hasPermissions) { + let text: string = ''; + if (!hasPermissions) { + text = i18n.translate('xpack.monitoring.setupMode.notAvailablePermissions', { + defaultMessage: 'You do not have the necessary permissions to do this.', + }); + } + + Legacy.shims.toastNotifications.addDanger({ + title: i18n.translate('xpack.monitoring.setupMode.notAvailableTitle', { + defaultMessage: 'Setup mode is not available', + }), + text, + }); + return toggleSetupMode(false); + } + notifySetupModeDataChange(); + + const clusterUuid = globalState.cluster_uuid; + if (!clusterUuid) { + const liveClusterUuid: string = get(data, '_meta.liveClusterUuid'); + const migratedEsNodes = Object.values(get(data, 'elasticsearch.byUuid', {})).filter( + (node: any) => node.isPartiallyMigrated || node.isFullyMigrated + ); + if (liveClusterUuid && migratedEsNodes.length > 0) { + setNewlyDiscoveredClusterUuid(liveClusterUuid); + } + } +}; + +export const hideBottomBar = () => { + setupModeState.hideBottomBar = true; + notifySetupModeDataChange(); +}; +export const showBottomBar = () => { + setupModeState.hideBottomBar = false; + notifySetupModeDataChange(); +}; + +export const disableElasticsearchInternalCollection = async () => { + const clusterUuid = globalState.cluster_uuid; + const url = `../api/monitoring/v1/setup/collection/${clusterUuid}/disable_internal_collection`; + try { + const response = await httpService.post(url); + return response; + } catch (err) { + // TODO: handle errors + throw new Error(err); + } +}; + +export const toggleSetupMode = (inSetupMode: boolean) => { + setupModeState.enabled = inSetupMode; + globalState.inSetupMode = inSetupMode; + globalState.save?.(); + setSetupModeMenuItem(); + notifySetupModeDataChange(); + + if (inSetupMode) { + // Intentionally do not await this so we don't block UI operations + updateSetupModeData(); + } +}; + +export const setSetupModeMenuItem = () => { + if (isOnPage('no-data')) { + return; + } + + const enabled = !globalState.inSetupMode; + const I18nContext = Legacy.shims.I18nContext; + + render( + + + + + , + document.getElementById('setupModeNav') + ); +}; + +export const initSetupModeState = async ( + state: GlobalState, + http: HttpStart, + callback?: () => void +) => { + globalState = state; + httpService = http; + if (callback) { + setupModeState.callback = callback; + } + + if (globalState.inSetupMode) { + toggleSetupMode(true); + } +}; + +export const isInSetupMode = (context?: ISetupModeContext) => { + if (context?.setupModeSupported === false) { + return false; + } + if (setupModeState.enabled) { + return true; + } + + return globalState.inSetupMode; +}; + +export const isSetupModeFeatureEnabled = (feature: SetupModeFeature) => { + if (!setupModeState.enabled) { + return false; + } + + if (feature === SetupModeFeature.MetricbeatMigration) { + if (Legacy.shims.isCloud) { + return false; + } + } + + return true; +}; diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts new file mode 100644 index 000000000000..27462f07c07b --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const SetupModeRenderer: FunctionComponent; diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js new file mode 100644 index 000000000000..337dacd4ecae --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js @@ -0,0 +1,217 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Fragment } from 'react'; +import { + getSetupModeState, + initSetupModeState, + updateSetupModeData, + disableElasticsearchInternalCollection, + toggleSetupMode, + setSetupModeMenuItem, +} from './setup_mode'; +import { Flyout } from '../../components/metricbeat_migration/flyout'; +import { + EuiBottomBar, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiTextColor, + EuiIcon, + EuiSpacer, +} from '@elastic/eui'; +import { findNewUuid } from '../../components/renderers/lib/find_new_uuid'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { GlobalStateContext } from '../../application/global_state_context'; +import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; + +class WrappedSetupModeRenderer extends React.Component { + globalState; + state = { + renderState: false, + isFlyoutOpen: false, + instance: null, + newProduct: null, + isSettingUpNew: false, + }; + + UNSAFE_componentWillMount() { + this.globalState = this.context; + const { kibana } = this.props; + initSetupModeState(this.globalState, kibana.services.http, (_oldData) => { + const newState = { renderState: true }; + + const { productName } = this.props; + if (!productName) { + this.setState(newState); + return; + } + + const setupModeState = getSetupModeState(); + if (!setupModeState.enabled || !setupModeState.data) { + this.setState(newState); + return; + } + + const data = setupModeState.data[productName]; + const oldData = _oldData ? _oldData[productName] : null; + if (data && oldData) { + const newUuid = findNewUuid(Object.keys(oldData.byUuid), Object.keys(data.byUuid)); + if (newUuid) { + newState.newProduct = data.byUuid[newUuid]; + } + } + + this.setState(newState); + }); + setSetupModeMenuItem(); + } + + reset() { + this.setState({ + renderState: false, + isFlyoutOpen: false, + instance: null, + newProduct: null, + isSettingUpNew: false, + }); + } + + getFlyout(data, meta) { + const { productName } = this.props; + const { isFlyoutOpen, instance, isSettingUpNew, newProduct } = this.state; + if (!data || !isFlyoutOpen) { + return null; + } + + let product = null; + if (newProduct) { + product = newProduct; + } + // For new instance discovery flow, we pass in empty instance object + else if (instance && Object.keys(instance).length) { + product = data.byUuid[instance.uuid]; + } + + if (!product) { + const uuids = Object.values(data.byUuid); + if (uuids.length && !isSettingUpNew) { + product = uuids[0]; + } else { + product = { + isNetNewUser: true, + }; + } + } + + return ( + this.reset()} + productName={productName} + product={product} + meta={meta} + instance={instance} + updateProduct={updateSetupModeData} + isSettingUpNew={isSettingUpNew} + /> + ); + } + + getBottomBar(setupModeState) { + if (!setupModeState.enabled || setupModeState.hideBottomBar) { + return null; + } + + return ( + + + + + + + + + , + }} + /> + + + + + + + + toggleSetupMode(false)} + > + {i18n.translate('xpack.monitoring.setupMode.exit', { + defaultMessage: `Exit setup mode`, + })} + + + + + + + + ); + } + + async shortcutToFinishMigration() { + await disableElasticsearchInternalCollection(); + await updateSetupModeData(); + } + + render() { + const { render, productName } = this.props; + const setupModeState = getSetupModeState(); + + let data = { byUuid: {} }; + if (setupModeState.data) { + if (productName && setupModeState.data[productName]) { + data = setupModeState.data[productName]; + } else if (setupModeState.data) { + data = setupModeState.data; + } + } + + const meta = setupModeState.data ? setupModeState.data._meta : null; + + return render({ + setupMode: { + data, + meta, + enabled: setupModeState.enabled, + productName, + updateSetupModeData, + shortcutToFinishMigration: () => this.shortcutToFinishMigration(), + openFlyout: (instance, isSettingUpNew) => + this.setState({ isFlyoutOpen: true, instance, isSettingUpNew }), + closeFlyout: () => this.setState({ isFlyoutOpen: false }), + }, + flyoutComponent: this.getFlyout(data, meta), + bottomBarComponent: this.getBottomBar(setupModeState), + }); + } +} + +WrappedSetupModeRenderer.contextType = GlobalStateContext; +export const SetupModeRenderer = withKibana(WrappedSetupModeRenderer); diff --git a/x-pack/plugins/monitoring/public/components/shared/toolbar.tsx b/x-pack/plugins/monitoring/public/components/shared/toolbar.tsx index 6e45d4d831ec..e5962b7f8087 100644 --- a/x-pack/plugins/monitoring/public/components/shared/toolbar.tsx +++ b/x-pack/plugins/monitoring/public/components/shared/toolbar.tsx @@ -5,11 +5,21 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker, OnRefreshChangeProps } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSuperDatePicker, + EuiTitle, + OnRefreshChangeProps, +} from '@elastic/eui'; import React, { useContext, useCallback } from 'react'; import { MonitoringTimeContainer } from '../../application/pages/use_monitoring_time'; -export const MonitoringToolbar = () => { +interface MonitoringToolbarProps { + pageTitle?: string; +} + +export const MonitoringToolbar: React.FC = ({ pageTitle }) => { const { currentTimerange, handleTimeChange, @@ -38,18 +48,36 @@ export const MonitoringToolbar = () => { ); return ( - - Setup Button + + + + +
{/* HERE GOES THE SETUP BUTTON */}
+
+ + {pageTitle && ( +
+ +

{pageTitle}

+
+
+ )} +
+
+
+ - {}} - isPaused={isPaused} - refreshInterval={refreshInterval} - onRefreshChange={onRefreshChange} - /> +
+ {}} + isPaused={isPaused} + refreshInterval={refreshInterval} + onRefreshChange={onRefreshChange} + /> +
); diff --git a/x-pack/plugins/monitoring/public/external_config.ts b/x-pack/plugins/monitoring/public/external_config.ts new file mode 100644 index 000000000000..29ce410a5a9d --- /dev/null +++ b/x-pack/plugins/monitoring/public/external_config.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +let config: { [key: string]: unknown } = {}; + +export const setConfig = (externalConfig: { [key: string]: unknown }) => { + config = externalConfig; +}; + +export const isReactMigrationEnabled = () => { + return config.renderReactApp; +}; diff --git a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx index 28fd7494b1d1..f622f2944a31 100644 --- a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx +++ b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx @@ -15,6 +15,8 @@ import { ajaxErrorHandlersProvider } from './ajax_error_handler'; import { SetupModeEnterButton } from '../components/setup_mode/enter_button'; import { SetupModeFeature } from '../../common/enums'; import { ISetupModeContext } from '../components/setup_mode/setup_mode_context'; +import * as setupModeReact from '../application/setup_mode/setup_mode'; +import { isReactMigrationEnabled } from '../external_config'; function isOnPage(hash: string) { return includes(window.location.hash, hash); @@ -209,6 +211,7 @@ export const initSetupModeState = async ($scope: any, $injector: any, callback?: }; export const isInSetupMode = (context?: ISetupModeContext) => { + if (isReactMigrationEnabled()) return setupModeReact.isInSetupMode(context); if (context?.setupModeSupported === false) { return false; } @@ -222,6 +225,7 @@ export const isInSetupMode = (context?: ISetupModeContext) => { }; export const isSetupModeFeatureEnabled = (feature: SetupModeFeature) => { + if (isReactMigrationEnabled()) return setupModeReact.isSetupModeFeatureEnabled(feature); if (!setupModeState.enabled) { return false; } diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index 6884dba760fc..6f625194287b 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -36,6 +36,7 @@ import { createThreadPoolRejectionsAlertType } from './alerts/thread_pool_reject import { createMemoryUsageAlertType } from './alerts/memory_usage_alert'; import { createCCRReadExceptionsAlertType } from './alerts/ccr_read_exceptions_alert'; import { createLargeShardSizeAlertType } from './alerts/large_shard_size_alert'; +import { setConfig } from './external_config'; interface MonitoringSetupPluginDependencies { home?: HomePublicPluginSetup; @@ -125,6 +126,7 @@ export class MonitoringPlugin }); const config = Object.fromEntries(externalConfig); + setConfig(config); if (config.renderReactApp) { const { renderApp } = await import('./application'); return renderApp(coreStart, pluginsStart, params, config); From 9ba00ee594ee7dc8411127d983195efe71051ac1 Mon Sep 17 00:00:00 2001 From: ymao1 Date: Fri, 3 Sep 2021 09:49:00 -0400 Subject: [PATCH 15/47] [Actions] Allowing `service` specification in email connector config (#110458) * Initial commit of serverType in email connector config * Fleshing in route to get well known email service configs from nodemailer * Adding elastic cloud to well known server type * Cleaning up email constants and allowing for empty selection * Showing error if user doesn't select server type * Adding hook for setting email config based on server type * Adding tests and making sure settings are not overwritten on edit * Fixing functional test * Adding migration * Adding functional test for migration * Repurposing service instead of adding serverType * Cleanup * Disabling host/port/secure form fields when settings retrieved from API * Updating docs for service * Filtering options based on whether cloud is enabled * Initialize as disabled * Fixing types * Update docs/management/connectors/action-types/email.asciidoc Co-authored-by: David Kilfoyle <41695641+kilfoyle@users.noreply.github.com> Co-authored-by: David Kilfoyle <41695641+kilfoyle@users.noreply.github.com> --- .../connectors/action-types/email.asciidoc | 2 +- x-pack/plugins/actions/common/index.ts | 1 + .../server/builtin_action_types/email.test.ts | 109 ++++++++++- .../server/builtin_action_types/email.ts | 43 ++++- .../get_well_known_email_service.test.ts | 175 ++++++++++++++++++ .../routes/get_well_known_email_service.ts | 57 ++++++ x-pack/plugins/actions/server/routes/index.ts | 3 + .../saved_objects/actions_migrations.test.ts | 48 +++++ .../saved_objects/actions_migrations.ts | 25 +++ .../plugins/triggers_actions_ui/kibana.json | 2 +- .../public/application/app.tsx | 1 + .../builtin_action_types/email/api.ts | 20 ++ .../builtin_action_types/email/email.test.tsx | 61 ++++++ .../builtin_action_types/email/email.tsx | 68 +++++++ .../email/email_connector.test.tsx | 137 +++++++++++++- .../email/email_connector.tsx | 46 ++++- .../email/translations.ts | 7 + .../email/use_email_config.test.ts | 118 ++++++++++++ .../email/use_email_config.ts | 66 +++++++ .../components/builtin_action_types/types.ts | 1 + .../public/application/constants/index.ts | 2 +- .../common/lib/kibana/kibana_react.mock.ts | 1 + .../triggers_actions_ui/public/plugin.ts | 2 + .../alerting_api_integration/common/config.ts | 6 +- .../actions/builtin_action_types/email.ts | 117 ++++++++++++ .../spaces_only/tests/actions/migrations.ts | 18 ++ .../functional/es_archives/actions/data.json | 64 +++++++ .../es_archives/actions/mappings.json | 3 + 28 files changed, 1181 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/actions/server/routes/get_well_known_email_service.test.ts create mode 100644 x-pack/plugins/actions/server/routes/get_well_known_email_service.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/api.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.ts diff --git a/docs/management/connectors/action-types/email.asciidoc b/docs/management/connectors/action-types/email.asciidoc index bab04b805267..98d7b2591a57 100644 --- a/docs/management/connectors/action-types/email.asciidoc +++ b/docs/management/connectors/action-types/email.asciidoc @@ -51,7 +51,7 @@ Use the <> to customize connecto Config defines information for the connector type. -`service`:: The name of a https://nodemailer.com/smtp/well-known/[well-known email service provider]. If `service` is provided, `host`, `port`, and `secure` properties are ignored. For more information on the `gmail` service value, see the https://nodemailer.com/usage/using-gmail/[Nodemailer Gmail documentation]. +`service`:: The name of the email service. If `service` is `elastic_cloud` (for Elastic Cloud notifications) or one of Nodemailer's https://nodemailer.com/smtp/well-known/[well-known email service providers], `host`, `port`, and `secure` properties are ignored. If `service` is `other`, `host` and `port` properties must be defined. For more information on the `gmail` service value, see the https://nodemailer.com/usage/using-gmail/[Nodemailer Gmail documentation]. `from`:: An email address that corresponds to *Sender*. `host`:: A string that corresponds to *Host*. `port`:: A number that corresponds to *Port*. diff --git a/x-pack/plugins/actions/common/index.ts b/x-pack/plugins/actions/common/index.ts index 7825cbfb45f3..d3abfca83c8e 100644 --- a/x-pack/plugins/actions/common/index.ts +++ b/x-pack/plugins/actions/common/index.ts @@ -13,3 +13,4 @@ export * from './alert_history_schema'; export * from './rewrite_request_case'; export const BASE_ACTION_API_PATH = '/api/actions'; +export const INTERNAL_BASE_ACTION_API_PATH = '/internal/actions'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index 8e9ea1c5e4aa..450bf1744150 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -52,7 +52,7 @@ describe('actionTypeRegistry.get() works', () => { }); describe('config validation', () => { - test('config validation succeeds when config is valid', () => { + test('config validation succeeds when config is valid for nodemailer well known service', () => { const config: Record = { service: 'gmail', from: 'bob@example.com', @@ -64,14 +64,46 @@ describe('config validation', () => { port: null, secure: null, }); + }); + + test(`config validation succeeds when config is valid and defaults to 'other' when service is undefined`, () => { + const config: Record = { + from: 'bob@example.com', + host: 'elastic.co', + port: 8080, + hasAuth: true, + }; + expect(validateConfig(actionType, config)).toEqual({ + ...config, + service: 'other', + secure: null, + }); + }); + + test(`config validation succeeds when config is valid and service requires custom host/port value`, () => { + const config: Record = { + service: 'exchange_server', + from: 'bob@example.com', + host: 'elastic.co', + port: 8080, + hasAuth: true, + }; + expect(validateConfig(actionType, config)).toEqual({ + ...config, + secure: null, + }); + }); - delete config.service; - config.host = 'elastic.co'; - config.port = 8080; - config.hasAuth = true; + test(`config validation succeeds when config is valid and service is elastic_cloud`, () => { + const config: Record = { + service: 'elastic_cloud', + from: 'bob@example.com', + hasAuth: true, + }; expect(validateConfig(actionType, config)).toEqual({ ...config, - service: null, + host: null, + port: null, secure: null, }); }); @@ -325,7 +357,7 @@ describe('execute()', () => { ...executorOptions, config: { ...config, - service: null, + service: 'other', hasAuth: false, }, secrets: { @@ -381,12 +413,73 @@ describe('execute()', () => { `); }); + test('parameters are as expected when using elastic_cloud service', async () => { + const customExecutorOptions: EmailActionTypeExecutorOptions = { + ...executorOptions, + config: { + ...config, + service: 'elastic_cloud', + hasAuth: false, + }, + secrets: { + ...secrets, + user: null, + password: null, + }, + }; + + sendEmailMock.mockReset(); + await actionType.executor(customExecutorOptions); + expect(sendEmailMock.mock.calls[0][1]).toMatchInlineSnapshot(` + Object { + "configurationUtilities": Object { + "ensureActionTypeEnabled": [MockFunction], + "ensureHostnameAllowed": [MockFunction], + "ensureUriAllowed": [MockFunction], + "getCustomHostSettings": [MockFunction], + "getProxySettings": [MockFunction], + "getResponseSettings": [MockFunction], + "getSSLSettings": [MockFunction], + "isActionTypeEnabled": [MockFunction], + "isHostnameAllowed": [MockFunction], + "isUriAllowed": [MockFunction], + }, + "content": Object { + "message": "a message to you + + -- + + This message was sent by Kibana.", + "subject": "the subject", + }, + "hasAuth": false, + "routing": Object { + "bcc": Array [ + "jimmy@example.com", + ], + "cc": Array [ + "james@example.com", + ], + "from": "bob@example.com", + "to": Array [ + "jim@example.com", + ], + }, + "transport": Object { + "host": "dockerhost", + "port": 10025, + "secure": false, + }, + } + `); + }); + test('returns expected result when an error is thrown', async () => { const customExecutorOptions: EmailActionTypeExecutorOptions = { ...executorOptions, config: { ...config, - service: null, + service: 'other', hasAuth: false, }, secrets: { diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.ts b/x-pack/plugins/actions/server/builtin_action_types/email.ts index 47748f0f1372..9b11aec6251f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -9,6 +9,7 @@ import { curry } from 'lodash'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import nodemailerGetService from 'nodemailer/lib/well-known'; +import SMTPConnection from 'nodemailer/lib/smtp-connection'; import { sendEmail, JSON_TRANSPORT_SERVICE, SendEmailOptions, Transport } from './lib/send_email'; import { portSchema } from './lib/schemas'; @@ -32,10 +33,29 @@ export type EmailActionTypeExecutorOptions = ActionTypeExecutorOptions< // config definition export type ActionTypeConfigType = TypeOf; +// supported values for `service` in addition to nodemailer's list of well-known services +export enum AdditionalEmailServices { + ELASTIC_CLOUD = 'elastic_cloud', + EXCHANGE = 'exchange_server', + OTHER = 'other', +} + +// these values for `service` require users to fill in host/port/secure +export const CUSTOM_CONFIG_SERVICES: string[] = [ + AdditionalEmailServices.EXCHANGE, + AdditionalEmailServices.OTHER, +]; + +export const ELASTIC_CLOUD_SERVICE: SMTPConnection.Options = { + host: 'dockerhost', + port: 10025, + secure: false, +}; + const EMAIL_FOOTER_DIVIDER = '\n\n--\n\n'; const ConfigSchemaProps = { - service: schema.nullable(schema.string()), + service: schema.string({ defaultValue: 'other' }), host: schema.nullable(schema.string()), port: schema.nullable(portSchema()), secure: schema.nullable(schema.boolean()), @@ -58,7 +78,8 @@ function validateConfig( // translate messages. if (config.service === JSON_TRANSPORT_SERVICE) { return; - } else if (config.service == null) { + } else if (CUSTOM_CONFIG_SERVICES.indexOf(config.service) >= 0) { + // If configured `service` requires custom host/port/secure settings, validate that they are set if (config.host == null && config.port == null) { return 'either [service] or [host]/[port] is required'; } @@ -75,6 +96,7 @@ function validateConfig( return `[host] value '${config.host}' is not in the allowedHosts configuration`; } } else { + // Check configured `service` against nodemailer list of well known services + any custom ones allowed by Kibana const host = getServiceNameHost(config.service); if (host == null) { return `[service] value '${config.service}' is not valid`; @@ -201,13 +223,20 @@ async function executor( transport.password = secrets.password; } - if (config.service !== null) { - transport.service = config.service; - } else { + if (CUSTOM_CONFIG_SERVICES.indexOf(config.service) >= 0) { + // use configured host/port/secure values // already validated service or host/port is not null ... transport.host = config.host!; transport.port = config.port!; transport.secure = getSecureValue(config.secure, config.port); + } else if (config.service === AdditionalEmailServices.ELASTIC_CLOUD) { + // use custom elastic cloud settings + transport.host = ELASTIC_CLOUD_SERVICE.host!; + transport.port = ELASTIC_CLOUD_SERVICE.port!; + transport.secure = ELASTIC_CLOUD_SERVICE.secure!; + } else { + // use nodemailer's well known service config + transport.service = config.service; } const footerMessage = getFooterMessage({ @@ -253,6 +282,10 @@ async function executor( // utilities function getServiceNameHost(service: string): string | null { + if (service === AdditionalEmailServices.ELASTIC_CLOUD) { + return ELASTIC_CLOUD_SERVICE.host!; + } + const serviceEntry = nodemailerGetService(service); if (serviceEntry === false) return null; diff --git a/x-pack/plugins/actions/server/routes/get_well_known_email_service.test.ts b/x-pack/plugins/actions/server/routes/get_well_known_email_service.test.ts new file mode 100644 index 000000000000..bbcedf18142e --- /dev/null +++ b/x-pack/plugins/actions/server/routes/get_well_known_email_service.test.ts @@ -0,0 +1,175 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getWellKnownEmailServiceRoute } from './get_well_known_email_service'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; +import { verifyAccessAndContext } from './verify_access_and_context'; + +jest.mock('./verify_access_and_context.ts', () => ({ + verifyAccessAndContext: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); + (verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler); +}); + +describe('getWellKnownEmailServiceRoute', () => { + it('returns config for well known email service', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getWellKnownEmailServiceRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + expect(config.path).toMatchInlineSnapshot( + `"/internal/actions/connector/_email_config/{service}"` + ); + + const [context, req, res] = mockHandlerArguments( + {}, + { + params: { service: 'gmail' }, + }, + ['ok'] + ); + + expect(await handler(context, req, res)).toMatchInlineSnapshot(` + Object { + "body": Object { + "host": "smtp.gmail.com", + "port": 465, + "secure": true, + }, + } + `); + + expect(res.ok).toHaveBeenCalledWith({ + body: { + host: 'smtp.gmail.com', + port: 465, + secure: true, + }, + }); + }); + + it('returns config for elastic cloud email service', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getWellKnownEmailServiceRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + expect(config.path).toMatchInlineSnapshot( + `"/internal/actions/connector/_email_config/{service}"` + ); + + const [context, req, res] = mockHandlerArguments( + {}, + { + params: { service: 'elastic_cloud' }, + }, + ['ok'] + ); + + expect(await handler(context, req, res)).toMatchInlineSnapshot(` + Object { + "body": Object { + "host": "dockerhost", + "port": 10025, + "secure": false, + }, + } + `); + + expect(res.ok).toHaveBeenCalledWith({ + body: { + host: 'dockerhost', + port: 10025, + secure: false, + }, + }); + }); + + it('returns empty for unknown service', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getWellKnownEmailServiceRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + expect(config.path).toMatchInlineSnapshot( + `"/internal/actions/connector/_email_config/{service}"` + ); + + const [context, req, res] = mockHandlerArguments( + {}, + { + params: { service: 'foo' }, + }, + ['ok'] + ); + + expect(await handler(context, req, res)).toMatchInlineSnapshot(` + Object { + "body": Object {}, + } + `); + + expect(res.ok).toHaveBeenCalledWith({ + body: {}, + }); + }); + + it('ensures the license allows getting well known email service config', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getWellKnownEmailServiceRoute(router, licenseState); + + const [, handler] = router.get.mock.calls[0]; + + const [context, req, res] = mockHandlerArguments( + {}, + { + params: { service: 'gmail' }, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function)); + }); + + it('ensures the license check prevents getting well known email service config', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + (verifyAccessAndContext as jest.Mock).mockImplementation(() => async () => { + throw new Error('OMG'); + }); + + getWellKnownEmailServiceRoute(router, licenseState); + + const [, handler] = router.get.mock.calls[0]; + + const [context, req, res] = mockHandlerArguments( + {}, + { + params: { service: 'gmail' }, + }, + ['ok'] + ); + + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function)); + }); +}); diff --git a/x-pack/plugins/actions/server/routes/get_well_known_email_service.ts b/x-pack/plugins/actions/server/routes/get_well_known_email_service.ts new file mode 100644 index 000000000000..837084f43b86 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/get_well_known_email_service.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'kibana/server'; +import nodemailerGetService from 'nodemailer/lib/well-known'; +import SMTPConnection from 'nodemailer/lib/smtp-connection'; +import { ILicenseState } from '../lib'; +import { INTERNAL_BASE_ACTION_API_PATH } from '../../common'; +import { ActionsRequestHandlerContext } from '../types'; +import { verifyAccessAndContext } from './verify_access_and_context'; +import { AdditionalEmailServices, ELASTIC_CLOUD_SERVICE } from '../builtin_action_types/email'; + +const paramSchema = schema.object({ + service: schema.string(), +}); + +export const getWellKnownEmailServiceRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.get( + { + path: `${INTERNAL_BASE_ACTION_API_PATH}/connector/_email_config/{service}`, + validate: { + params: paramSchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const { service } = req.params; + + let response: SMTPConnection.Options = {}; + if (service === AdditionalEmailServices.ELASTIC_CLOUD) { + response = ELASTIC_CLOUD_SERVICE; + } else { + const serviceEntry = nodemailerGetService(service); + if (serviceEntry) { + response = { + host: serviceEntry.host, + port: serviceEntry.port, + secure: serviceEntry.secure, + }; + } + } + + return res.ok({ + body: response, + }); + }) + ) + ); +}; diff --git a/x-pack/plugins/actions/server/routes/index.ts b/x-pack/plugins/actions/server/routes/index.ts index a236e514ef78..0d39d87635d5 100644 --- a/x-pack/plugins/actions/server/routes/index.ts +++ b/x-pack/plugins/actions/server/routes/index.ts @@ -15,6 +15,7 @@ import { getActionRoute } from './get'; import { getAllActionRoute } from './get_all'; import { connectorTypesRoute } from './connector_types'; import { updateActionRoute } from './update'; +import { getWellKnownEmailServiceRoute } from './get_well_known_email_service'; import { defineLegacyRoutes } from './legacy'; export function defineRoutes( @@ -30,4 +31,6 @@ export function defineRoutes( updateActionRoute(router, licenseState); connectorTypesRoute(router, licenseState); executeActionRoute(router, licenseState); + + getWellKnownEmailServiceRoute(router, licenseState); } diff --git a/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts b/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts index 7dc1426c13a4..c094109a43d9 100644 --- a/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts +++ b/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts @@ -119,6 +119,54 @@ describe('successful migrations', () => { }); }); + describe('7.16.0', () => { + test('set service config property for .email connectors if service is undefined', () => { + const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0']; + const action = getMockDataForEmail({ config: { service: undefined } }); + const migratedAction = migration716(action, context); + expect(migratedAction.attributes.config).toEqual({ + service: 'other', + }); + expect(migratedAction).toEqual({ + ...action, + attributes: { + ...action.attributes, + config: { + service: 'other', + }, + }, + }); + }); + + test('set service config property for .email connectors if service is null', () => { + const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0']; + const action = getMockDataForEmail({ config: { service: null } }); + const migratedAction = migration716(action, context); + expect(migratedAction.attributes.config).toEqual({ + service: 'other', + }); + expect(migratedAction).toEqual({ + ...action, + attributes: { + ...action.attributes, + config: { + service: 'other', + }, + }, + }); + }); + + test('skips migrating .email connectors if service is defined, even if value is nonsense', () => { + const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0']; + const action = getMockDataForEmail({ config: { service: 'gobbledygook' } }); + const migratedAction = migration716(action, context); + expect(migratedAction.attributes.config).toEqual({ + service: 'gobbledygook', + }); + expect(migratedAction).toEqual(action); + }); + }); + describe('8.0.0', () => { test('no op migration for rules SO', () => { const migration800 = getActionsMigrations(encryptedSavedObjectsSetup)['8.0.0']; diff --git a/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts b/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts index 7857a9e1f833..e75f3eb41f2d 100644 --- a/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts +++ b/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts @@ -62,6 +62,12 @@ export function getActionsMigrations( pipeMigrations(addisMissingSecretsField) ); + const migrationEmailActionsSixteen = createEsoMigration( + encryptedSavedObjects, + (doc): doc is SavedObjectUnsanitizedDoc => doc.attributes.actionTypeId === '.email', + pipeMigrations(setServiceConfigIfNotSet) + ); + const migrationActions800 = createEsoMigration( encryptedSavedObjects, (doc: SavedObjectUnsanitizedDoc): doc is SavedObjectUnsanitizedDoc => @@ -73,6 +79,7 @@ export function getActionsMigrations( '7.10.0': executeMigrationWithErrorHandling(migrationActionsTen, '7.10.0'), '7.11.0': executeMigrationWithErrorHandling(migrationActionsEleven, '7.11.0'), '7.14.0': executeMigrationWithErrorHandling(migrationActionsFourteen, '7.14.0'), + '7.16.0': executeMigrationWithErrorHandling(migrationEmailActionsSixteen, '7.16.0'), '8.0.0': executeMigrationWithErrorHandling(migrationActions800, '8.0.0'), }; } @@ -157,6 +164,24 @@ const addHasAuthConfigurationObject = ( }; }; +const setServiceConfigIfNotSet = ( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc => { + if (doc.attributes.actionTypeId !== '.email' || null != doc.attributes.config.service) { + return doc; + } + return { + ...doc, + attributes: { + ...doc.attributes, + config: { + ...doc.attributes.config, + service: 'other', + }, + }, + }; +}; + const addisMissingSecretsField = ( doc: SavedObjectUnsanitizedDoc ): SavedObjectUnsanitizedDoc => { diff --git a/x-pack/plugins/triggers_actions_ui/kibana.json b/x-pack/plugins/triggers_actions_ui/kibana.json index 4033889d9811..b72a7fe96817 100644 --- a/x-pack/plugins/triggers_actions_ui/kibana.json +++ b/x-pack/plugins/triggers_actions_ui/kibana.json @@ -7,7 +7,7 @@ "version": "kibana", "server": true, "ui": true, - "optionalPlugins": ["alerting", "features", "home", "spaces"], + "optionalPlugins": ["alerting", "cloud", "features", "home", "spaces"], "requiredPlugins": ["management", "charts", "data", "kibanaReact", "kibanaUtils", "savedObjects"], "configPath": ["xpack", "trigger_actions_ui"], "extraPublicDirs": ["public/common", "public/common/constants"], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index 9786f5dcb949..ac0e6d95393b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -37,6 +37,7 @@ export interface TriggersAndActionsUiServices extends CoreStart { alerting?: AlertingStart; spaces?: SpacesPluginStart; storage?: Storage; + isCloud: boolean; setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; actionTypeRegistry: ActionTypeRegistryContract; ruleTypeRegistry: RuleTypeRegistryContract; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/api.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/api.ts new file mode 100644 index 000000000000..82c787426a38 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/api.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HttpSetup } from 'kibana/public'; +import { INTERNAL_BASE_ACTION_API_PATH } from '../../../constants'; +import { EmailConfig } from '../types'; + +export async function getServiceConfig({ + http, + service, +}: { + http: HttpSetup; + service: string; +}): Promise>> { + return await http.get(`${INTERNAL_BASE_ACTION_API_PATH}/connector/_email_config/${service}`); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx index 4d669ab4c76a..0e1bf9ef53e1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx @@ -9,6 +9,7 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '../index'; import { ActionTypeModel } from '../../../../types'; import { EmailActionConnector } from '../types'; +import { getEmailServices } from './email'; const ACTION_TYPE_ID = '.email'; let actionTypeModel: ActionTypeModel; @@ -29,6 +30,18 @@ describe('actionTypeRegistry.get() works', () => { }); }); +describe('getEmailServices', () => { + test('should return elastic cloud service if isCloudEnabled is true', () => { + const services = getEmailServices(true); + expect(services.find((service) => service.value === 'elastic_cloud')).toBeTruthy(); + }); + + test('should not return elastic cloud service if isCloudEnabled is false', () => { + const services = getEmailServices(false); + expect(services.find((service) => service.value === 'elastic_cloud')).toBeFalsy(); + }); +}); + describe('connector validation', () => { test('connector validation succeeds when connector config is valid', async () => { const actionConnector = { @@ -46,6 +59,7 @@ describe('connector validation', () => { host: 'localhost', test: 'test', hasAuth: true, + service: 'other', }, } as EmailActionConnector; @@ -55,6 +69,7 @@ describe('connector validation', () => { from: [], port: [], host: [], + service: [], }, }, secrets: { @@ -82,6 +97,7 @@ describe('connector validation', () => { host: 'localhost', test: 'test', hasAuth: false, + service: 'other', }, } as EmailActionConnector; @@ -91,6 +107,7 @@ describe('connector validation', () => { from: [], port: [], host: [], + service: [], }, }, secrets: { @@ -113,6 +130,7 @@ describe('connector validation', () => { config: { from: 'test@test.com', hasAuth: true, + service: 'other', }, } as EmailActionConnector; @@ -122,6 +140,7 @@ describe('connector validation', () => { from: [], port: ['Port is required.'], host: ['Host is required.'], + service: [], }, }, secrets: { @@ -148,6 +167,7 @@ describe('connector validation', () => { host: 'localhost', test: 'test', hasAuth: true, + service: 'other', }, } as EmailActionConnector; @@ -157,6 +177,7 @@ describe('connector validation', () => { from: [], port: [], host: [], + service: [], }, }, secrets: { @@ -183,6 +204,7 @@ describe('connector validation', () => { host: 'localhost', test: 'test', hasAuth: true, + service: 'other', }, } as EmailActionConnector; @@ -192,6 +214,7 @@ describe('connector validation', () => { from: [], port: [], host: [], + service: [], }, }, secrets: { @@ -202,6 +225,44 @@ describe('connector validation', () => { }, }); }); + test('connector validation fails when server type is not selected', async () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'password', + }, + id: 'test', + actionTypeId: '.email', + isPreconfigured: false, + name: 'email', + config: { + from: 'test@test.com', + port: 2323, + host: 'localhost', + test: 'test', + hasAuth: true, + }, + }; + + expect( + await actionTypeModel.validateConnector((actionConnector as unknown) as EmailActionConnector) + ).toEqual({ + config: { + errors: { + from: [], + port: [], + host: [], + service: ['Service is required.'], + }, + }, + secrets: { + errors: { + user: [], + password: [], + }, + }, + }); + }); }); describe('action params validation', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx index 5e2375462143..fe0b18b1b2e6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx @@ -7,6 +7,7 @@ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; +import { EuiSelectOption } from '@elastic/eui'; import { ActionTypeModel, ConnectorValidationResult, @@ -14,6 +15,69 @@ import { } from '../../../../types'; import { EmailActionParams, EmailConfig, EmailSecrets, EmailActionConnector } from '../types'; +const emailServices: EuiSelectOption[] = [ + { + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.gmailServerTypeLabel', + { + defaultMessage: 'Gmail', + } + ), + value: 'gmail', + }, + { + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.outlookServerTypeLabel', + { + defaultMessage: 'Outlook', + } + ), + value: 'outlook365', + }, + { + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.amazonSesServerTypeLabel', + { + defaultMessage: 'Amazon SES', + } + ), + value: 'ses', + }, + { + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.elasticCloudServerTypeLabel', + { + defaultMessage: 'Elastic Cloud', + } + ), + value: 'elastic_cloud', + }, + { + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.exchangeServerTypeLabel', + { + defaultMessage: 'MS Exchange Server', + } + ), + value: 'exchange_server', + }, + { + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.otherServerTypeLabel', + { + defaultMessage: 'Other', + } + ), + value: 'other', + }, +]; + +export function getEmailServices(isCloudEnabled: boolean) { + return isCloudEnabled + ? emailServices + : emailServices.filter((service) => service.value !== 'elastic_cloud'); +} + export function getActionType(): ActionTypeModel { const mailformat = /^[^@\s]+@[^@\s]+$/; return { @@ -41,6 +105,7 @@ export function getActionType(): ActionTypeModel(), port: new Array(), host: new Array(), + service: new Array(), }; const secretsErrors = { user: new Array(), @@ -69,6 +134,9 @@ export function getActionType(): ActionTypeModel { test('all connector fields is rendered', () => { const actionConnector = { @@ -29,7 +31,7 @@ describe('EmailActionConnectorFields renders', () => { const wrapper = mountWithIntl( {}} editActionSecrets={() => {}} readOnly={false} @@ -39,6 +41,7 @@ describe('EmailActionConnectorFields renders', () => { expect(wrapper.find('[data-test-subj="emailFromInput"]').first().prop('value')).toBe( 'test@test.com' ); + expect(wrapper.find('[data-test-subj="emailServiceSelectInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="emailHostInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="emailPortInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="emailUserInput"]').length > 0).toBeTruthy(); @@ -59,7 +62,7 @@ describe('EmailActionConnectorFields renders', () => { const wrapper = mountWithIntl( {}} editActionSecrets={() => {}} readOnly={false} @@ -75,6 +78,136 @@ describe('EmailActionConnectorFields renders', () => { expect(wrapper.find('[data-test-subj="emailPasswordInput"]').length > 0).toBeFalsy(); }); + test('service field defaults to empty when not defined', () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: { + from: 'test@test.com', + hasAuth: true, + }, + } as EmailActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="emailFromInput"]').first().prop('value')).toBe( + 'test@test.com' + ); + expect(wrapper.find('[data-test-subj="emailServiceSelectInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('select[data-test-subj="emailServiceSelectInput"]').prop('value')).toEqual( + '' + ); + }); + + test('service field is correctly selected when defined', () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: { + from: 'test@test.com', + hasAuth: true, + service: 'gmail', + }, + } as EmailActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="emailServiceSelectInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('select[data-test-subj="emailServiceSelectInput"]').prop('value')).toEqual( + 'gmail' + ); + }); + + test('host, port and secure fields should be disabled when service field is set to well known service', () => { + jest + .spyOn(hooks, 'useEmailConfig') + .mockImplementation(() => ({ emailServiceConfigurable: false, setEmailService: jest.fn() })); + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: { + from: 'test@test.com', + hasAuth: true, + service: 'gmail', + }, + } as EmailActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="emailHostInput"]').first().prop('disabled')).toBe(true); + expect(wrapper.find('[data-test-subj="emailPortInput"]').first().prop('disabled')).toBe(true); + expect(wrapper.find('[data-test-subj="emailSecureSwitch"]').first().prop('disabled')).toBe( + true + ); + }); + + test('host, port and secure fields should not be disabled when service field is set to other', () => { + jest + .spyOn(hooks, 'useEmailConfig') + .mockImplementation(() => ({ emailServiceConfigurable: true, setEmailService: jest.fn() })); + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: { + from: 'test@test.com', + hasAuth: true, + service: 'other', + }, + } as EmailActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="emailHostInput"]').first().prop('disabled')).toBe(false); + expect(wrapper.find('[data-test-subj="emailPortInput"]').first().prop('disabled')).toBe(false); + expect(wrapper.find('[data-test-subj="emailSecureSwitch"]').first().prop('disabled')).toBe( + false + ); + }); + test('should display a message to remember username and password when creating a connector with authentication', () => { const actionConnector = { actionTypeId: '.email', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx index e4d73ced1eb5..c37c3fc8355b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx @@ -12,6 +12,7 @@ import { EuiFlexGroup, EuiFieldNumber, EuiFieldPassword, + EuiSelect, EuiSwitch, EuiFormRow, EuiTitle, @@ -24,13 +25,22 @@ import { ActionConnectorFieldsProps } from '../../../../types'; import { EmailActionConnector } from '../types'; import { useKibana } from '../../../../common/lib/kibana'; import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; +import { getEmailServices } from './email'; +import { useEmailConfig } from './use_email_config'; export const EmailActionConnectorFields: React.FunctionComponent< ActionConnectorFieldsProps > = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => { - const { docLinks } = useKibana().services; - const { from, host, port, secure, hasAuth } = action.config; + const { docLinks, http, isCloud } = useKibana().services; + const { from, host, port, secure, hasAuth, service } = action.config; const { user, password } = action.secrets; + + const { emailServiceConfigurable, setEmailService } = useEmailConfig( + http, + service, + editActionConfig + ); + useEffect(() => { if (!action.id) { editActionConfig('hasAuth', true); @@ -42,6 +52,8 @@ export const EmailActionConnectorFields: React.FunctionComponent< from !== undefined && errors.from !== undefined && errors.from.length > 0; const isHostInvalid: boolean = host !== undefined && errors.host !== undefined && errors.host.length > 0; + const isServiceInvalid: boolean = + service !== undefined && errors.service !== undefined && errors.service.length > 0; const isPortInvalid: boolean = port !== undefined && errors.port !== undefined && errors.port.length > 0; @@ -93,6 +105,31 @@ export const EmailActionConnectorFields: React.FunctionComponent<
+ + + { + setEmailService(e.target.value); + }} + /> + + { editActionConfig('secure', e.target.checked); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts index 5da9145ecec0..df68d0d1237e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts @@ -28,6 +28,13 @@ export const PORT_REQUIRED = i18n.translate( } ); +export const SERVICE_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredServiceText', + { + defaultMessage: 'Service is required.', + } +); + export const HOST_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredHostText', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.test.ts new file mode 100644 index 000000000000..7d9cf1585274 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.test.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { HttpSetup } from 'kibana/public'; +import { useEmailConfig } from './use_email_config'; + +const http = { + get: jest.fn(), +}; + +const editActionConfig = jest.fn(); + +const renderUseEmailConfigHook = (currentService?: string) => + renderHook(() => + useEmailConfig((http as unknown) as HttpSetup, currentService, editActionConfig) + ); + +describe('useEmailConfig', () => { + beforeEach(() => jest.clearAllMocks()); + + it('should call get email config API when service changes and handle result', async () => { + http.get.mockResolvedValueOnce({ + host: 'smtp.gmail.com', + port: 465, + secure: true, + }); + const { result, waitForNextUpdate } = renderUseEmailConfigHook(); + await act(async () => { + result.current.setEmailService('gmail'); + await waitForNextUpdate(); + }); + + expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/gmail'); + expect(editActionConfig).toHaveBeenCalledWith('service', 'gmail'); + + expect(editActionConfig).toHaveBeenCalledWith('host', 'smtp.gmail.com'); + expect(editActionConfig).toHaveBeenCalledWith('port', 465); + expect(editActionConfig).toHaveBeenCalledWith('secure', true); + + expect(result.current.emailServiceConfigurable).toEqual(false); + }); + + it('should call get email config API when service changes and handle partial result', async () => { + http.get.mockResolvedValueOnce({ + host: 'smtp.gmail.com', + port: 465, + }); + const { result, waitForNextUpdate } = renderUseEmailConfigHook(); + await act(async () => { + result.current.setEmailService('gmail'); + await waitForNextUpdate(); + }); + + expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/gmail'); + expect(editActionConfig).toHaveBeenCalledWith('service', 'gmail'); + + expect(editActionConfig).toHaveBeenCalledWith('host', 'smtp.gmail.com'); + expect(editActionConfig).toHaveBeenCalledWith('port', 465); + expect(editActionConfig).toHaveBeenCalledWith('secure', false); + + expect(result.current.emailServiceConfigurable).toEqual(false); + }); + + it('should call get email config API when service changes and handle empty result', async () => { + http.get.mockResolvedValueOnce({}); + const { result, waitForNextUpdate } = renderUseEmailConfigHook(); + await act(async () => { + result.current.setEmailService('foo'); + await waitForNextUpdate(); + }); + + expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/foo'); + expect(editActionConfig).toHaveBeenCalledWith('service', 'foo'); + + expect(editActionConfig).toHaveBeenCalledWith('host', ''); + expect(editActionConfig).toHaveBeenCalledWith('port', 0); + expect(editActionConfig).toHaveBeenCalledWith('secure', false); + + expect(result.current.emailServiceConfigurable).toEqual(true); + }); + + it('should call get email config API when service changes and handle errors', async () => { + http.get.mockImplementationOnce(() => { + throw new Error('no!'); + }); + const { result, waitForNextUpdate } = renderUseEmailConfigHook(); + await act(async () => { + result.current.setEmailService('foo'); + await waitForNextUpdate(); + }); + + expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/foo'); + expect(editActionConfig).toHaveBeenCalledWith('service', 'foo'); + + expect(editActionConfig).toHaveBeenCalledWith('host', ''); + expect(editActionConfig).toHaveBeenCalledWith('port', 0); + expect(editActionConfig).toHaveBeenCalledWith('secure', false); + + expect(result.current.emailServiceConfigurable).toEqual(true); + }); + + it('should call get email config API when initial service value is passed and determine if config is editable without overwriting config', async () => { + http.get.mockResolvedValueOnce({ + host: 'smtp.gmail.com', + port: 465, + secure: true, + }); + const { result } = renderUseEmailConfigHook('gmail'); + expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/gmail'); + expect(editActionConfig).not.toHaveBeenCalled(); + expect(result.current.emailServiceConfigurable).toEqual(false); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.ts new file mode 100644 index 000000000000..fad71cf5d638 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useEffect, useState } from 'react'; +import { HttpSetup } from 'kibana/public'; +import { isEmpty } from 'lodash'; +import { EmailConfig } from '../types'; +import { getServiceConfig } from './api'; + +export function useEmailConfig( + http: HttpSetup, + currentService: string | undefined, + editActionConfig: (property: string, value: unknown) => void +) { + const [emailServiceConfigurable, setEmailServiceConfigurable] = useState(false); + const [emailService, setEmailService] = useState(undefined); + + const getEmailServiceConfig = useCallback( + async (service: string) => { + let serviceConfig: Partial>; + try { + serviceConfig = await getServiceConfig({ http, service }); + setEmailServiceConfigurable(isEmpty(serviceConfig)); + } catch (err) { + serviceConfig = {}; + setEmailServiceConfigurable(true); + } + + return serviceConfig; + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [editActionConfig] + ); + + useEffect(() => { + (async () => { + if (emailService) { + const serviceConfig = await getEmailServiceConfig(emailService); + + editActionConfig('service', emailService); + editActionConfig('host', serviceConfig?.host ? serviceConfig.host : ''); + editActionConfig('port', serviceConfig?.port ? serviceConfig.port : 0); + editActionConfig('secure', null != serviceConfig?.secure ? serviceConfig.secure : false); + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [emailService]); + + useEffect(() => { + (async () => { + if (currentService) { + await getEmailServiceConfig(currentService); + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentService]); + + return { + emailServiceConfigurable, + setEmailService, + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts index 50410ba3c153..60e0a0f14b89 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts @@ -78,6 +78,7 @@ export interface EmailConfig { port: number; secure?: boolean; hasAuth: boolean; + service: string; } export interface EmailSecrets { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts index cc04b8e7871c..bed7b09110d8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts @@ -11,7 +11,7 @@ export { BASE_ALERTING_API_PATH, INTERNAL_BASE_ALERTING_API_PATH, } from '../../../../alerting/common'; -export { BASE_ACTION_API_PATH } from '../../../../actions/common'; +export { BASE_ACTION_API_PATH, INTERNAL_BASE_ACTION_API_PATH } from '../../../../actions/common'; export type Section = 'connectors' | 'rules'; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts index 2985a5306ed5..de64906f75de 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts @@ -40,6 +40,7 @@ export const createStartServicesMock = (): TriggersAndActionsUiServices => { list: jest.fn(), } as ActionTypeRegistryContract, charts: chartPluginMock.createStartContract(), + isCloud: false, kibanaFeatures: [], element: ({ style: { cursor: 'pointer' }, diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 7661eefba7f6..17f0766a826e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -66,6 +66,7 @@ export interface TriggersAndActionsUIPublicPluginStart { interface PluginsSetup { management: ManagementSetup; home?: HomePublicPluginSetup; + cloud?: { isCloudEnabled: boolean }; } interface PluginsStart { @@ -148,6 +149,7 @@ export class Plugin charts: pluginsStart.charts, alerting: pluginsStart.alerting, spaces: pluginsStart.spaces, + isCloud: Boolean(plugins.cloud?.isCloudEnabled), element: params.element, storage: new Storage(window.localStorage), setBreadcrumbs: params.setBreadcrumbs, diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 5a9d2a20fee1..dd43606cc79b 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -149,7 +149,11 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) serverArgs: [ ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), ...(options.publicBaseUrl ? ['--server.publicBaseUrl=https://localhost:5601'] : []), - `--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`, + `--xpack.actions.allowedHosts=${JSON.stringify([ + 'localhost', + 'some.non.existent.com', + 'smtp.live.com', + ])}`, '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', '--xpack.alerting.invalidateApiKeysTask.interval="15s"', '--xpack.alerting.healthCheck.interval="1s"', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts index 917246f09a99..b3829824b797 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts @@ -319,5 +319,122 @@ export default function emailTest({ getService }: FtrProviderContext) { }); }); }); + + it('should return 200 when creating an email action without defining service', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'An email action', + connector_type_id: '.email', + config: { + from: 'bob@example.com', + host: 'some.non.existent.com', + port: 25, + hasAuth: true, + }, + secrets: { + user: 'bob', + password: 'supersecret', + }, + }) + .expect(200); + + expect(createdAction).to.eql({ + id: createdAction.id, + is_preconfigured: false, + name: 'An email action', + connector_type_id: '.email', + is_missing_secrets: false, + config: { + service: 'other', + hasAuth: true, + host: 'some.non.existent.com', + port: 25, + secure: null, + from: 'bob@example.com', + }, + }); + + expect(typeof createdAction.id).to.be('string'); + + const { body: fetchedAction } = await supertest + .get(`/api/actions/connector/${createdAction.id}`) + .expect(200); + + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + is_preconfigured: false, + name: 'An email action', + connector_type_id: '.email', + is_missing_secrets: false, + config: { + from: 'bob@example.com', + service: 'other', + hasAuth: true, + host: 'some.non.existent.com', + port: 25, + secure: null, + }, + }); + }); + + it('should return 200 when creating an email action with nodemailer well-defined service', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'An email action', + connector_type_id: '.email', + config: { + from: 'bob@example.com', + service: 'hotmail', + hasAuth: true, + }, + secrets: { + user: 'bob', + password: 'supersecret', + }, + }) + .expect(200); + + expect(createdAction).to.eql({ + id: createdAction.id, + is_preconfigured: false, + name: 'An email action', + connector_type_id: '.email', + is_missing_secrets: false, + config: { + service: 'hotmail', + hasAuth: true, + host: null, + port: null, + secure: null, + from: 'bob@example.com', + }, + }); + + expect(typeof createdAction.id).to.be('string'); + + const { body: fetchedAction } = await supertest + .get(`/api/actions/connector/${createdAction.id}`) + .expect(200); + + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + is_preconfigured: false, + name: 'An email action', + connector_type_id: '.email', + is_missing_secrets: false, + config: { + from: 'bob@example.com', + service: 'hotmail', + hasAuth: true, + host: null, + port: null, + secure: null, + }, + }); + }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/migrations.ts index 811a9470611e..9b88dace1323 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/migrations.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/migrations.ts @@ -64,5 +64,23 @@ export default function createGetTests({ getService }: FtrProviderContext) { expect(responseWithisMissingSecrets.status).to.eql(200); expect(responseWithisMissingSecrets.body.isMissingSecrets).to.eql(false); }); + + it('7.16.0 migrates email connector configurations to set `service` property if not set', async () => { + const connectorWithService = await supertest.get( + `${getUrlPrefix(``)}/api/actions/action/0f8f2810-0a59-11ec-9a7c-fd0c2b83ff7c` + ); + + expect(connectorWithService.status).to.eql(200); + expect(connectorWithService.body.config).key('service'); + expect(connectorWithService.body.config.service).to.eql('someservice'); + + const connectorWithoutService = await supertest.get( + `${getUrlPrefix(``)}/api/actions/action/1e0824a0-0a59-11ec-9a7c-fd0c2b83ff7c` + ); + + expect(connectorWithoutService.status).to.eql(200); + expect(connectorWithoutService.body.config).key('service'); + expect(connectorWithoutService.body.config.service).to.eql('other'); + }); }); } diff --git a/x-pack/test/functional/es_archives/actions/data.json b/x-pack/test/functional/es_archives/actions/data.json index 18d67da1752b..31d10005c093 100644 --- a/x-pack/test/functional/es_archives/actions/data.json +++ b/x-pack/test/functional/es_archives/actions/data.json @@ -110,3 +110,67 @@ } } } + +{ + "type": "doc", + "value": { + "id": "action:0f8f2810-0a59-11ec-9a7c-fd0c2b83ff7c", + "index": ".kibana_1", + "source": { + "action": { + "actionTypeId" : ".email", + "name" : "test email connector with auth", + "isMissingSecrets" : false, + "config" : { + "hasAuth" : true, + "from" : "me@me.com", + "host" : "smtp.myhost.com", + "port" : 25, + "service" : "someservice", + "secure" : null + }, + "secrets" : "V2EJEtTv3yTFi1kdglhNahnKYWCS+J7aWCJQU+eEqGPZEz6n7G1NsBWoh7IY0FteLTilTteQXyY/Eg3k/7bb0G8Mz+WBZ1mRvUggGTFqgoOptyUsvHoBhv0R/1bCTCabN3Pe88AfnC+VDXqwuMifpmgKEEsKF3H8VONv7TYO02FW" + }, + "migrationVersion": { + "action": "7.14.0" + }, + "coreMigrationVersion" : "7.15.0", + "references": [ + ], + "type": "action", + "updated_at": "2021-08-31T12:43:37.117Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "action:1e0824a0-0a59-11ec-9a7c-fd0c2b83ff7c", + "index": ".kibana_1", + "source": { + "action": { + "actionTypeId" : ".email", + "name" : "test email connector no auth", + "isMissingSecrets" : false, + "config" : { + "hasAuth" : false, + "from" : "you@you.com", + "host" : "smtp.you.com", + "port" : 485, + "secure" : true, + "service" : null + }, + "secrets" : "iw/bRTXZQXOV0ODocb6FQnHR6AyeVyD91We03llNStyTNFwuHVWdFl6ZdiEEeDOadBMeJomvp/dAfQevGpbwWdclcu9F87x3CfeGqV9DtBy0dXRbx9PzKBwgJdK3ucHQDFAs8ZXQbefvCOFjCHGAsJDPhTKj5rTUyg==" + }, + "migrationVersion": { + "action": "7.14.0" + }, + "coreMigrationVersion" : "7.15.0", + "references": [ + ], + "type": "action", + "updated_at": "2021-08-31T12:44:01.396Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/actions/mappings.json b/x-pack/test/functional/es_archives/actions/mappings.json index 737e0df57552..8289174ffd57 100644 --- a/x-pack/test/functional/es_archives/actions/mappings.json +++ b/x-pack/test/functional/es_archives/actions/mappings.json @@ -572,6 +572,9 @@ } } }, + "coreMigrationVersion": { + "type": "keyword" + }, "dashboard": { "properties": { "description": { From 21b4752dba84d8c2ebaa29ec4a65703ea416d60d Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Fri, 3 Sep 2021 15:57:57 +0200 Subject: [PATCH 16/47] [Lens] Fix transition to custom palette inconsistency when in number mode (#110852) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/services/palettes/helpers.ts | 5 +- .../coloring/palette_configuration.test.tsx | 182 ++++++++++++------ .../coloring/palette_configuration.tsx | 36 ++-- .../shared_components/coloring/utils.test.ts | 73 +++++++ .../shared_components/coloring/utils.ts | 5 +- x-pack/test/functional/apps/lens/table.ts | 21 ++ 6 files changed, 246 insertions(+), 76 deletions(-) diff --git a/src/plugins/charts/public/services/palettes/helpers.ts b/src/plugins/charts/public/services/palettes/helpers.ts index d4b1e98f94cc..bd1f8350ba9f 100644 --- a/src/plugins/charts/public/services/palettes/helpers.ts +++ b/src/plugins/charts/public/services/palettes/helpers.ts @@ -70,7 +70,10 @@ export function workoutColorForValue( const comparisonFn = (v: number, threshold: number) => v - threshold; // if steps are defined consider the specific rangeMax/Min as data boundaries - const maxRange = stops.length ? rangeMax : dataRangeArguments[1]; + // as of max reduce its value by 1/colors.length for correct continuity checks + const maxRange = stops.length + ? rangeMax + : dataRangeArguments[1] - (dataRangeArguments[1] - dataRangeArguments[0]) / colors.length; const minRange = stops.length ? rangeMin : dataRangeArguments[0]; // in case of shorter rangers, extends the steps on the sides to cover the whole set diff --git a/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.test.tsx b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.test.tsx index ad1755bdbe85..cda891871168 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.test.tsx +++ b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiColorPalettePickerPaletteProps } from '@elastic/eui'; +import { EuiButtonGroup, EuiColorPalettePickerPaletteProps } from '@elastic/eui'; import { mountWithIntl } from '@kbn/test/jest'; import { chartPluginMock } from 'src/plugins/charts/public/mocks'; import type { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; @@ -14,6 +14,7 @@ import { ReactWrapper } from 'enzyme'; import type { CustomPaletteParams } from '../../../common'; import { applyPaletteParams } from './utils'; import { CustomizablePalette } from './palette_configuration'; +import { act } from 'react-dom/test-utils'; // mocking random id generator function jest.mock('@elastic/eui', () => { @@ -128,71 +129,136 @@ describe('palette panel', () => { }); }); - describe('reverse option', () => { - beforeEach(() => { - props = { - activePalette: { type: 'palette', name: 'positive' }, - palettes: paletteRegistry, - setPalette: jest.fn(), - dataBounds: { min: 0, max: 100 }, - }; - }); + it('should rewrite the min/max range values on palette change', () => { + const instance = mountWithIntl(); + + changePaletteIn(instance, 'custom'); - function toggleReverse(instance: ReactWrapper, checked: boolean) { - return instance - .find('[data-test-subj="lnsPalettePanel_dynamicColoring_reverse"]') - .first() - .prop('onClick')!({} as React.MouseEvent); - } - - it('should reverse the colorStops on click', () => { - const instance = mountWithIntl(); - - toggleReverse(instance, true); - - expect(props.setPalette).toHaveBeenCalledWith( - expect.objectContaining({ - params: expect.objectContaining({ - reverse: true, - }), - }) - ); + expect(props.setPalette).toHaveBeenCalledWith({ + type: 'palette', + name: 'custom', + params: expect.objectContaining({ + rangeMin: 0, + rangeMax: 50, + }), }); }); + }); + + describe('reverse option', () => { + beforeEach(() => { + props = { + activePalette: { type: 'palette', name: 'positive' }, + palettes: paletteRegistry, + setPalette: jest.fn(), + dataBounds: { min: 0, max: 100 }, + }; + }); + + function toggleReverse(instance: ReactWrapper, checked: boolean) { + return instance + .find('[data-test-subj="lnsPalettePanel_dynamicColoring_reverse"]') + .first() + .prop('onClick')!({} as React.MouseEvent); + } + + it('should reverse the colorStops on click', () => { + const instance = mountWithIntl(); + + toggleReverse(instance, true); + + expect(props.setPalette).toHaveBeenCalledWith( + expect.objectContaining({ + params: expect.objectContaining({ + reverse: true, + }), + }) + ); + }); + }); + + describe('percentage / number modes', () => { + beforeEach(() => { + props = { + activePalette: { type: 'palette', name: 'positive' }, + palettes: paletteRegistry, + setPalette: jest.fn(), + dataBounds: { min: 5, max: 200 }, + }; + }); - describe('custom stops', () => { - beforeEach(() => { - props = { - activePalette: { type: 'palette', name: 'positive' }, - palettes: paletteRegistry, - setPalette: jest.fn(), - dataBounds: { min: 0, max: 100 }, - }; + it('should switch mode and range boundaries on click', () => { + const instance = mountWithIntl(); + act(() => { + instance + .find('[data-test-subj="lnsPalettePanel_dynamicColoring_custom_range_groups"]') + .find(EuiButtonGroup) + .prop('onChange')!('number'); }); - it('should be visible for predefined palettes', () => { - const instance = mountWithIntl(); - expect( - instance.find('[data-test-subj="lnsPalettePanel_dynamicColoring_custom_stops"]').exists() - ).toEqual(true); + + act(() => { + instance + .find('[data-test-subj="lnsPalettePanel_dynamicColoring_custom_range_groups"]') + .find(EuiButtonGroup) + .prop('onChange')!('percent'); }); - it('should be visible for custom palettes', () => { - const instance = mountWithIntl( - { + beforeEach(() => { + props = { + activePalette: { type: 'palette', name: 'positive' }, + palettes: paletteRegistry, + setPalette: jest.fn(), + dataBounds: { min: 0, max: 100 }, + }; + }); + it('should be visible for predefined palettes', () => { + const instance = mountWithIntl(); + expect( + instance.find('[data-test-subj="lnsPalettePanel_dynamicColoring_custom_stops"]').exists() + ).toEqual(true); + }); + + it('should be visible for custom palettes', () => { + const instance = mountWithIntl( + - ); - expect( - instance.find('[data-test-subj="lnsPalettePanel_dynamicColoring_custom_stops"]').exists() - ).toEqual(true); - }); + }, + }} + /> + ); + expect( + instance.find('[data-test-subj="lnsPalettePanel_dynamicColoring_custom_stops"]').exists() + ).toEqual(true); }); }); }); diff --git a/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx index bc6a590db0cb..1d1e212b87c0 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx +++ b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx @@ -108,16 +108,21 @@ export function CustomizablePalette({ colorStops: undefined, }; + const newColorStops = getColorStops(palettes, [], activePalette, dataBounds); if (isNewPaletteCustom) { - newParams.colorStops = getColorStops(palettes, [], activePalette, dataBounds); + newParams.colorStops = newColorStops; } newParams.stops = getPaletteStops(palettes, newParams, { prevPalette: isNewPaletteCustom || isCurrentPaletteCustom ? undefined : newPalette.name, dataBounds, + mapFromMinValue: true, }); + newParams.rangeMin = newColorStops[0].stop; + newParams.rangeMax = newColorStops[newColorStops.length - 1].stop; + setPalette({ ...newPalette, params: newParams, @@ -266,18 +271,18 @@ export function CustomizablePalette({ ) as RequiredPaletteParamTypes['rangeType']; const params: CustomPaletteParams = { rangeType: newRangeType }; + const { min: newMin, max: newMax } = getDataMinMax(newRangeType, dataBounds); + const { min: oldMin, max: oldMax } = getDataMinMax( + activePalette.params?.rangeType, + dataBounds + ); + const newColorStops = remapStopsByNewInterval(colorStopsToShow, { + oldInterval: oldMax - oldMin, + newInterval: newMax - newMin, + newMin, + oldMin, + }); if (isCurrentPaletteCustom) { - const { min: newMin, max: newMax } = getDataMinMax(newRangeType, dataBounds); - const { min: oldMin, max: oldMax } = getDataMinMax( - activePalette.params?.rangeType, - dataBounds - ); - const newColorStops = remapStopsByNewInterval(colorStopsToShow, { - oldInterval: oldMax - oldMin, - newInterval: newMax - newMin, - newMin, - oldMin, - }); const stops = getPaletteStops( palettes, { ...activePalette.params, colorStops: newColorStops, ...params }, @@ -285,8 +290,6 @@ export function CustomizablePalette({ ); params.colorStops = newColorStops; params.stops = stops; - params.rangeMin = newColorStops[0].stop; - params.rangeMax = newColorStops[newColorStops.length - 1].stop; } else { params.stops = getPaletteStops( palettes, @@ -294,6 +297,11 @@ export function CustomizablePalette({ { prevPalette: activePalette.name, dataBounds } ); } + // why not use newMin/newMax here? + // That's because there's the concept of continuity to accomodate, where in some scenarios it has to + // take into account the stop value rather than the data value + params.rangeMin = newColorStops[0].stop; + params.rangeMax = newColorStops[newColorStops.length - 1].stop; setPalette(mergePaletteParams(activePalette, params)); }} /> diff --git a/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts b/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts index 97dc2e45c96d..07d93ca5c40c 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts +++ b/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts @@ -8,6 +8,7 @@ import { chartPluginMock } from 'src/plugins/charts/public/mocks'; import { applyPaletteParams, + getColorStops, getContrastColor, getDataMinMax, getPaletteStops, @@ -59,6 +60,78 @@ describe('applyPaletteParams', () => { }); }); +describe('getColorStops', () => { + const paletteRegistry = chartPluginMock.createPaletteRegistry(); + it('should return the same colorStops if a custom palette is passed, avoiding recomputation', () => { + const colorStops = [ + { stop: 0, color: 'red' }, + { stop: 100, color: 'blue' }, + ]; + expect( + getColorStops( + paletteRegistry, + colorStops, + { name: 'custom', type: 'palette' }, + { min: 0, max: 100 } + ) + ).toBe(colorStops); + }); + + it('should get a fresh list of colors', () => { + expect( + getColorStops( + paletteRegistry, + [ + { stop: 0, color: 'red' }, + { stop: 100, color: 'blue' }, + ], + { name: 'mocked', type: 'palette' }, + { min: 0, max: 100 } + ) + ).toEqual([ + { color: 'blue', stop: 0 }, + { color: 'yellow', stop: 50 }, + ]); + }); + + it('should get a fresh list of colors even if custom palette but empty colorStops', () => { + expect( + getColorStops(paletteRegistry, [], { name: 'mocked', type: 'palette' }, { min: 0, max: 100 }) + ).toEqual([ + { color: 'blue', stop: 0 }, + { color: 'yellow', stop: 50 }, + ]); + }); + + it('should correctly map the new colorStop to the current data bound and minValue', () => { + expect( + getColorStops( + paletteRegistry, + [], + { name: 'mocked', type: 'palette', params: { rangeType: 'number' } }, + { min: 100, max: 1000 } + ) + ).toEqual([ + { color: 'blue', stop: 100 }, + { color: 'yellow', stop: 550 }, + ]); + }); + + it('should reverse the colors', () => { + expect( + getColorStops( + paletteRegistry, + [], + { name: 'mocked', type: 'palette', params: { reverse: true } }, + { min: 100, max: 1000 } + ) + ).toEqual([ + { color: 'yellow', stop: 0 }, + { color: 'blue', stop: 50 }, + ]); + }); +}); + describe('remapStopsByNewInterval', () => { it('should correctly remap the current palette from 0..1 to 0...100', () => { expect( diff --git a/x-pack/plugins/lens/public/shared_components/coloring/utils.ts b/x-pack/plugins/lens/public/shared_components/coloring/utils.ts index b2969565f539..413e3708e9c9 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/utils.ts +++ b/x-pack/plugins/lens/public/shared_components/coloring/utils.ts @@ -269,11 +269,10 @@ export function getColorStops( palettes: PaletteRegistry, colorStops: Required['stops'], activePalette: PaletteOutput, - dataBounds: { min: number; max: number }, - defaultPalette?: string + dataBounds: { min: number; max: number } ) { // just forward the current stops if custom - if (activePalette?.name === CUSTOM_PALETTE) { + if (activePalette?.name === CUSTOM_PALETTE && colorStops?.length) { return colorStops; } // for predefined palettes create some stops, then drop the last one. diff --git a/x-pack/test/functional/apps/lens/table.ts b/x-pack/test/functional/apps/lens/table.ts index da079c0976db..892534eec703 100644 --- a/x-pack/test/functional/apps/lens/table.ts +++ b/x-pack/test/functional/apps/lens/table.ts @@ -122,7 +122,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(styleObj['background-color']).to.be('rgb(235, 239, 245)'); }); + it('should keep the coloring consistent when changing mode', async () => { + // Change mode from percent to number + await testSubjects.click('lnsPalettePanel_dynamicColoring_rangeType_groups_number'); + await PageObjects.header.waitUntilLoadingHasFinished(); + // check that all remained the same + const styleObj = await PageObjects.lens.getDatatableCellStyle(0, 2); + expect(styleObj['background-color']).to.be('rgb(235, 239, 245)'); + }); + + it('should keep the coloring consistent when moving to custom palette from default', async () => { + await PageObjects.lens.changePaletteTo('custom'); + await PageObjects.header.waitUntilLoadingHasFinished(); + // check that all remained the same + const styleObj = await PageObjects.lens.getDatatableCellStyle(0, 2); + expect(styleObj['background-color']).to.be('rgb(235, 239, 245)'); + }); + it('tweak the color stops numeric value', async () => { + // restore default palette and percent mode + await PageObjects.lens.changePaletteTo('temperature'); + await testSubjects.click('lnsPalettePanel_dynamicColoring_rangeType_groups_percent'); + // now tweak the value await testSubjects.setValue('lnsPalettePanel_dynamicColoring_stop_value_0', '30', { clearWithKeyboard: true, }); From ed18699e387cf661581e484a764f8e008de50d8f Mon Sep 17 00:00:00 2001 From: Josh Dover <1813008+joshdover@users.noreply.github.com> Date: Fri, 3 Sep 2021 16:01:28 +0200 Subject: [PATCH 17/47] Handle bulkGet errors on package retrieval from ES storage (#111114) --- .../server/services/epm/archive/storage.ts | 22 +++++++++++++++++++ .../fleet/server/services/epm/packages/get.ts | 5 ++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/server/services/epm/archive/storage.ts b/x-pack/plugins/fleet/server/services/epm/archive/storage.ts index dde6459addcb..d3bc4afae622 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/storage.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/storage.ts @@ -23,6 +23,8 @@ import type { } from '../../../../common'; import { pkgToPkgKey } from '../registry'; +import { appContextService } from '../../app_context'; + import { getArchiveEntry, setArchiveEntry, setArchiveFilelist, setPackageInfo } from './index'; import type { ArchiveEntry } from './index'; import { parseAndVerifyPolicyTemplates, parseAndVerifyStreams } from './validation'; @@ -165,6 +167,7 @@ export const getEsPackage = async ( references: PackageAssetReference[], savedObjectsClient: SavedObjectsClientContract ) => { + const logger = appContextService.getLogger(); const pkgKey = pkgToPkgKey({ name: pkgName, version: pkgVersion }); const bulkRes = await savedObjectsClient.bulkGet( references.map((reference) => ({ @@ -172,8 +175,27 @@ export const getEsPackage = async ( fields: ['asset_path', 'data_utf8', 'data_base64'], })) ); + const errors = bulkRes.saved_objects.filter((so) => so.error || !so.attributes); const assets = bulkRes.saved_objects.map((so) => so.attributes); + if (errors.length) { + const resolvedErrors = errors.map((so) => + so.error + ? { type: so.type, id: so.id, error: so.error } + : !so.attributes + ? { type: so.type, id: so.id, error: { error: `No attributes retrieved` } } + : { type: so.type, id: so.id, error: { error: `Unknown` } } + ); + + logger.warn( + `Failed to retrieve ${pkgName}-${pkgVersion} package from ES storage. bulkGet failed for assets: ${JSON.stringify( + resolvedErrors + )}` + ); + + return undefined; + } + const paths: string[] = []; const entries: ArchiveEntry[] = assets.map(packageAssetToArchiveEntry); entries.forEach(({ path, buffer }) => { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index e493095bc4b3..0e23981b95fc 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -217,7 +217,10 @@ export async function getPackageFromSource(options: { installedPkg.package_assets, savedObjectsClient ); - logger.debug(`retrieved installed package ${pkgName}-${pkgVersion} from ES`); + + if (res) { + logger.debug(`retrieved installed package ${pkgName}-${pkgVersion} from ES`); + } } // for packages not in cache or package storage and installed from registry, check registry if (!res && pkgInstallSource === 'registry') { From 66cb058fa79a3f53d25695e49f5ce04b93862c3a Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Fri, 3 Sep 2021 07:22:14 -0700 Subject: [PATCH 18/47] Removes support for legacy exports (#110738) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__snapshots__/flyout.test.tsx.snap | 276 +------------- .../components/flyout.test.mocks.ts | 5 - .../objects_table/components/flyout.test.tsx | 235 +----------- .../objects_table/components/flyout.tsx | 353 +----------------- .../components/import_mode_control.test.tsx | 9 +- .../components/import_mode_control.tsx | 29 +- test/functional/apps/dashboard/bwc_import.ts | 4 +- test/functional/apps/dashboard/time_zones.ts | 4 +- .../apps/management/_import_objects.ts | 281 -------------- .../management/_mgmt_import_saved_objects.js | 2 +- ...import_index_patterns_multiple_exists.json | 26 -- .../management/exports/_import_objects.json | 19 - ...ort_objects_connected_to_saved_search.json | 20 - .../exports/_import_objects_exists.json | 19 - ...rt_objects_missing_all_index_patterns.json | 121 ------ .../_import_objects_multiple_exists.json | 36 -- .../exports/_import_objects_saved_search.json | 25 -- .../_import_objects_with_index_patterns.json | 31 -- .../exports/mgmt_import_objects.json | 37 -- .../translations/translations/ja-JP.json | 15 - .../translations/translations/zh-CN.json | 15 - 21 files changed, 32 insertions(+), 1530 deletions(-) delete mode 100644 test/functional/apps/management/exports/_import_index_patterns_multiple_exists.json delete mode 100644 test/functional/apps/management/exports/_import_objects.json delete mode 100644 test/functional/apps/management/exports/_import_objects_connected_to_saved_search.json delete mode 100644 test/functional/apps/management/exports/_import_objects_exists.json delete mode 100644 test/functional/apps/management/exports/_import_objects_missing_all_index_patterns.json delete mode 100644 test/functional/apps/management/exports/_import_objects_multiple_exists.json delete mode 100644 test/functional/apps/management/exports/_import_objects_saved_search.json delete mode 100644 test/functional/apps/management/exports/_import_objects_with_index_patterns.json delete mode 100644 test/functional/apps/management/exports/mgmt_import_objects.json diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap index bd97f2e6bffb..015c7068d72b 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap @@ -238,7 +238,6 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` "title": undefined, }, ], - "isLegacyFile": false, "loadingMessage": undefined, "status": "loading", "successfulImports": Array [], @@ -276,278 +275,6 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` } `; -exports[`Flyout legacy conflicts should allow conflict resolution 1`] = ` - - - -

- -

-
-
- - - - - } - > -

- -

-
- -
- - - - } - > -

- - - , - } - } - /> -

-
-
- -
- - - - - - - - - - - - - - -
-`; - -exports[`Flyout legacy conflicts should handle errors 2`] = ` -Array [ - - } - > -

- -

-
, - - } - > -

- - - , - } - } - /> -

-
, - - } - > -

- The file could not be processed due to error: "foobar" -

-
, -] -`; - exports[`Flyout should render import step 1`] = `
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.mocks.ts b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.mocks.ts index fc8558fa82c2..7b716e1b813c 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.mocks.ts +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.mocks.ts @@ -16,11 +16,6 @@ jest.doMock('../../../lib/resolve_import_errors', () => ({ resolveImportErrors: resolveImportErrorsMock, })); -export const importLegacyFileMock = jest.fn(); -jest.doMock('../../../lib/import_legacy_file', () => ({ - importLegacyFile: importLegacyFileMock, -})); - export const resolveSavedObjectsMock = jest.fn(); export const resolveSavedSearchesMock = jest.fn(); export const resolveIndexPatternConflictsMock = jest.fn(); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx index a1eb94ab55cf..28190e6bd872 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx @@ -6,15 +6,7 @@ * Side Public License, v 1. */ -import { - importFileMock, - importLegacyFileMock, - resolveImportErrorsMock, - resolveIndexPatternConflictsMock, - resolveSavedObjectsMock, - resolveSavedSearchesMock, - saveObjectsMock, -} from './flyout.test.mocks'; +import { importFileMock, resolveImportErrorsMock } from './flyout.test.mocks'; import React from 'react'; import { shallowWithI18nProvider } from '@kbn/test/jest'; @@ -28,10 +20,6 @@ const mockFile = ({ name: 'foo.ndjson', path: '/home/foo.ndjson', } as unknown) as File; -const legacyMockFile = ({ - name: 'foo.json', - path: '/home/foo.json', -} as unknown) as File; describe('Flyout', () => { let defaultProps: FlyoutProps; @@ -107,31 +95,6 @@ describe('Flyout', () => { expect(component.state('file')).toBe(undefined); }); - it('should handle invalid files', async () => { - const component = shallowRender(defaultProps); - - // Ensure all promises resolve - await new Promise((resolve) => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - importLegacyFileMock.mockImplementation(() => { - throw new Error('foobar'); - }); - - await component.instance().legacyImport(); - expect(component.state('error')).toBe('The file could not be processed.'); - - importLegacyFileMock.mockImplementation(() => ({ - invalid: true, - })); - - await component.instance().legacyImport(); - expect(component.state('error')).toBe( - 'Saved objects file format is invalid and cannot be imported.' - ); - }); - describe('conflicts', () => { beforeEach(() => { importFileMock.mockImplementation(() => ({ @@ -169,7 +132,7 @@ describe('Flyout', () => { // Ensure the state changes are reflected component.update(); - component.setState({ file: mockFile, isLegacyFile: false }); + component.setState({ file: mockFile }); await component.instance().import(); expect(importFileMock).toHaveBeenCalledWith(defaultProps.http, mockFile, { @@ -207,7 +170,7 @@ describe('Flyout', () => { // Ensure the state changes are reflected component.update(); - component.setState({ file: mockFile, isLegacyFile: false }); + component.setState({ file: mockFile }); await component.instance().import(); // Ensure it looks right @@ -250,7 +213,7 @@ describe('Flyout', () => { successfulImports, })); - component.setState({ file: mockFile, isLegacyFile: false }); + component.setState({ file: mockFile }); // Go through the import flow await component.instance().import(); @@ -267,194 +230,4 @@ describe('Flyout', () => { expect(cancelButton.prop('disabled')).toBe(true); }); }); - - describe('legacy conflicts', () => { - const mockData = [ - { - _id: '1', - _type: 'search', - }, - { - _id: '2', - _type: 'index-pattern', - }, - { - _id: '3', - _type: 'invalid', - }, - ]; - - const mockConflictedIndexPatterns = [ - { - doc: { - _type: 'index-pattern', - _id: '1', - _source: { - title: 'MyIndexPattern*', - }, - }, - obj: { - searchSource: { - getOwnField: (field: string) => { - if (field === 'index') { - return 'MyIndexPattern*'; - } - if (field === 'filter') { - return [{ meta: { index: 'filterIndex' } }]; - } - }, - }, - _serialize: () => { - return { references: [{ id: 'MyIndexPattern*' }, { id: 'filterIndex' }] }; - }, - }, - }, - ]; - - const mockConflictedSavedObjectsLinkedToSavedSearches = [2]; - const mockConflictedSearchDocs = [3]; - - beforeEach(() => { - importLegacyFileMock.mockImplementation(() => mockData); - resolveSavedObjectsMock.mockImplementation(() => ({ - conflictedIndexPatterns: mockConflictedIndexPatterns, - conflictedSavedObjectsLinkedToSavedSearches: mockConflictedSavedObjectsLinkedToSavedSearches, - conflictedSearchDocs: mockConflictedSearchDocs, - importedObjectCount: 2, - confirmModalPromise: () => {}, - })); - }); - - it('should figure out unmatchedReferences', async () => { - const component = shallowRender(defaultProps); - - // Ensure all promises resolve - await new Promise((resolve) => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - component.setState({ file: legacyMockFile, isLegacyFile: true }); - await component.instance().legacyImport(); - - expect(importLegacyFileMock).toHaveBeenCalledWith(legacyMockFile); - // Remove the last element from data since it should be filtered out - expect(resolveSavedObjectsMock).toHaveBeenCalledWith( - mockData.slice(0, 2).map((doc) => ({ ...doc, _migrationVersion: {} })), - true, - defaultProps.serviceRegistry.all().map((s) => s.service), - defaultProps.indexPatterns, - defaultProps.overlays.openConfirm - ); - - expect(component.state()).toMatchObject({ - conflictedIndexPatterns: mockConflictedIndexPatterns, - conflictedSavedObjectsLinkedToSavedSearches: mockConflictedSavedObjectsLinkedToSavedSearches, - conflictedSearchDocs: mockConflictedSearchDocs, - importCount: 2, - status: 'idle', - error: undefined, - unmatchedReferences: [ - { - existingIndexPatternId: 'MyIndexPattern*', - newIndexPatternId: undefined, - list: [ - { - id: 'MyIndexPattern*', - title: 'MyIndexPattern*', - type: 'index-pattern', - }, - ], - }, - { - existingIndexPatternId: 'filterIndex', - list: [ - { - id: 'filterIndex', - title: 'MyIndexPattern*', - type: 'index-pattern', - }, - ], - newIndexPatternId: undefined, - }, - ], - }); - }); - - it('should allow conflict resolution', async () => { - const component = shallowRender(defaultProps); - - // Ensure all promises resolve - await new Promise((resolve) => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - component.setState({ file: legacyMockFile, isLegacyFile: true }); - await component.instance().legacyImport(); - - // Ensure it looks right - component.update(); - expect(component).toMatchSnapshot(); - - // Ensure we can change the resolution - component.instance().onIndexChanged('MyIndexPattern*', { target: { value: '2' } }); - expect(component.state('unmatchedReferences')![0].newIndexPatternId).toBe('2'); - - // Let's resolve now - await component - .find('EuiButton[data-test-subj="importSavedObjectsConfirmBtn"]') - .simulate('click'); - // Ensure all promises resolve - await new Promise((resolve) => process.nextTick(resolve)); - expect(resolveIndexPatternConflictsMock).toHaveBeenCalledWith( - component.instance().resolutions, - mockConflictedIndexPatterns, - true, - { - search: defaultProps.search, - indexPatterns: defaultProps.indexPatterns, - } - ); - expect(saveObjectsMock).toHaveBeenCalledWith( - mockConflictedSavedObjectsLinkedToSavedSearches, - true - ); - expect(resolveSavedSearchesMock).toHaveBeenCalledWith( - mockConflictedSearchDocs, - defaultProps.serviceRegistry.all().map((s) => s.service), - defaultProps.indexPatterns, - true - ); - }); - - it('should handle errors', async () => { - const component = shallowRender(defaultProps); - - // Ensure all promises resolve - await new Promise((resolve) => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - resolveIndexPatternConflictsMock.mockImplementation(() => { - throw new Error('foobar'); - }); - - component.setState({ file: legacyMockFile, isLegacyFile: true }); - - // Go through the import flow - await component.instance().legacyImport(); - component.update(); - // Set a resolution - component.instance().onIndexChanged('MyIndexPattern*', { target: { value: '2' } }); - await component - .find('EuiButton[data-test-subj="importSavedObjectsConfirmBtn"]') - .simulate('click'); - // Ensure all promises resolve - await new Promise((resolve) => process.nextTick(resolve)); - - expect(component.state('error')).toMatchInlineSnapshot( - `"The file could not be processed due to error: \\"foobar\\""` - ); - expect(component.find('EuiFlyoutBody EuiCallOut')).toMatchSnapshot(); - }); - }); }); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx index 8f4940ffb05c..aca229b9a70e 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx @@ -7,7 +7,7 @@ */ import React, { Component, Fragment, ReactNode } from 'react'; -import { take, get as getField } from 'lodash'; +import { take } from 'lodash'; import { EuiFlyout, EuiFlyoutBody, @@ -39,18 +39,10 @@ import { } from '../../../../../data/public'; import { importFile, - importLegacyFile, resolveImportErrors, - logLegacyImport, processImportResponse, ProcessedImportResponse, } from '../../../lib'; -import { - resolveSavedObjects, - resolveSavedSearches, - resolveIndexPatternConflicts, - saveObjects, -} from '../../../lib/resolve_saved_objects'; import { ISavedObjectsManagementServiceRegistry } from '../../../services'; import { FailedImportConflict, RetryDecision } from '../../../lib/resolve_import_errors'; import { OverwriteModal } from './overwrite_modal'; @@ -89,7 +81,6 @@ export interface FlyoutState { indexPatterns?: IndexPattern[]; importMode: ImportMode; loadingMessage?: string; - isLegacyFile: boolean; status: string; } @@ -129,7 +120,6 @@ export class Flyout extends Component { indexPatterns: undefined, importMode: { createNewCopies: CREATE_NEW_COPIES_DEFAULT, overwrite: OVERWRITE_ALL_DEFAULT }, loadingMessage: undefined, - isLegacyFile: false, status: 'idle', }; } @@ -152,14 +142,11 @@ export class Flyout extends Component { setImportFile = (files: FileList | null) => { if (!files || !files[0]) { - this.setState({ file: undefined, isLegacyFile: false }); + this.setState({ file: undefined }); return; } const file = files[0]; - this.setState({ - file, - isLegacyFile: /\.json$/i.test(file.name) || file.type === 'application/json', - }); + this.setState({ file }); }; /** @@ -246,103 +233,6 @@ export class Flyout extends Component { } }; - legacyImport = async () => { - const { serviceRegistry, indexPatterns, overlays, http, allowedTypes } = this.props; - const { file, importMode } = this.state; - - this.setState({ status: 'loading', error: undefined }); - - // Log warning on server, don't wait for response - logLegacyImport(http); - - let contents; - try { - contents = await importLegacyFile(file!); - } catch (e) { - this.setState({ - status: 'error', - error: i18n.translate( - 'savedObjectsManagement.objectsTable.flyout.importLegacyFileErrorMessage', - { defaultMessage: 'The file could not be processed.' } - ), - }); - return; - } - - if (!Array.isArray(contents)) { - this.setState({ - status: 'error', - error: i18n.translate( - 'savedObjectsManagement.objectsTable.flyout.invalidFormatOfImportedFileErrorMessage', - { defaultMessage: 'Saved objects file format is invalid and cannot be imported.' } - ), - }); - return; - } - - contents = contents - .filter((content) => allowedTypes.includes(content._type)) - .map((doc) => ({ - ...doc, - // The server assumes that documents with no migrationVersion are up to date. - // That assumption enables Kibana and other API consumers to not have to build - // up migrationVersion prior to creating new objects. But it means that imports - // need to set migrationVersion to something other than undefined, so that imported - // docs are not seen as automatically up-to-date. - _migrationVersion: doc._migrationVersion || {}, - })); - - const { - conflictedIndexPatterns, - conflictedSavedObjectsLinkedToSavedSearches, - conflictedSearchDocs, - importedObjectCount, - failedImports, - } = await resolveSavedObjects( - contents, - importMode.overwrite, - serviceRegistry.all().map((e) => e.service), - indexPatterns, - overlays.openConfirm - ); - - const byId: Record = {}; - conflictedIndexPatterns - .map(({ doc, obj }) => { - return { doc, obj: obj._serialize() }; - }) - .forEach(({ doc, obj }) => - obj.references.forEach((ref: Record) => { - byId[ref.id] = byId[ref.id] != null ? byId[ref.id].concat({ doc, obj }) : [{ doc, obj }]; - }) - ); - const unmatchedReferences = Object.entries(byId).reduce( - (accum, [existingIndexPatternId, list]) => { - accum.push({ - existingIndexPatternId, - newIndexPatternId: undefined, - list: list.map(({ doc }) => ({ - id: existingIndexPatternId, - type: doc._type, - title: doc._source.title, - })), - }); - return accum; - }, - [] as any[] - ); - - this.setState({ - conflictedIndexPatterns, - conflictedSavedObjectsLinkedToSavedSearches, - conflictedSearchDocs, - failedImports, - unmatchedReferences, - importCount: importedObjectCount, - status: unmatchedReferences.length === 0 ? 'success' : 'idle', - }); - }; - public get hasUnmatchedReferences() { return this.state.unmatchedReferences && this.state.unmatchedReferences.length > 0; } @@ -362,89 +252,6 @@ export class Flyout extends Component { ); } - confirmLegacyImport = async () => { - const { - conflictedIndexPatterns, - importMode, - conflictedSavedObjectsLinkedToSavedSearches, - conflictedSearchDocs, - failedImports, - } = this.state; - - const { serviceRegistry, indexPatterns, search } = this.props; - - this.setState({ - error: undefined, - status: 'loading', - loadingMessage: undefined, - }); - - let importCount = this.state.importCount; - - if (this.hasUnmatchedReferences) { - try { - const resolutions = this.resolutions; - - // Do not Promise.all these calls as the order matters - this.setState({ - loadingMessage: i18n.translate( - 'savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.resolvingConflictsLoadingMessage', - { defaultMessage: 'Resolving conflicts…' } - ), - }); - if (resolutions.length) { - importCount += await resolveIndexPatternConflicts( - resolutions, - conflictedIndexPatterns!, - importMode.overwrite, - { indexPatterns, search } - ); - } - this.setState({ - loadingMessage: i18n.translate( - 'savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.savingConflictsLoadingMessage', - { defaultMessage: 'Saving conflicts…' } - ), - }); - importCount += await saveObjects( - conflictedSavedObjectsLinkedToSavedSearches!, - importMode.overwrite - ); - this.setState({ - loadingMessage: i18n.translate( - 'savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.savedSearchAreLinkedProperlyLoadingMessage', - { defaultMessage: 'Ensure saved searches are linked properly…' } - ), - }); - importCount += await resolveSavedSearches( - conflictedSearchDocs!, - serviceRegistry.all().map((e) => e.service), - indexPatterns, - importMode.overwrite - ); - this.setState({ - loadingMessage: i18n.translate( - 'savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.retryingFailedObjectsLoadingMessage', - { defaultMessage: 'Retrying failed objects…' } - ), - }); - importCount += await saveObjects( - failedImports!.map(({ obj }) => obj) as any[], - importMode.overwrite - ); - } catch (e) { - this.setState({ - status: 'error', - error: getErrorMessage(e), - loadingMessage: undefined, - }); - return; - } - } - - this.setState({ status: 'success', importCount }); - }; - onIndexChanged = (id: string, e: any) => { const value = e.target.value; this.setState((state) => { @@ -613,10 +420,8 @@ export class Flyout extends Component { const { status, loadingMessage, - importCount, failedImports = [], successfulImports = [], - isLegacyFile, importMode, importWarnings, } = this.state; @@ -635,7 +440,8 @@ export class Flyout extends Component { ); } - if (!isLegacyFile && status === 'success') { + // Import summary for completed import + if (status === 'success') { return ( { ); } - // Import summary for failed legacy import - if (failedImports.length && !this.hasUnmatchedReferences) { - return ( - - } - color="warning" - iconType="help" - > -

- -

-

- {failedImports - .map(({ error, obj }) => { - if (error.type === 'missing_references') { - return error.references.map((reference) => { - return i18n.translate( - 'savedObjectsManagement.objectsTable.flyout.importFailedMissingReference', - { - defaultMessage: '{type} [id={id}] could not locate {refType} [id={refId}]', - values: { - id: obj.id, - type: obj.type, - refId: reference.id, - refType: reference.type, - }, - } - ); - }); - } else if (error.type === 'unsupported_type') { - return i18n.translate( - 'savedObjectsManagement.objectsTable.flyout.importFailedUnsupportedType', - { - defaultMessage: '{type} [id={id}] unsupported type', - values: { - id: obj.id, - type: obj.type, - }, - } - ); - } - return getField(error, 'body.message', (error as any).message ?? ''); - }) - .join(' ')} -

-
- ); - } - - // Import summary for completed legacy import - if (status === 'success') { - if (importCount === 0) { - return ( - - } - color="primary" - /> - ); - } - - return ( - - } - color="success" - iconType="check" - > -

- -

-
- ); - } - + // Failed imports if (this.hasUnmatchedReferences) { return this.renderUnmatchedReferences(); } @@ -768,7 +473,7 @@ export class Flyout extends Component { } > { this.changeImportMode(newValues)} /> @@ -791,7 +495,7 @@ export class Flyout extends Component { } renderFooter() { - const { isLegacyFile, status } = this.state; + const { status } = this.state; const { done, close } = this.props; let confirmButton; @@ -808,7 +512,7 @@ export class Flyout extends Component { } else if (this.hasUnmatchedReferences) { confirmButton = ( { } else { confirmButton = ( { { return null; } - let legacyFileWarning; - if (this.state.isLegacyFile) { - legacyFileWarning = ( - <> - - } - color="warning" - iconType="help" - > -

- -

-
- - - ); - } - let indexPatternConflictsWarning; if (this.hasUnmatchedReferences) { indexPatternConflictsWarning = ( @@ -925,18 +602,12 @@ export class Flyout extends Component { ); } - if (!legacyFileWarning && !indexPatternConflictsWarning) { + if (!indexPatternConflictsWarning) { return null; } return ( - {legacyFileWarning && ( - - - {legacyFileWarning} - - )} {indexPatternConflictsWarning && ( diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.test.tsx index 2ece42123863..fbf50e0ee0c8 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.test.tsx @@ -32,14 +32,9 @@ describe('ImportModeControl', () => { jest.resetAllMocks(); }); - const props: ImportModeControlProps = { initialValues, updateSelection, isLegacyFile: false }; + const props: ImportModeControlProps = { initialValues, updateSelection }; - it('returns partial import mode control when used with a legacy file', async () => { - const wrapper = shallowWithI18nProvider(); - expect(wrapper.find('EuiFormFieldset')).toHaveLength(0); - }); - - it('returns full import mode control when used without a legacy file', async () => { + it('returns full import mode control', async () => { const wrapper = shallowWithI18nProvider(); expect(wrapper.find('EuiFormFieldset')).toHaveLength(1); }); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.tsx index ee36ef67ee96..6641e53be8f5 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.tsx @@ -22,7 +22,6 @@ import { i18n } from '@kbn/i18n'; export interface ImportModeControlProps { initialValues: ImportMode; - isLegacyFile: boolean; updateSelection: (result: ImportMode) => void; } @@ -87,11 +86,7 @@ const createLabel = ({ text, tooltip }: { text: string; tooltip: string }) => (
); -export const ImportModeControl = ({ - initialValues, - isLegacyFile, - updateSelection, -}: ImportModeControlProps) => { +export const ImportModeControl = ({ initialValues, updateSelection }: ImportModeControlProps) => { const [createNewCopies, setCreateNewCopies] = useState(initialValues.createNewCopies); const [overwrite, setOverwrite] = useState(initialValues.overwrite); @@ -104,20 +99,6 @@ export const ImportModeControl = ({ updateSelection({ createNewCopies, overwrite, ...partial }); }; - const overwriteRadio = ( - onChange({ overwrite: id === overwriteEnabled.id })} - disabled={createNewCopies && !isLegacyFile} - data-test-subj={'savedObjectsManagement-importModeControl-overwriteRadioGroup'} - /> - ); - - if (isLegacyFile) { - return overwriteRadio; - } - return ( onChange({ createNewCopies: false })} > - {overwriteRadio} + onChange({ overwrite: id === overwriteEnabled.id })} + disabled={createNewCopies} + data-test-subj={'savedObjectsManagement-importModeControl-overwriteRadioGroup'} + /> diff --git a/test/functional/apps/dashboard/bwc_import.ts b/test/functional/apps/dashboard/bwc_import.ts index 03f1f126338f..ebb9d2b99ffa 100644 --- a/test/functional/apps/dashboard/bwc_import.ts +++ b/test/functional/apps/dashboard/bwc_import.ts @@ -12,8 +12,8 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['dashboard', 'header', 'settings', 'savedObjects', 'common']); const dashboardExpect = getService('dashboardExpect'); - - describe('bwc import', function describeIndexTests() { + // Legacy imports are no longer supported https://github.com/elastic/kibana/issues/103921 + describe.skip('bwc import', function describeIndexTests() { before(async function () { await PageObjects.dashboard.initTests(); await PageObjects.settings.navigateTo(); diff --git a/test/functional/apps/dashboard/time_zones.ts b/test/functional/apps/dashboard/time_zones.ts index e5c532537b6f..f60792b3f292 100644 --- a/test/functional/apps/dashboard/time_zones.ts +++ b/test/functional/apps/dashboard/time_zones.ts @@ -22,8 +22,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'common', 'savedObjects', ]); - - describe('dashboard time zones', function () { + // Legacy imports are no longer supported https://github.com/elastic/kibana/issues/103921 + describe.skip('dashboard time zones', function () { this.tags('includeFirefox'); before(async () => { diff --git a/test/functional/apps/management/_import_objects.ts b/test/functional/apps/management/_import_objects.ts index 6ef0bfd5a09e..81350b3542c4 100644 --- a/test/functional/apps/management/_import_objects.ts +++ b/test/functional/apps/management/_import_objects.ts @@ -11,8 +11,6 @@ import path from 'path'; import { keyBy } from 'lodash'; import { FtrProviderContext } from '../../ftr_provider_context'; -const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - function uniq(input: T[]): T[] { return [...new Set(input)]; } @@ -210,284 +208,5 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(isSavedObjectImported).to.be(true); }); }); - - describe('.json file', () => { - beforeEach(async function () { - await esArchiver.load('test/functional/fixtures/es_archiver/saved_objects_imports'); - await kibanaServer.uiSettings.replace({}); - await PageObjects.settings.navigateTo(); - await PageObjects.settings.clickKibanaSavedObjects(); - }); - - afterEach(async function () { - await esArchiver.unload('test/functional/fixtures/es_archiver/saved_objects_imports'); - }); - - it('should import saved objects', async function () { - await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects.json') - ); - await PageObjects.savedObjects.checkImportSucceeded(); - await PageObjects.savedObjects.clickImportDone(); - const objects = await PageObjects.savedObjects.getRowTitles(); - const isSavedObjectImported = objects.includes('Log Agents'); - expect(isSavedObjectImported).to.be(true); - }); - - it('should provide dialog to allow the importing of saved objects with index pattern conflicts', async function () { - await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects-conflicts.json') - ); - await PageObjects.savedObjects.checkImportLegacyWarning(); - await PageObjects.savedObjects.checkImportConflictsWarning(); - await PageObjects.settings.associateIndexPattern( - 'd1e4c910-a2e6-11e7-bb30-233be9be6a15', - 'logstash-*' - ); - await PageObjects.savedObjects.clickConfirmChanges(); - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.savedObjects.clickImportDone(); - const objects = await PageObjects.savedObjects.getRowTitles(); - const isSavedObjectImported = objects.includes('saved object with index pattern conflict'); - expect(isSavedObjectImported).to.be(true); - }); - - it('should allow the user to override duplicate saved objects', async function () { - // This data has already been loaded by the "visualize" esArchive. We'll load it again - // so that we can override the existing visualization. - await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects_exists.json'), - false - ); - - await PageObjects.savedObjects.checkImportLegacyWarning(); - await PageObjects.savedObjects.checkImportConflictsWarning(); - await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*'); - await PageObjects.savedObjects.clickConfirmChanges(); - - // Override the visualization. - await PageObjects.common.clickConfirmOnModal(); - - const isSuccessful = await testSubjects.exists('importSavedObjectsSuccess'); - expect(isSuccessful).to.be(true); - }); - - it('should allow the user to cancel overriding duplicate saved objects', async function () { - // This data has already been loaded by the "visualize" esArchive. We'll load it again - // so that we can be prompted to override the existing visualization. - await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects_exists.json'), - false - ); - - await PageObjects.savedObjects.checkImportLegacyWarning(); - await PageObjects.savedObjects.checkImportConflictsWarning(); - await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*'); - await PageObjects.savedObjects.clickConfirmChanges(); - - // *Don't* override the visualization. - await PageObjects.common.clickCancelOnModal(); - - const isSuccessful = await testSubjects.exists('importSavedObjectsSuccessNoneImported'); - expect(isSuccessful).to.be(true); - }); - - it('should allow the user to confirm overriding multiple duplicate saved objects', async function () { - // This data has already been loaded by the "visualize" esArchive. We'll load it again - // so that we can override the existing visualization. - await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects_multiple_exists.json'), - false - ); - - await PageObjects.savedObjects.checkImportLegacyWarning(); - await PageObjects.savedObjects.checkImportConflictsWarning(); - - await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*'); - await PageObjects.savedObjects.clickConfirmChanges(); - - // Override the visualizations. - await PageObjects.common.clickConfirmOnModal(false); - // as the second confirm can pop instantly, we can't wait for it to be hidden - // with is why we call clickConfirmOnModal with ensureHidden: false in previous statement - // but as the initial popin can take a few ms before fading, we need to wait a little - // to avoid clicking twice on the same modal. - await delay(1000); - await PageObjects.common.clickConfirmOnModal(true); - - const isSuccessful = await testSubjects.exists('importSavedObjectsSuccess'); - expect(isSuccessful).to.be(true); - }); - - it('should allow the user to confirm overriding multiple duplicate index patterns', async function () { - // This data has already been loaded by the "visualize" esArchive. We'll load it again - // so that we can override the existing visualization. - await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_index_patterns_multiple_exists.json'), - false - ); - - // Override the index patterns. - await PageObjects.common.clickConfirmOnModal(false); - // as the second confirm can pop instantly, we can't wait for it to be hidden - // with is why we call clickConfirmOnModal with ensureHidden: false in previous statement - // but as the initial popin can take a few ms before fading, we need to wait a little - // to avoid clicking twice on the same modal. - await delay(1000); - await PageObjects.common.clickConfirmOnModal(true); - - const isSuccessful = await testSubjects.exists('importSavedObjectsSuccess'); - expect(isSuccessful).to.be(true); - }); - - it('should import saved objects linked to saved searches', async function () { - await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects_saved_search.json') - ); - await PageObjects.savedObjects.checkImportSucceeded(); - await PageObjects.savedObjects.clickImportDone(); - - await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json') - ); - await PageObjects.savedObjects.checkImportSucceeded(); - await PageObjects.savedObjects.clickImportDone(); - - const objects = await PageObjects.savedObjects.getRowTitles(); - const isSavedObjectImported = objects.includes('saved object connected to saved search'); - expect(isSavedObjectImported).to.be(true); - }); - - it('should not import saved objects linked to saved searches when saved search does not exist', async function () { - await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json') - ); - await PageObjects.savedObjects.checkImportFailedWarning(); - await PageObjects.savedObjects.clickImportDone(); - - const objects = await PageObjects.savedObjects.getRowTitles(); - const isSavedObjectImported = objects.includes('saved object connected to saved search'); - expect(isSavedObjectImported).to.be(false); - }); - - it('should not import saved objects linked to saved searches when saved search index pattern does not exist', async function () { - // First, import the saved search - await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects_saved_search.json') - ); - // Wait for all the saves to happen - await PageObjects.savedObjects.checkImportSucceeded(); - await PageObjects.savedObjects.clickImportDone(); - - // Second, we need to delete the index pattern - await PageObjects.savedObjects.clickCheckboxByTitle('logstash-*'); - await PageObjects.savedObjects.clickDelete(); - - // Last, import a saved object connected to the saved search - // This should NOT show the conflicts - await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json') - ); - // Wait for all the saves to happen - await PageObjects.savedObjects.checkNoneImported(); - await PageObjects.savedObjects.clickImportDone(); - - const objects = await PageObjects.savedObjects.getRowTitles(); - const isSavedObjectImported = objects.includes('saved object connected to saved search'); - expect(isSavedObjectImported).to.be(false); - }); - - it('should import saved objects with index patterns when index patterns already exists', async () => { - // First, import the objects - await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json') - ); - await PageObjects.savedObjects.clickImportDone(); - - const objects = await PageObjects.savedObjects.getRowTitles(); - const isSavedObjectImported = objects.includes('saved object imported with index pattern'); - expect(isSavedObjectImported).to.be(true); - }); - - it('should preserve index patterns selection when switching between pages', async () => { - await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects_missing_all_index_patterns.json') - ); - - await PageObjects.savedObjects.setOverriddenIndexPatternValue( - 'missing-index-pattern-1', - 'index-pattern-test-1' - ); - - const flyout = await testSubjects.find('importSavedObjectsFlyout'); - - await (await flyout.findByTestSubject('pagination-button-next')).click(); - - await PageObjects.savedObjects.setOverriddenIndexPatternValue( - 'missing-index-pattern-7', - 'index-pattern-test-2' - ); - - await (await flyout.findByTestSubject('pagination-button-previous')).click(); - - const selectedIdForMissingIndexPattern1 = await testSubjects.getAttribute( - 'managementChangeIndexSelection-missing-index-pattern-1', - 'value' - ); - - expect(selectedIdForMissingIndexPattern1).to.eql('f1e4c910-a2e6-11e7-bb30-233be9be6a20'); - - await (await flyout.findByTestSubject('pagination-button-next')).click(); - - const selectedIdForMissingIndexPattern7 = await testSubjects.getAttribute( - 'managementChangeIndexSelection-missing-index-pattern-7', - 'value' - ); - - expect(selectedIdForMissingIndexPattern7).to.eql('f1e4c910-a2e6-11e7-bb30-233be9be6a87'); - }); - - it('should display an explicit error message when importing object from a higher Kibana version', async () => { - await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_higher_version.ndjson') - ); - - await PageObjects.savedObjects.checkImportError(); - - const errorText = await PageObjects.savedObjects.getImportErrorText(); - - expect(errorText).to.contain( - `has property "visualization" which belongs to a more recent version of Kibana [9.15.82]` - ); - }); - - describe('when bigger than savedObjects.maxImportPayloadBytes (not Cloud)', function () { - // see --savedObjects.maxImportPayloadBytes in config file - this.tags(['skipCloud']); - it('should display an explicit error message when importing a file bigger than allowed', async () => { - await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_too_big.ndjson') - ); - - await PageObjects.savedObjects.checkImportError(); - - const errorText = await PageObjects.savedObjects.getImportErrorText(); - - expect(errorText).to.contain(`Payload content length greater than maximum allowed`); - }); - }); - - it('should display an explicit error message when importing an invalid file', async () => { - await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_invalid_format.ndjson') - ); - - await PageObjects.savedObjects.checkImportError(); - - const errorText = await PageObjects.savedObjects.getImportErrorText(); - - expect(errorText).to.contain(`Unexpected token T in JSON at position 0`); - }); - }); }); } diff --git a/test/functional/apps/management/_mgmt_import_saved_objects.js b/test/functional/apps/management/_mgmt_import_saved_objects.js index 84e57a798c00..b7bca79a2594 100644 --- a/test/functional/apps/management/_mgmt_import_saved_objects.js +++ b/test/functional/apps/management/_mgmt_import_saved_objects.js @@ -30,7 +30,7 @@ export default function ({ getService, getPageObjects }) { it('should import saved objects mgmt', async function () { await PageObjects.settings.clickKibanaSavedObjects(); await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', 'mgmt_import_objects.json') + path.join(__dirname, 'exports', 'mgmt_import_objects.ndjson') ); await PageObjects.settings.associateIndexPattern( '4c3f3c30-ac94-11e8-a651-614b2788174a', diff --git a/test/functional/apps/management/exports/_import_index_patterns_multiple_exists.json b/test/functional/apps/management/exports/_import_index_patterns_multiple_exists.json deleted file mode 100644 index 2eb64b1c7ca9..000000000000 --- a/test/functional/apps/management/exports/_import_index_patterns_multiple_exists.json +++ /dev/null @@ -1,26 +0,0 @@ -[ - { - "_id": "f1e4c910-a2e6-11e7-bb30-233be9be6a20", - "_type": "index-pattern", - "_source": { - "title": "index-pattern-test-1", - "timeFieldName": "@timestamp", - "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"expression script\",\"type\":\"number\",\"count\":0,\"scripted\":true,\"script\":\"doc['bytes'].value\",\"lang\":\"expression\",\"indexed\":true,\"analyzed\":false,\"doc_values\":false}]" - }, - "_meta": { - "savedObjectVersion": 1 - } - }, - { - "_id": "f1e4c910-a2e6-11e7-bb30-233be9be6a87", - "_type": "index-pattern", - "_source": { - "title": "index-pattern-test-2", - "timeFieldName": "@timestamp", - "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"expression script\",\"type\":\"number\",\"count\":0,\"scripted\":true,\"script\":\"doc['bytes'].value\",\"lang\":\"expression\",\"indexed\":true,\"analyzed\":false,\"doc_values\":false}]" - }, - "_meta": { - "savedObjectVersion": 1 - } - } -] diff --git a/test/functional/apps/management/exports/_import_objects.json b/test/functional/apps/management/exports/_import_objects.json deleted file mode 100644 index 48015d64133f..000000000000 --- a/test/functional/apps/management/exports/_import_objects.json +++ /dev/null @@ -1,19 +0,0 @@ -[ - { - "_id": "082f1d60-a2e7-11e7-bb30-233be9be6a15", - "_type": "visualization", - "_source": { - "title": "Log Agents", - "visState": "{\"title\":\"Log Agents\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{\"text\":\"agent.raw: Descending\"}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"agent.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}", - "uiStateJSON": "{}", - "description": "", - "version": 1, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"f1e4c910-a2e6-11e7-bb30-233be9be6a15\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" - } - }, - "_meta": { - "savedObjectVersion": 2 - } - } -] diff --git a/test/functional/apps/management/exports/_import_objects_connected_to_saved_search.json b/test/functional/apps/management/exports/_import_objects_connected_to_saved_search.json deleted file mode 100644 index 7088e1ab34b6..000000000000 --- a/test/functional/apps/management/exports/_import_objects_connected_to_saved_search.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "_id": "saved_object_connected_to_saved_search", - "_type": "visualization", - "_source": { - "title": "saved object connected to saved search", - "visState": "{\"title\":\"PHP Viz\",\"type\":\"horizontal_bar\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":200},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":75,\"filter\":true,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"histogram\",\"mode\":\"normal\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}", - "uiStateJSON": "{}", - "description": "", - "savedSearchId": "c45e6c50-ba72-11e7-a8f9-ad70f02e633d", - "version": 1, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}" - } - }, - "_meta": { - "savedObjectVersion": 2 - } - } -] diff --git a/test/functional/apps/management/exports/_import_objects_exists.json b/test/functional/apps/management/exports/_import_objects_exists.json deleted file mode 100644 index 5356d1fdf647..000000000000 --- a/test/functional/apps/management/exports/_import_objects_exists.json +++ /dev/null @@ -1,19 +0,0 @@ -[ - { - "_id": "Shared-Item-Visualization-AreaChart", - "_type": "visualization", - "_source": { - "title": "Shared-Item Visualization AreaChart", - "visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}", - "uiStateJSON": "{}", - "description": "AreaChart", - "version": 1, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}" - } - }, - "_meta": { - "savedObjectVersion": 2 - } - } -] diff --git a/test/functional/apps/management/exports/_import_objects_missing_all_index_patterns.json b/test/functional/apps/management/exports/_import_objects_missing_all_index_patterns.json deleted file mode 100644 index 45572b0bf34f..000000000000 --- a/test/functional/apps/management/exports/_import_objects_missing_all_index_patterns.json +++ /dev/null @@ -1,121 +0,0 @@ -[ - { - "_id": "test-vis-1", - "_type": "visualization", - "_source": { - "title": "Test VIS 1", - "visState": "{\"title\":\"test vis 1\",\"type\":\"histogram\"}", - "uiStateJSON": "{}", - "description": "", - "version": 1, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"missing-index-pattern-1\",\"query\":{}}" - } - }, - "_meta": { - "savedObjectVersion": 2 - } - }, - { - "_id": "test-vis-2", - "_type": "visualization", - "_source": { - "title": "Test VIS 2", - "visState": "{\"title\":\"test vis 2\",\"type\":\"histogram\"}", - "uiStateJSON": "{}", - "description": "", - "version": 1, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"missing-index-pattern-2\",\"query\":{}}" - } - }, - "_meta": { - "savedObjectVersion": 2 - } - }, - { - "_id": "test-vis-3", - "_type": "visualization", - "_source": { - "title": "Test VIS 3", - "visState": "{\"title\":\"test vis 3\",\"type\":\"histogram\"}", - "uiStateJSON": "{}", - "description": "", - "version": 1, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"missing-index-pattern-3\",\"query\":{}}" - } - }, - "_meta": { - "savedObjectVersion": 2 - } - }, - { - "_id": "test-vis-4", - "_type": "visualization", - "_source": { - "title": "Test VIS 4", - "visState": "{\"title\":\"test vis 4\",\"type\":\"histogram\"}", - "uiStateJSON": "{}", - "description": "", - "version": 1, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"missing-index-pattern-4\",\"query\":{}}" - } - }, - "_meta": { - "savedObjectVersion": 2 - } - }, - { - "_id": "test-vis-5", - "_type": "visualization", - "_source": { - "title": "Test VIS 5", - "visState": "{\"title\":\"test vis 5\",\"type\":\"histogram\"}", - "uiStateJSON": "{}", - "description": "", - "version": 1, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"missing-index-pattern-5\",\"query\":{}}" - } - }, - "_meta": { - "savedObjectVersion": 2 - } - }, - { - "_id": "test-vis-6", - "_type": "visualization", - "_source": { - "title": "Test VIS 6", - "visState": "{\"title\":\"test vis 6\",\"type\":\"histogram\"}", - "uiStateJSON": "{}", - "description": "", - "version": 1, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"missing-index-pattern-6\",\"query\":{}}" - } - }, - "_meta": { - "savedObjectVersion": 2 - } - }, - { - "_id": "test-vis-7", - "_type": "visualization", - "_source": { - "title": "Test VIS 7", - "visState": "{\"title\":\"test vis 7\",\"type\":\"histogram\"}", - "uiStateJSON": "{}", - "description": "", - "version": 1, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"missing-index-pattern-7\",\"query\":{}}" - } - }, - "_meta": { - "savedObjectVersion": 2 - } - } -] diff --git a/test/functional/apps/management/exports/_import_objects_multiple_exists.json b/test/functional/apps/management/exports/_import_objects_multiple_exists.json deleted file mode 100644 index 9e554aecd9f7..000000000000 --- a/test/functional/apps/management/exports/_import_objects_multiple_exists.json +++ /dev/null @@ -1,36 +0,0 @@ -[ - { - "_id": "test-1", - "_type": "visualization", - "_source": { - "title": "Visualization test 1", - "visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}", - "uiStateJSON": "{}", - "description": "AreaChart", - "version": 1, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}" - } - }, - "_meta": { - "savedObjectVersion": 2 - } - }, - { - "_id": "test-2", - "_type": "visualization", - "_source": { - "title": "Visualization test 2", - "visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}", - "uiStateJSON": "{}", - "description": "AreaChart", - "version": 1, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}" - } - }, - "_meta": { - "savedObjectVersion": 2 - } - } -] diff --git a/test/functional/apps/management/exports/_import_objects_saved_search.json b/test/functional/apps/management/exports/_import_objects_saved_search.json deleted file mode 100644 index bfd034a7086d..000000000000 --- a/test/functional/apps/management/exports/_import_objects_saved_search.json +++ /dev/null @@ -1,25 +0,0 @@ -[ - { - "_id": "c45e6c50-ba72-11e7-a8f9-ad70f02e633d", - "_type": "search", - "_source": { - "title": "PHP saved search", - "description": "", - "hits": 0, - "columns": [ - "_source" - ], - "sort": [ - "@timestamp", - "desc" - ], - "version": 1, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"f1e4c910-a2e6-11e7-bb30-233be9be6a15\",\"highlightAll\":true,\"version\":true,\"query\":{\"language\":\"lucene\",\"query\":\"php\"},\"filter\":[]}" - } - }, - "_meta": { - "savedObjectVersion": 2 - } - } -] diff --git a/test/functional/apps/management/exports/_import_objects_with_index_patterns.json b/test/functional/apps/management/exports/_import_objects_with_index_patterns.json deleted file mode 100644 index a0288652ddda..000000000000 --- a/test/functional/apps/management/exports/_import_objects_with_index_patterns.json +++ /dev/null @@ -1,31 +0,0 @@ -[ - { - "_id": "f1e4c910-a2e6-11e7-bb30-233be9be6a15", - "_type": "index-pattern", - "_source": { - "title": "logstash-*", - "timeFieldName": "@timestamp", - "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"expression script\",\"type\":\"number\",\"count\":0,\"scripted\":true,\"script\":\"doc['bytes'].value\",\"lang\":\"expression\",\"indexed\":true,\"analyzed\":false,\"doc_values\":false}]" - }, - "_meta": { - "savedObjectVersion": 2 - } - }, - { - "_id": "saved_object_imported_with_index_pattern", - "_type": "visualization", - "_source": { - "title": "saved object imported with index pattern", - "visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}", - "uiStateJSON": "{}", - "description": "AreaChart", - "version": 1, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"f1e4c910-a2e6-11e7-bb30-233be9be6a15\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}" - } - }, - "_meta": { - "savedObjectVersion": 2 - } - } -] diff --git a/test/functional/apps/management/exports/mgmt_import_objects.json b/test/functional/apps/management/exports/mgmt_import_objects.json deleted file mode 100644 index 88e03585bf1e..000000000000 --- a/test/functional/apps/management/exports/mgmt_import_objects.json +++ /dev/null @@ -1,37 +0,0 @@ -[ - { - "_id": "6aea5700-ac94-11e8-a651-614b2788174a", - "_type": "search", - "_source": { - "title": "mysavedsearch", - "description": "", - "hits": 0, - "columns": [ - "_source" - ], - "sort": [ - "@timestamp", - "desc" - ], - "version": 1, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"index\":\"4c3f3c30-ac94-11e8-a651-614b2788174a\",\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" - } - } - }, - { - "_id": "8411daa0-ac94-11e8-a651-614b2788174a", - "_type": "visualization", - "_source": { - "title": "mysavedviz", - "visState": "{\"title\":\"mysavedviz\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}", - "uiStateJSON": "{}", - "description": "", - "savedSearchId": "6aea5700-ac94-11e8-a651-614b2788174a", - "version": 1, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" - } - } - } -] diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index fd445f34317e..ffd3adce378f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3302,32 +3302,17 @@ "savedObjectsManagement.objectsTable.exportObjectsConfirmModal.exportOptionsLabel": "オプション", "savedObjectsManagement.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel": "関連オブジェクトを含める", "savedObjectsManagement.objectsTable.exportObjectsConfirmModalDescription": "エクスポートするタイプを選択してください", - "savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.resolvingConflictsLoadingMessage": "矛盾を解決中…", - "savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.retryingFailedObjectsLoadingMessage": "失敗したオブジェクトを再試行中…", - "savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.savedSearchAreLinkedProperlyLoadingMessage": "保存された検索が正しくリンクされていることを確認してください…", - "savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.savingConflictsLoadingMessage": "矛盾を保存中…", "savedObjectsManagement.objectsTable.flyout.errorCalloutTitle": "申し訳ございません、エラーが発生しました", "savedObjectsManagement.objectsTable.flyout.import.cancelButtonLabel": "キャンセル", "savedObjectsManagement.objectsTable.flyout.import.confirmButtonLabel": "インポート", - "savedObjectsManagement.objectsTable.flyout.importFailedDescription": "{totalImportCount}個中{failedImportCount}個のオブジェクトのインポートに失敗しました。インポート失敗", - "savedObjectsManagement.objectsTable.flyout.importFailedMissingReference": "{type} [id={id}]は{refType} [id={refId}]を見つけられませんでした", - "savedObjectsManagement.objectsTable.flyout.importFailedTitle": "インポート失敗", - "savedObjectsManagement.objectsTable.flyout.importFailedUnsupportedType": "{type} [id={id}]サポートされていないタイプ", "savedObjectsManagement.objectsTable.flyout.importFileErrorMessage": "エラーのためファイルを処理できませんでした:「{error}」", - "savedObjectsManagement.objectsTable.flyout.importLegacyFileErrorMessage": "ファイルを処理できませんでした。", "savedObjectsManagement.objectsTable.flyout.importPromptText": "インポート", "savedObjectsManagement.objectsTable.flyout.importSavedObjectTitle": "保存されたオブジェクトのインポート", "savedObjectsManagement.objectsTable.flyout.importSuccessful.confirmAllChangesButtonLabel": "すべての変更を確定", "savedObjectsManagement.objectsTable.flyout.importSuccessful.confirmButtonLabel": "完了", - "savedObjectsManagement.objectsTable.flyout.importSuccessfulCallout.noObjectsImportedTitle": "オブジェクトがインポートされませんでした", - "savedObjectsManagement.objectsTable.flyout.importSuccessfulDescription": "{importCount}個のオブジェクトがインポートされました。", - "savedObjectsManagement.objectsTable.flyout.importSuccessfulTitle": "インポート成功", "savedObjectsManagement.objectsTable.flyout.indexPatternConflictsCalloutLinkText": "新規インデックスパターンを作成", "savedObjectsManagement.objectsTable.flyout.indexPatternConflictsDescription": "次の保存されたオブジェクトは、存在しないインデックスパターンを使用しています。関連付け直す別のインデックスパターンを選択してください。必要に応じて、{indexPatternLink}できます。", "savedObjectsManagement.objectsTable.flyout.indexPatternConflictsTitle": "インデックスパターンの矛盾", - "savedObjectsManagement.objectsTable.flyout.invalidFormatOfImportedFileErrorMessage": "保存されたオブジェクトのファイル形式が無効なため、インポートできません。", - "savedObjectsManagement.objectsTable.flyout.legacyFileUsedBody": "最新のレポートでNDJSONファイルを作成すれば完了です。", - "savedObjectsManagement.objectsTable.flyout.legacyFileUsedTitle": "JSONファイルのサポートが終了します", "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnCountDescription": "影響されるオブジェクトの数です", "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnCountName": "カウント", "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnIdDescription": "インデックスパターンのIDです", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index beb4f3d0c286..0fc6f0ec119e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3320,32 +3320,17 @@ "savedObjectsManagement.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel": "包括相关对象", "savedObjectsManagement.objectsTable.exportObjectsConfirmModalDescription": "选择要导出的类型", "savedObjectsManagement.objectsTable.exportObjectsConfirmModalTitle": "导出 {filteredItemCount, plural, other {# 个对象}}", - "savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.resolvingConflictsLoadingMessage": "正在解决冲突……", - "savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.retryingFailedObjectsLoadingMessage": "正在重试失败的对象……", - "savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.savedSearchAreLinkedProperlyLoadingMessage": "确保已保存搜索已正确链接……", - "savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.savingConflictsLoadingMessage": "正在保存冲突……", "savedObjectsManagement.objectsTable.flyout.errorCalloutTitle": "抱歉,有错误", "savedObjectsManagement.objectsTable.flyout.import.cancelButtonLabel": "取消", "savedObjectsManagement.objectsTable.flyout.import.confirmButtonLabel": "导入", - "savedObjectsManagement.objectsTable.flyout.importFailedDescription": "{totalImportCount} 个对象中有 {failedImportCount} 个无法导入。导入失败", - "savedObjectsManagement.objectsTable.flyout.importFailedMissingReference": "{type} [id={id}] 无法找到 {refType} [id={refId}]", - "savedObjectsManagement.objectsTable.flyout.importFailedTitle": "导入失败", - "savedObjectsManagement.objectsTable.flyout.importFailedUnsupportedType": "{type} [id={id}] 不受支持的类型", "savedObjectsManagement.objectsTable.flyout.importFileErrorMessage": "由于以下错误,无法处理文件:“{error}”", - "savedObjectsManagement.objectsTable.flyout.importLegacyFileErrorMessage": "无法处理该文件。", "savedObjectsManagement.objectsTable.flyout.importPromptText": "导入", "savedObjectsManagement.objectsTable.flyout.importSavedObjectTitle": "导入已保存对象", "savedObjectsManagement.objectsTable.flyout.importSuccessful.confirmAllChangesButtonLabel": "确认所有更改", "savedObjectsManagement.objectsTable.flyout.importSuccessful.confirmButtonLabel": "完成", - "savedObjectsManagement.objectsTable.flyout.importSuccessfulCallout.noObjectsImportedTitle": "未导入任何对象", - "savedObjectsManagement.objectsTable.flyout.importSuccessfulDescription": "已成功导入 {importCount} 个对象。", - "savedObjectsManagement.objectsTable.flyout.importSuccessfulTitle": "导入成功", "savedObjectsManagement.objectsTable.flyout.indexPatternConflictsCalloutLinkText": "创建新的索引模式", "savedObjectsManagement.objectsTable.flyout.indexPatternConflictsDescription": "以下已保存对象使用不存在的索引模式。请选择要重新关联的索引模式。必要时可以{indexPatternLink}。", "savedObjectsManagement.objectsTable.flyout.indexPatternConflictsTitle": "索引模式冲突", - "savedObjectsManagement.objectsTable.flyout.invalidFormatOfImportedFileErrorMessage": "已保存对象文件格式无效,无法导入。", - "savedObjectsManagement.objectsTable.flyout.legacyFileUsedBody": "只需使用更新的导出功能生成 NDJSON 文件,便万事俱备。", - "savedObjectsManagement.objectsTable.flyout.legacyFileUsedTitle": "将不再支持 JSON 文件", "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnCountDescription": "受影响对象数目", "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnCountName": "计数", "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnIdDescription": "索引模式的 ID", From a18cc31924c58c9a3f2d80a6e0b6c2a3ae0ab535 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Fri, 3 Sep 2021 15:36:03 +0100 Subject: [PATCH 19/47] [ML] Datafeed preview based job validation check (#109080) * [ML] Datafeed preview based job validation check * updating warning text * fix tests * adding jest test * updating tests * fixing translation ids * fixing more tests * changes based on review * disabled validation step next button when validation fails * disabling nano job test * adding test skip comment Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../ml/common/constants/messages.test.mock.ts | 3 + .../ml/common/constants/messages.test.ts | 6 + .../plugins/ml/common/constants/messages.ts | 24 +++ .../components/validation_step/validation.tsx | 6 +- .../job_validation/job_validation.test.ts | 150 ++++++++++++++---- .../models/job_validation/job_validation.ts | 9 +- .../validate_datafeed_preview.ts | 38 +++++ .../ml/server/routes/job_validation.ts | 6 +- .../server/routes/schemas/datafeeds_schema.ts | 2 +- .../apis/ml/job_validation/validate.ts | 2 +- .../ml/anomaly_detection/date_nanos_job.ts | 3 +- 11 files changed, 209 insertions(+), 40 deletions(-) create mode 100644 x-pack/plugins/ml/server/models/job_validation/validate_datafeed_preview.ts diff --git a/x-pack/plugins/ml/common/constants/messages.test.mock.ts b/x-pack/plugins/ml/common/constants/messages.test.mock.ts index 6e539617604c..fbfff20adc5c 100644 --- a/x-pack/plugins/ml/common/constants/messages.test.mock.ts +++ b/x-pack/plugins/ml/common/constants/messages.test.mock.ts @@ -78,4 +78,7 @@ export const nonBasicIssuesMessages = [ { id: 'missing_summary_count_field_name', }, + { + id: 'datafeed_preview_failed', + }, ]; diff --git a/x-pack/plugins/ml/common/constants/messages.test.ts b/x-pack/plugins/ml/common/constants/messages.test.ts index 59fc50757b67..c46eba458d1d 100644 --- a/x-pack/plugins/ml/common/constants/messages.test.ts +++ b/x-pack/plugins/ml/common/constants/messages.test.ts @@ -173,6 +173,12 @@ describe('Constants: Messages parseMessages()', () => { text: 'A job configured with a datafeed with aggregations must set summary_count_field_name; use doc_count or suitable alternative.', }, + { + id: 'datafeed_preview_failed', + status: 'error', + text: + 'The datafeed preview failed. This may be due to an error in the job or datafeed configurations.', + }, ]); }); }); diff --git a/x-pack/plugins/ml/common/constants/messages.ts b/x-pack/plugins/ml/common/constants/messages.ts index 0327e8746c7d..fd3b9aa9d19b 100644 --- a/x-pack/plugins/ml/common/constants/messages.ts +++ b/x-pack/plugins/ml/common/constants/messages.ts @@ -626,6 +626,30 @@ export const getMessages = once((docLinks?: DocLinksStart) => { 'the UNIX epoch beginning. Timestamps before 01/01/1970 00:00:00 (UTC) are not supported for machine learning jobs.', }), }, + datafeed_preview_no_documents: { + status: VALIDATION_STATUS.WARNING, + heading: i18n.translate( + 'xpack.ml.models.jobValidation.messages.datafeedPreviewNoDocumentsHeading', + { + defaultMessage: 'Datafeed preview', + } + ), + text: i18n.translate( + 'xpack.ml.models.jobValidation.messages.datafeedPreviewNoDocumentsMessage', + { + defaultMessage: + 'Running the datafeed preview over the current job configuration produces no results. ' + + 'If the index contains no documents this warning can be ignored, otherwise the job may be misconfigured.', + } + ), + }, + datafeed_preview_failed: { + status: VALIDATION_STATUS.ERROR, + text: i18n.translate('xpack.ml.models.jobValidation.messages.datafeedPreviewFailedMessage', { + defaultMessage: + 'The datafeed preview failed. This may be due to an error in the job or datafeed configurations.', + }), + }, }; }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx index a1c7eab6b746..d2a0e83200d9 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, FC, useContext, useEffect } from 'react'; +import React, { Fragment, FC, useContext, useEffect, useState } from 'react'; import { WizardNav } from '../wizard_nav'; import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; @@ -22,6 +22,7 @@ const idFilterList = [ export const ValidationStep: FC = ({ setCurrentStep, isCurrentStep }) => { const { jobCreator, jobCreatorUpdate, jobValidator } = useContext(JobCreatorContext); + const [nextActive, setNextActive] = useState(false); if (jobCreator.type === JOB_TYPE.ADVANCED) { // for advanced jobs, ignore time range warning as the @@ -52,6 +53,7 @@ export const ValidationStep: FC = ({ setCurrentStep, isCurrentStep }) // keep a record of the advanced validation in the jobValidator function setIsValid(valid: boolean) { jobValidator.advancedValid = valid; + setNextActive(valid); } return ( @@ -69,7 +71,7 @@ export const ValidationStep: FC = ({ setCurrentStep, isCurrentStep }) setCurrentStep(WIZARD_STEPS.JOB_DETAILS)} next={() => setCurrentStep(WIZARD_STEPS.SUMMARY)} - nextActive={true} + nextActive={nextActive} /> )} diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts index a5483491f135..e890020eb726 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts @@ -10,6 +10,7 @@ import { IScopedClusterClient } from 'kibana/server'; import { validateJob, ValidateJobPayload } from './job_validation'; import { ES_CLIENT_TOTAL_HITS_RELATION } from '../../../common/types/es_client'; import type { MlClient } from '../../lib/ml_client'; +import type { AuthorizationHeader } from '../../lib/request_authorization'; const callAs = { fieldCaps: () => Promise.resolve({ body: { fields: [] } }), @@ -19,6 +20,8 @@ const callAs = { }), }; +const authHeader: AuthorizationHeader = {}; + const mlClusterClient = ({ asCurrentUser: callAs, asInternalUser: callAs, @@ -34,18 +37,19 @@ const mlClient = ({ }, }, }), + previewDatafeed: () => Promise.resolve({ body: [{}] }), } as unknown) as MlClient; // Note: The tests cast `payload` as any // so we can simulate possible runtime payloads // that don't satisfy the TypeScript specs. describe('ML - validateJob', () => { - it('basic validation messages', () => { + it('basic validation messages', async () => { const payload = ({ job: { analysis_config: { detectors: [] } }, } as unknown) as ValidateJobPayload; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids).toStrictEqual([ @@ -58,14 +62,14 @@ describe('ML - validateJob', () => { }); const jobIdTests = (testIds: string[], messageId: string) => { - const promises = testIds.map((id) => { + const promises = testIds.map(async (id) => { const payload = ({ job: { analysis_config: { detectors: [] }, job_id: id, }, } as unknown) as ValidateJobPayload; - return validateJob(mlClusterClient, mlClient, payload).catch(() => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).catch(() => { new Error('Promise should not fail for jobIdTests.'); }); }); @@ -86,7 +90,7 @@ describe('ML - validateJob', () => { job: { analysis_config: { detectors: [] }, groups: testIds }, } as unknown) as ValidateJobPayload; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids.includes(messageId)).toBe(true); }); @@ -126,7 +130,7 @@ describe('ML - validateJob', () => { const payload = ({ job: { analysis_config: { bucket_span: format, detectors: [] } }, } as unknown) as ValidateJobPayload; - return validateJob(mlClusterClient, mlClient, payload).catch(() => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).catch(() => { new Error('Promise should not fail for bucketSpanFormatTests.'); }); }); @@ -150,7 +154,7 @@ describe('ML - validateJob', () => { return bucketSpanFormatTests(validBucketSpanFormats, 'bucket_span_valid'); }); - it('at least one detector function is empty', () => { + it('at least one detector function is empty', async () => { const payload = ({ job: { analysis_config: { detectors: [] as Array<{ function?: string }> } }, } as unknown) as ValidateJobPayload; @@ -165,13 +169,13 @@ describe('ML - validateJob', () => { function: undefined, }); - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids.includes('detectors_function_empty')).toBe(true); }); }); - it('detector function is not empty', () => { + it('detector function is not empty', async () => { const payload = ({ job: { analysis_config: { detectors: [] as Array<{ function?: string }> } }, } as unknown) as ValidateJobPayload; @@ -179,37 +183,37 @@ describe('ML - validateJob', () => { function: 'count', }); - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids.includes('detectors_function_not_empty')).toBe(true); }); }); - it('invalid index fields', () => { + it('invalid index fields', async () => { const payload = ({ job: { analysis_config: { detectors: [] } }, fields: {}, } as unknown) as ValidateJobPayload; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids.includes('index_fields_invalid')).toBe(true); }); }); - it('valid index fields', () => { + it('valid index fields', async () => { const payload = ({ job: { analysis_config: { detectors: [] } }, fields: { testField: {} }, } as unknown) as ValidateJobPayload; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids.includes('index_fields_valid')).toBe(true); }); }); - const getBasicPayload = (): any => ({ + const getBasicPayload = (): ValidateJobPayload => ({ job: { job_id: 'test', analysis_config: { @@ -231,7 +235,7 @@ describe('ML - validateJob', () => { const payload = getBasicPayload() as any; delete payload.job.analysis_config.influencers; - validateJob(mlClusterClient, mlClient, payload).then( + validateJob(mlClusterClient, mlClient, payload, authHeader).then( () => done( new Error('Promise should not resolve for this test when influencers is not an Array.') @@ -240,10 +244,10 @@ describe('ML - validateJob', () => { ); }); - it('detect duplicate detectors', () => { + it('detect duplicate detectors', async () => { const payload = getBasicPayload() as any; payload.job.analysis_config.detectors.push({ function: 'count' }); - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids).toStrictEqual([ 'job_id_valid', @@ -256,7 +260,7 @@ describe('ML - validateJob', () => { }); }); - it('dedupe duplicate messages', () => { + it('dedupe duplicate messages', async () => { const payload = getBasicPayload() as any; // in this test setup, the following configuration passes // the duplicate detectors check, but would return the same @@ -266,7 +270,7 @@ describe('ML - validateJob', () => { { function: 'count', by_field_name: 'airline' }, { function: 'count', partition_field_name: 'airline' }, ]; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids).toStrictEqual([ 'job_id_valid', @@ -278,9 +282,9 @@ describe('ML - validateJob', () => { }); }); - it('basic validation passes, extended checks return some messages', () => { + it('basic validation passes, extended checks return some messages', async () => { const payload = getBasicPayload(); - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids).toStrictEqual([ 'job_id_valid', @@ -291,8 +295,8 @@ describe('ML - validateJob', () => { }); }); - it('categorization job using mlcategory passes aggregatable field check', () => { - const payload: any = { + it('categorization job using mlcategory passes aggregatable field check', async () => { + const payload: ValidateJobPayload = { job: { job_id: 'categorization_test', analysis_config: { @@ -312,7 +316,7 @@ describe('ML - validateJob', () => { fields: { testField: {} }, }; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids).toStrictEqual([ 'job_id_valid', @@ -325,8 +329,8 @@ describe('ML - validateJob', () => { }); }); - it('non-existent field reported as non aggregatable', () => { - const payload: any = { + it('non-existent field reported as non aggregatable', async () => { + const payload: ValidateJobPayload = { job: { job_id: 'categorization_test', analysis_config: { @@ -345,7 +349,7 @@ describe('ML - validateJob', () => { fields: { testField: {} }, }; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids).toStrictEqual([ 'job_id_valid', @@ -357,8 +361,8 @@ describe('ML - validateJob', () => { }); }); - it('script field not reported as non aggregatable', () => { - const payload: any = { + it('script field not reported as non aggregatable', async () => { + const payload: ValidateJobPayload = { job: { job_id: 'categorization_test', analysis_config: { @@ -387,7 +391,7 @@ describe('ML - validateJob', () => { fields: { testField: {} }, }; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids).toStrictEqual([ 'job_id_valid', @@ -399,4 +403,88 @@ describe('ML - validateJob', () => { ]); }); }); + + it('datafeed preview contains no docs', async () => { + const payload: ValidateJobPayload = { + job: { + job_id: 'categorization_test', + analysis_config: { + bucket_span: '15m', + detectors: [ + { + function: 'count', + partition_field_name: 'custom_script_field', + }, + ], + influencers: [''], + }, + data_description: { time_field: '@timestamp' }, + datafeed_config: { + indices: [], + }, + }, + fields: { testField: {} }, + }; + + const mlClientEmptyDatafeedPreview = ({ + ...mlClient, + previewDatafeed: () => Promise.resolve({ body: [] }), + } as unknown) as MlClient; + + return validateJob(mlClusterClient, mlClientEmptyDatafeedPreview, payload, authHeader).then( + (messages) => { + const ids = messages.map((m) => m.id); + expect(ids).toStrictEqual([ + 'job_id_valid', + 'detectors_function_not_empty', + 'index_fields_valid', + 'field_not_aggregatable', + 'time_field_invalid', + 'datafeed_preview_no_documents', + ]); + } + ); + }); + + it('datafeed preview failed', async () => { + const payload: ValidateJobPayload = { + job: { + job_id: 'categorization_test', + analysis_config: { + bucket_span: '15m', + detectors: [ + { + function: 'count', + partition_field_name: 'custom_script_field', + }, + ], + influencers: [''], + }, + data_description: { time_field: '@timestamp' }, + datafeed_config: { + indices: [], + }, + }, + fields: { testField: {} }, + }; + + const mlClientEmptyDatafeedPreview = ({ + ...mlClient, + previewDatafeed: () => Promise.reject({}), + } as unknown) as MlClient; + + return validateJob(mlClusterClient, mlClientEmptyDatafeedPreview, payload, authHeader).then( + (messages) => { + const ids = messages.map((m) => m.id); + expect(ids).toStrictEqual([ + 'job_id_valid', + 'detectors_function_not_empty', + 'index_fields_valid', + 'field_not_aggregatable', + 'time_field_invalid', + 'datafeed_preview_failed', + ]); + } + ); + }); }); diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts index 80eba7b86405..838f188455d4 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts @@ -6,7 +6,7 @@ */ import Boom from '@hapi/boom'; -import { IScopedClusterClient } from 'kibana/server'; +import type { IScopedClusterClient } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; import { fieldsServiceProvider } from '../fields_service'; import { getMessages, MessageId, JobValidationMessage } from '../../../common/constants/messages'; @@ -17,12 +17,14 @@ import { basicJobValidation, uniqWithIsEqual } from '../../../common/util/job_ut import { validateBucketSpan } from './validate_bucket_span'; import { validateCardinality } from './validate_cardinality'; import { validateInfluencers } from './validate_influencers'; +import { validateDatafeedPreview } from './validate_datafeed_preview'; import { validateModelMemoryLimit } from './validate_model_memory_limit'; import { validateTimeRange, isValidTimeField } from './validate_time_range'; import { validateJobSchema } from '../../routes/schemas/job_validation_schema'; -import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; +import type { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import type { MlClient } from '../../lib/ml_client'; import { getDatafeedAggregations } from '../../../common/util/datafeed_utils'; +import type { AuthorizationHeader } from '../../lib/request_authorization'; export type ValidateJobPayload = TypeOf; @@ -34,6 +36,7 @@ export async function validateJob( client: IScopedClusterClient, mlClient: MlClient, payload: ValidateJobPayload, + authHeader: AuthorizationHeader, isSecurityDisabled?: boolean ) { const messages = getMessages(); @@ -107,6 +110,8 @@ export async function validateJob( if (datafeedAggregations !== undefined && !job.analysis_config?.summary_count_field_name) { validationMessages.push({ id: 'missing_summary_count_field_name' }); } + + validationMessages.push(...(await validateDatafeedPreview(mlClient, authHeader, job))); } else { validationMessages = basicValidation.messages; validationMessages.push({ id: 'skipped_extended_tests' }); diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_datafeed_preview.ts b/x-pack/plugins/ml/server/models/job_validation/validate_datafeed_preview.ts new file mode 100644 index 000000000000..e009dcf49fda --- /dev/null +++ b/x-pack/plugins/ml/server/models/job_validation/validate_datafeed_preview.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { MlClient } from '../../lib/ml_client'; +import type { AuthorizationHeader } from '../../lib/request_authorization'; +import type { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; +import type { JobValidationMessage } from '../../../common/constants/messages'; + +export async function validateDatafeedPreview( + mlClient: MlClient, + authHeader: AuthorizationHeader, + job: CombinedJob +): Promise { + const { datafeed_config: datafeed, ...tempJob } = job; + try { + const { body } = ((await mlClient.previewDatafeed( + { + body: { + job_config: tempJob, + datafeed_config: datafeed, + }, + }, + authHeader + // previewDatafeed response type is incorrect + )) as unknown) as { body: unknown[] }; + + if (Array.isArray(body) === false || body.length === 0) { + return [{ id: 'datafeed_preview_no_documents' }]; + } + return []; + } catch (error) { + return [{ id: 'datafeed_preview_failed' }]; + } +} diff --git a/x-pack/plugins/ml/server/routes/job_validation.ts b/x-pack/plugins/ml/server/routes/job_validation.ts index 9309592dfc47..b75eab20e7bc 100644 --- a/x-pack/plugins/ml/server/routes/job_validation.ts +++ b/x-pack/plugins/ml/server/routes/job_validation.ts @@ -8,9 +8,9 @@ import Boom from '@hapi/boom'; import { IScopedClusterClient } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; -import { AnalysisConfig, Datafeed } from '../../common/types/anomaly_detection_jobs'; +import type { AnalysisConfig, Datafeed } from '../../common/types/anomaly_detection_jobs'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization } from '../types'; +import type { RouteInitialization } from '../types'; import { estimateBucketSpanSchema, modelMemoryLimitSchema, @@ -20,6 +20,7 @@ import { import { estimateBucketSpanFactory } from '../models/bucket_span_estimator'; import { calculateModelMemoryLimitProvider } from '../models/calculate_model_memory_limit'; import { validateJob, validateCardinality } from '../models/job_validation'; +import { getAuthorizationHeader } from '../lib/request_authorization'; import type { MlClient } from '../lib/ml_client'; type CalculateModelMemoryLimitPayload = TypeOf; @@ -192,6 +193,7 @@ export function jobValidationRoutes({ router, mlLicense, routeGuard }: RouteInit client, mlClient, request.body, + getAuthorizationHeader(request), mlLicense.isSecurityEnabled() === false ); diff --git a/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts b/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts index 118d2e4140ce..27e1b6afe336 100644 --- a/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts @@ -52,7 +52,7 @@ export const datafeedConfigSchema = schema.object({ runtime_mappings: schema.maybe(schema.any()), scroll_size: schema.maybe(schema.number()), delayed_data_check_config: schema.maybe(schema.any()), - indices_options: indicesOptionsSchema, + indices_options: schema.maybe(indicesOptionsSchema), }); export const datafeedIdSchema = schema.object({ datafeedId: schema.string() }); diff --git a/x-pack/test/api_integration/apis/ml/job_validation/validate.ts b/x-pack/test/api_integration/apis/ml/job_validation/validate.ts index 06d966851abf..293b0e94351d 100644 --- a/x-pack/test/api_integration/apis/ml/job_validation/validate.ts +++ b/x-pack/test/api_integration/apis/ml/job_validation/validate.ts @@ -184,7 +184,7 @@ export default ({ getService }: FtrProviderContext) => { expect(body.length).to.eql( expectedResponse.length, - `Response body should have ${expectedResponse.length} entries (got ${body})` + `Response body should have ${expectedResponse.length} entries (got ${JSON.stringify(body)})` ); for (const entry of expectedResponse) { const responseEntry = body.find((obj: any) => obj.id === entry.id); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts index d351e8f7057e..5f8d346ee447 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts @@ -114,7 +114,8 @@ export default function ({ getService }: FtrProviderContext) { }, ]; - describe('job on data set with date_nanos time field', function () { + // test skipped until https://github.com/elastic/elasticsearch/pull/77109 is fixed + describe.skip('job on data set with date_nanos time field', function () { this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/event_rate_nanos'); From 98014d0cb9dcdb767f9cdd4d6efefe64706aaca2 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Fri, 3 Sep 2021 15:36:45 +0100 Subject: [PATCH 20/47] [ML] Datafeed preview based job validation check (#109080) * [ML] Datafeed preview based job validation check * updating warning text * fix tests * adding jest test * updating tests * fixing translation ids * fixing more tests * changes based on review * disabled validation step next button when validation fails * disabling nano job test * adding test skip comment Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> From 19260d5f02361c28276306b37cbd39dc2732020b Mon Sep 17 00:00:00 2001 From: Dmitry Shevchenko Date: Fri, 3 Sep 2021 16:38:58 +0200 Subject: [PATCH 21/47] Fix "Expression produces a union type that is too complex to represent" TS error (#111111) --- .../rule_registry_log_client.ts | 126 +++++++++--------- 1 file changed, 64 insertions(+), 62 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/rule_registry_log_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/rule_registry_log_client.ts index f0da8dad16ab..a5515f8db855 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/rule_registry_log_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/rule_registry_log_client.ts @@ -142,76 +142,78 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient { invariant(result.aggregations, 'Search response should contain aggregations'); return Object.fromEntries( - result.aggregations.rules.buckets.map((bucket) => [ - bucket.key, - bucket.most_recent_logs.hits.hits.map((event) => { - const logEntry = parseRuleExecutionLog(event._source); - invariant( - logEntry[ALERT_RULE_UUID] ?? '', - 'Malformed execution log entry: rule.id field not found' - ); + result.aggregations.rules.buckets.map<[ruleId: string, logs: IRuleStatusSOAttributes[]]>( + (bucket) => [ + bucket.key as string, + bucket.most_recent_logs.hits.hits.map((event) => { + const logEntry = parseRuleExecutionLog(event._source); + invariant( + logEntry[ALERT_RULE_UUID] ?? '', + 'Malformed execution log entry: rule.id field not found' + ); - const lastFailure = bucket.last_failure.event.hits.hits[0] - ? parseRuleExecutionLog(bucket.last_failure.event.hits.hits[0]._source) - : undefined; + const lastFailure = bucket.last_failure.event.hits.hits[0] + ? parseRuleExecutionLog(bucket.last_failure.event.hits.hits[0]._source) + : undefined; - const lastSuccess = bucket.last_success.event.hits.hits[0] - ? parseRuleExecutionLog(bucket.last_success.event.hits.hits[0]._source) - : undefined; + const lastSuccess = bucket.last_success.event.hits.hits[0] + ? parseRuleExecutionLog(bucket.last_success.event.hits.hits[0]._source) + : undefined; - const lookBack = bucket.indexing_lookback.event.hits.hits[0] - ? parseRuleExecutionLog(bucket.indexing_lookback.event.hits.hits[0]._source) - : undefined; + const lookBack = bucket.indexing_lookback.event.hits.hits[0] + ? parseRuleExecutionLog(bucket.indexing_lookback.event.hits.hits[0]._source) + : undefined; - const executionGap = bucket.execution_gap.event.hits.hits[0] - ? parseRuleExecutionLog(bucket.execution_gap.event.hits.hits[0]._source)[ - getMetricField(ExecutionMetric.executionGap) - ] - : undefined; + const executionGap = bucket.execution_gap.event.hits.hits[0] + ? parseRuleExecutionLog(bucket.execution_gap.event.hits.hits[0]._source)[ + getMetricField(ExecutionMetric.executionGap) + ] + : undefined; - const searchDuration = bucket.search_duration_max.event.hits.hits[0] - ? parseRuleExecutionLog(bucket.search_duration_max.event.hits.hits[0]._source)[ - getMetricField(ExecutionMetric.searchDurationMax) - ] - : undefined; + const searchDuration = bucket.search_duration_max.event.hits.hits[0] + ? parseRuleExecutionLog(bucket.search_duration_max.event.hits.hits[0]._source)[ + getMetricField(ExecutionMetric.searchDurationMax) + ] + : undefined; - const indexingDuration = bucket.indexing_duration_max.event.hits.hits[0] - ? parseRuleExecutionLog(bucket.indexing_duration_max.event.hits.hits[0]._source)[ - getMetricField(ExecutionMetric.indexingDurationMax) - ] - : undefined; + const indexingDuration = bucket.indexing_duration_max.event.hits.hits[0] + ? parseRuleExecutionLog(bucket.indexing_duration_max.event.hits.hits[0]._source)[ + getMetricField(ExecutionMetric.indexingDurationMax) + ] + : undefined; - const alertId = logEntry[ALERT_RULE_UUID] ?? ''; - const statusDate = logEntry[TIMESTAMP]; - const lastFailureAt = lastFailure?.[TIMESTAMP]; - const lastFailureMessage = lastFailure?.[MESSAGE]; - const lastSuccessAt = lastSuccess?.[TIMESTAMP]; - const lastSuccessMessage = lastSuccess?.[MESSAGE]; - const status = (logEntry[RULE_STATUS] as RuleExecutionStatus) || null; - const lastLookBackDate = lookBack?.[getMetricField(ExecutionMetric.indexingLookback)]; - const gap = executionGap ? moment.duration(executionGap).humanize() : null; - const bulkCreateTimeDurations = indexingDuration - ? [makeFloatString(indexingDuration)] - : null; - const searchAfterTimeDurations = searchDuration - ? [makeFloatString(searchDuration)] - : null; + const alertId = logEntry[ALERT_RULE_UUID] ?? ''; + const statusDate = logEntry[TIMESTAMP]; + const lastFailureAt = lastFailure?.[TIMESTAMP]; + const lastFailureMessage = lastFailure?.[MESSAGE]; + const lastSuccessAt = lastSuccess?.[TIMESTAMP]; + const lastSuccessMessage = lastSuccess?.[MESSAGE]; + const status = (logEntry[RULE_STATUS] as RuleExecutionStatus) || null; + const lastLookBackDate = lookBack?.[getMetricField(ExecutionMetric.indexingLookback)]; + const gap = executionGap ? moment.duration(executionGap).humanize() : null; + const bulkCreateTimeDurations = indexingDuration + ? [makeFloatString(indexingDuration)] + : null; + const searchAfterTimeDurations = searchDuration + ? [makeFloatString(searchDuration)] + : null; - return { - alertId, - statusDate, - lastFailureAt, - lastFailureMessage, - lastSuccessAt, - lastSuccessMessage, - status, - lastLookBackDate, - gap, - bulkCreateTimeDurations, - searchAfterTimeDurations, - }; - }), - ]) + return { + alertId, + statusDate, + lastFailureAt, + lastFailureMessage, + lastSuccessAt, + lastSuccessMessage, + status, + lastLookBackDate, + gap, + bulkCreateTimeDurations, + searchAfterTimeDurations, + }; + }), + ] + ) ); } From 9a459806ad12526caf7b099db2664b0ef65b647b Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Fri, 3 Sep 2021 08:41:28 -0600 Subject: [PATCH 22/47] [Security Solutions][Detection Engine] Adds ability to ignore fields during alert indexing and a workaround for an EQL bug (#110927) ## Summary Adds a workaround for EQL bug: https://github.com/elastic/elasticsearch/issues/77152 Adds the safety feature mentioned here: https://github.com/elastic/kibana/issues/110802 Adds the ability to ignore particular [fields](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-fields.html#search-fields-param) when the field is merged with [_source](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-fields.html#source-filtering). Also fixes an EQL bug where EQL is introducing the meta field of `_ignored` within the fields and causing documents to not be indexable when we merge with the fields from EQL. Alerting document creation uses the fields API to get [runtime field](https://www.elastic.co/guide/en/elasticsearch/reference/current/runtime.html), [constant keyword](https://www.elastic.co/guide/en/elasticsearch/reference/master/keyword.html#constant-keyword-field-type), etc... that are only available within the [fields API](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-fields.html#search-fields-param) and then merges the field values not found within the `_source` document with the `_source` document and then finally indexes this merged document as an alert document. This fix/ability is a "safety feature" in that if a problematic [runtime field](https://www.elastic.co/guide/en/elasticsearch/reference/current/runtime.html), [constant keyword](https://www.elastic.co/guide/en/elasticsearch/reference/master/keyword.html#constant-keyword-field-type) is discovered or another bug along the stack we can set a `kibana.yml` key/value pair to ignore the problematic field. This _WILL NOT_ remove problematic fields from the `_source` document. This will only ignore problematic constant keyword, runtime fields, aliases, or anything else found in the fields API that is causing merge issues. This PR: * Adds a `alertIgnoreFields` `kibana.yml` array key with a default of an empty array if not specified. * Plumbs the `alertIgnoreFields` through the stack and into the fields/_source merge strategies of `missingFields` and `allFields` * Adds a temporary `isEqlBug77152` where it hard codes an ignore of `_ignored` until the EQL problem is fixed and then we will remove the workaround * Adds unit tests * Adds e2e tests which covers the described use cases above. The `alertIgnoreFields` key/value within `kibana.yml` if set should be an array of strings of each field you want to ignore. This can also contain regular expressions as long as they are of the form, `"/regex/"` in the array. Example if you want to ignore fields that are problematic called "host.name" and then one in which you want to ignore all fields that start with "user." using a regular expression: ```yml xpack.securitySolution.alertIgnoreFields: ['host.name', '/user\..*/'] ``` Although there are e2e tests which exercise the use cases... If you want to manual test the EQL bug fix you would add these documents in dev tools: ```json # Delete and add a mapping with a small ignore_above. DELETE eql-issue-ignore-fields-delme PUT eql-issue-ignore-fields-delme { "mappings" : { "dynamic": "strict", "properties" : { "@timestamp": { "type": "date" }, "some_keyword" : { "ignore_above": 5, "type" : "keyword" }, "other_keyword" : { "ignore_above": 10, "type" : "keyword" } } } } # Add a single document with one field that will be truncated and a second that will not. PUT eql-issue-ignore-fields-delme/_doc/1 { "@timestamp": "2021-09-02T04:13:05.626Z", "some_keyword": "longer than normal", "other_keyword": "normal" } ``` Then create an alert which queries everything from it: Screen Shot 2021-09-01 at 10 15 06 PM and ensure signals are created: Screen Shot 2021-09-01 at 10 30 18 PM To test the manual exclusions of any other problematic fields, create any index which has runtime fields or `constant keywords` but does not have anything within the `_source` document using dev tools. For example you can use `constant keyword` like so ```json PUT constant-keywords-deleme { "mappings": { "dynamic": "strict", "properties": { "@timestamp": { "type": "date" }, "testing_ignored": { "properties": { "constant": { "type": "constant_keyword", "value": "constant_value" } } }, "testing_regex": { "type": "constant_keyword", "value": "constant_value" }, "normal_constant": { "type": "constant_keyword", "value": "constant_value" }, "small_field": { "type": "keyword", "ignore_above": 10 } } } } PUT constant-keywords-deleme/_doc/1 { "@timestamp": "2021-09-02T04:20:01.760Z" } ``` Set in your `kibana.yml` the key/value of: ```yml xpack.securitySolution.alertIgnoreFields: ['testing_ignored.constant', '/.*_regex/'] ``` Setup a rule to run: Screen Shot 2021-09-01 at 10 23 23 PM Once it runs you should notice that the constant values for testing are not on the signals table since it only typically exists in the fields API: Screen Shot 2021-09-01 at 10 26 16 PM But the normal one still exists: Screen Shot 2021-09-01 at 10 26 31 PM If you change the `xpack.securitySolution.alertIgnoreFields` by removing it and re-generate the signals you will see these values added back. ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/master/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) --- .../resources/base/bin/kibana-docker | 1 + .../security_solution/server/config.test.ts | 80 +++++ .../security_solution/server/config.ts | 49 +++ .../routes/__mocks__/index.ts | 1 + .../create_security_rule_type_factory.ts | 2 + .../factories/utils/build_bulk_body.ts | 3 +- .../rule_types/factories/wrap_hits_factory.ts | 7 +- .../create_indicator_match_alert_type.test.ts | 3 + .../create_indicator_match_alert_type.ts | 2 + .../ml/create_ml_alert_type.test.ts | 1 + .../rule_types/ml/create_ml_alert_type.ts | 11 +- .../query/create_query_alert_type.test.ts | 2 + .../query/create_query_alert_type.ts | 2 + .../lib/detection_engine/rule_types/types.ts | 2 + .../signals/build_bulk_body.test.ts | 8 + .../signals/build_bulk_body.ts | 16 +- .../signals/search_after_bulk_create.test.ts | 1 + .../signals/signal_rule_alert_type.test.ts | 1 + .../signals/signal_rule_alert_type.ts | 4 + .../merge_all_fields_with_source.test.ts | 326 ++++++++++++------ .../merge_all_fields_with_source.ts | 8 +- .../merge_missing_fields_with_source.test.ts | 278 +++++++++------ .../merge_missing_fields_with_source.ts | 8 +- .../strategies/merge_no_fields.ts | 3 +- .../signals/source_fields_merging/types.ts | 10 +- .../utils/filter_field_entries.test.ts | 37 +- .../utils/filter_field_entries.ts | 9 +- .../source_fields_merging/utils/index.ts | 1 + .../utils/is_eql_bug_77152.test.ts | 29 ++ .../utils/is_eql_bug_77152.ts | 18 + .../utils/is_ignored.test.ts | 84 +++++ .../source_fields_merging/utils/is_ignored.ts | 23 ++ .../signals/wrap_hits_factory.ts | 11 +- .../signals/wrap_sequences_factory.ts | 3 + .../security_solution/server/plugin.ts | 2 + .../common/config.ts | 4 + .../tests/ignore_fields.ts | 133 +++++++ .../security_and_spaces/tests/index.ts | 1 + .../security_solution/ignore_fields/data.json | 51 +++ .../ignore_fields/mappings.json | 41 +++ 40 files changed, 1044 insertions(+), 232 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/config.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_eql_bug_77152.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_eql_bug_77152.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_ignored.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_ignored.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/ignore_fields.ts create mode 100644 x-pack/test/functional/es_archives/security_solution/ignore_fields/data.json create mode 100644 x-pack/test/functional/es_archives/security_solution/ignore_fields/mappings.json diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index 0af087f1427d..4bb89e1b7e60 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -383,6 +383,7 @@ kibana_vars=( xpack.security.session.lifespan xpack.security.sessionTimeout xpack.securitySolution.alertMergeStrategy + xpack.securitySolution.alertIgnoreFields xpack.securitySolution.endpointResultListDefaultFirstPageIndex xpack.securitySolution.endpointResultListDefaultPageSize xpack.securitySolution.maxRuleImportExportSize diff --git a/x-pack/plugins/security_solution/server/config.test.ts b/x-pack/plugins/security_solution/server/config.test.ts new file mode 100644 index 000000000000..67956acd6656 --- /dev/null +++ b/x-pack/plugins/security_solution/server/config.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { configSchema } from './config'; + +describe('config', () => { + describe('alertIgnoreFields', () => { + test('should default to an empty array', () => { + expect(configSchema.validate({}).alertIgnoreFields).toEqual([]); + }); + + test('should accept an array of strings', () => { + expect( + configSchema.validate({ alertIgnoreFields: ['foo.bar', 'mars.bar'] }).alertIgnoreFields + ).toEqual(['foo.bar', 'mars.bar']); + }); + + test('should throw if a non string is being sent in', () => { + expect( + () => + configSchema.validate({ + alertIgnoreFields: 5, + }).alertIgnoreFields + ).toThrow('[alertIgnoreFields]: expected value of type [array] but got [number]'); + }); + + test('should throw if we send in an invalid regular expression as a string', () => { + expect( + () => + configSchema.validate({ + alertIgnoreFields: ['/(/'], + }).alertIgnoreFields + ).toThrow( + '[alertIgnoreFields]: "Invalid regular expression: /(/: Unterminated group" at array position 0' + ); + }); + + test('should throw with two errors if we send two invalid regular expressions', () => { + expect( + () => + configSchema.validate({ + alertIgnoreFields: ['/(/', '/(invalid/'], + }).alertIgnoreFields + ).toThrow( + '[alertIgnoreFields]: "Invalid regular expression: /(/: Unterminated group" at array position 0. "Invalid regular expression: /(invalid/: Unterminated group" at array position 1' + ); + }); + + test('should throw with two errors with a valid string mixed in if we send two invalid regular expressions', () => { + expect( + () => + configSchema.validate({ + alertIgnoreFields: ['/(/', 'valid.string', '/(invalid/'], + }).alertIgnoreFields + ).toThrow( + '[alertIgnoreFields]: "Invalid regular expression: /(/: Unterminated group" at array position 0. "Invalid regular expression: /(invalid/: Unterminated group" at array position 2' + ); + }); + + test('should accept a valid regular expression within the string', () => { + expect( + configSchema.validate({ + alertIgnoreFields: ['/(.*)/'], + }).alertIgnoreFields + ).toEqual(['/(.*)/']); + }); + + test('should accept two valid regular expressions', () => { + expect( + configSchema.validate({ + alertIgnoreFields: ['/(.*)/', '/(.valid*)/'], + }).alertIgnoreFields + ).toEqual(['/(.*)/', '/(.valid*)/']); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/config.ts b/x-pack/plugins/security_solution/server/config.ts index a1c6601520a5..0850e43b21ed 100644 --- a/x-pack/plugins/security_solution/server/config.ts +++ b/x-pack/plugins/security_solution/server/config.ts @@ -21,12 +21,61 @@ export const configSchema = schema.object({ maxRuleImportPayloadBytes: schema.number({ defaultValue: 10485760 }), maxTimelineImportExportSize: schema.number({ defaultValue: 10000 }), maxTimelineImportPayloadBytes: schema.number({ defaultValue: 10485760 }), + + /** + * This is used within the merge strategies: + * server/lib/detection_engine/signals/source_fields_merging + * + * For determining which strategy for merging "fields" and "_source" together to get + * runtime fields, constant keywords, etc... + * + * "missingFields" (default) This will only merge fields that are missing from the _source and exist in the fields. + * "noFields" This will turn off all merging of runtime fields, constant keywords from fields. + * "allFields" This will merge and overwrite anything found within "fields" into "_source" before indexing the data. + */ alertMergeStrategy: schema.oneOf( [schema.literal('allFields'), schema.literal('missingFields'), schema.literal('noFields')], { defaultValue: 'missingFields', } ), + + /** + * This is used within the merge strategies: + * server/lib/detection_engine/signals/source_fields_merging + * + * For determining if we need to ignore particular "fields" and not merge them with "_source" such as + * runtime fields, constant keywords, etc... + * + * This feature and functionality is mostly as "safety feature" meaning that we have had bugs in the past + * where something down the stack unexpectedly ends up in the fields API which causes documents to not + * be indexable. Rather than changing alertMergeStrategy to be "noFields", you can use this array to add + * any problematic values. + * + * You can use plain dotted notation strings such as "host.name" or a regular expression such as "/host\..+/" + */ + alertIgnoreFields: schema.arrayOf(schema.string(), { + defaultValue: [], + validate(ignoreFields) { + const errors = ignoreFields.flatMap((ignoreField, index) => { + if (ignoreField.startsWith('/') && ignoreField.endsWith('/')) { + try { + new RegExp(ignoreField.slice(1, -1)); + return []; + } catch (error) { + return [`"${error.message}" at array position ${index}`]; + } + } else { + return []; + } + }); + if (errors.length !== 0) { + return errors.join('. '); + } else { + return undefined; + } + }, + }), [SIGNALS_INDEX_KEY]: schema.string({ defaultValue: DEFAULT_SIGNALS_INDEX }), /** diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts index a768273c9d14..1ac85f9a2796 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts @@ -26,6 +26,7 @@ export const createMockConfig = (): ConfigType => ({ endpointResultListDefaultPageSize: 10, packagerTaskInterval: '60s', alertMergeStrategy: 'missingFields', + alertIgnoreFields: [], prebuiltRulesFromFileSystem: true, prebuiltRulesFromSavedObjects: false, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_factory.ts index 879d776f83df..3992c3afaa30 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_factory.ts @@ -40,6 +40,7 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({ lists, logger, mergeStrategy, + ignoreFields, ruleDataClient, ruleDataService, }) => (type) => { @@ -208,6 +209,7 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({ const wrapHits = wrapHitsFactory({ logger, + ignoreFields, mergeStrategy, ruleSO, spaceId, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts index ae2ebc787451..c09d707fe484 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts @@ -36,10 +36,11 @@ export const buildBulkBody = ( ruleSO: SavedObject, doc: SignalSourceHit, mergeStrategy: ConfigType['alertMergeStrategy'], + ignoreFields: ConfigType['alertIgnoreFields'], applyOverrides: boolean, buildReasonMessage: BuildReasonMessage ): RACAlert => { - const mergedDoc = getMergeStrategy(mergeStrategy)({ doc }); + const mergedDoc = getMergeStrategy(mergeStrategy)({ doc, ignoreFields }); const rule = applyOverrides ? buildRuleWithOverrides(ruleSO, mergedDoc._source ?? {}) : buildRuleWithoutOverrides(ruleSO); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts index 62946c52b7f4..95c7b4e90b29 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts @@ -7,7 +7,7 @@ import { Logger } from 'kibana/server'; -import { SearchAfterAndBulkCreateParams, SignalSourceHit, WrapHits } from '../../signals/types'; +import { SearchAfterAndBulkCreateParams, WrapHits } from '../../signals/types'; import { buildBulkBody } from './utils/build_bulk_body'; import { generateId } from '../../signals/utils'; import { filterDuplicateSignals } from '../../signals/filter_duplicate_signals'; @@ -16,6 +16,7 @@ import { WrappedRACAlert } from '../types'; export const wrapHitsFactory = ({ logger, + ignoreFields, mergeStrategy, ruleSO, spaceId, @@ -23,6 +24,7 @@ export const wrapHitsFactory = ({ logger: Logger; ruleSO: SearchAfterAndBulkCreateParams['ruleSO']; mergeStrategy: ConfigType['alertMergeStrategy']; + ignoreFields: ConfigType['alertIgnoreFields']; spaceId: string | null | undefined; }): WrapHits => (events, buildReasonMessage) => { try { @@ -38,8 +40,9 @@ export const wrapHitsFactory = ({ _source: buildBulkBody( spaceId, ruleSO, - doc as SignalSourceHit, + doc, mergeStrategy, + ignoreFields, true, buildReasonMessage ), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.test.ts index f13a5a5e0e71..fe836c872dca 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.test.ts @@ -56,6 +56,7 @@ describe('Indicator Match Alerts', () => { experimentalFeatures: allowedExperimentalValues, lists: dependencies.lists, logger: dependencies.logger, + ignoreFields: [], mergeStrategy: 'allFields', ruleDataClient: dependencies.ruleDataClient, ruleDataService: dependencies.ruleDataService, @@ -97,6 +98,7 @@ describe('Indicator Match Alerts', () => { lists: dependencies.lists, logger: dependencies.logger, mergeStrategy: 'allFields', + ignoreFields: [], ruleDataClient: dependencies.ruleDataClient, ruleDataService: dependencies.ruleDataService, version: '1.0.0', @@ -135,6 +137,7 @@ describe('Indicator Match Alerts', () => { lists: dependencies.lists, logger: dependencies.logger, mergeStrategy: 'allFields', + ignoreFields: [], ruleDataClient: dependencies.ruleDataClient, ruleDataService: dependencies.ruleDataService, version: '1.0.0', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts index 71acc2e1cee8..f2dfe69debed 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts @@ -19,6 +19,7 @@ export const createIndicatorMatchAlertType = (createOptions: CreateRuleOptions) lists, logger, mergeStrategy, + ignoreFields, ruleDataClient, version, ruleDataService, @@ -27,6 +28,7 @@ export const createIndicatorMatchAlertType = (createOptions: CreateRuleOptions) lists, logger, mergeStrategy, + ignoreFields, ruleDataClient, ruleDataService, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.test.ts index 40566ffa04e6..23cd2e94aedf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.test.ts @@ -98,6 +98,7 @@ describe('Machine Learning Alerts', () => { lists: dependencies.lists, logger: dependencies.logger, mergeStrategy: 'allFields', + ignoreFields: [], ml: mlMock, ruleDataClient: dependencies.ruleDataClient, ruleDataService: dependencies.ruleDataService, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts index 1d872df35de3..cdaeb4be76d0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts @@ -14,11 +14,20 @@ import { createSecurityRuleTypeFactory } from '../create_security_rule_type_fact import { CreateRuleOptions } from '../types'; export const createMlAlertType = (createOptions: CreateRuleOptions) => { - const { lists, logger, mergeStrategy, ml, ruleDataClient, ruleDataService } = createOptions; + const { + lists, + logger, + mergeStrategy, + ignoreFields, + ml, + ruleDataClient, + ruleDataService, + } = createOptions; const createSecurityRuleType = createSecurityRuleTypeFactory({ lists, logger, mergeStrategy, + ignoreFields, ruleDataClient, ruleDataService, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts index 903cf6adadd4..ed791af08890 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts @@ -32,6 +32,7 @@ describe('Custom query alerts', () => { lists: dependencies.lists, logger: dependencies.logger, mergeStrategy: 'allFields', + ignoreFields: [], ruleDataClient: dependencies.ruleDataClient, ruleDataService: dependencies.ruleDataService, version: '1.0.0', @@ -79,6 +80,7 @@ describe('Custom query alerts', () => { lists: dependencies.lists, logger: dependencies.logger, mergeStrategy: 'allFields', + ignoreFields: [], ruleDataClient: dependencies.ruleDataClient, ruleDataService: dependencies.ruleDataService, version: '1.0.0', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts index e59037f38ce5..2f185853754b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts @@ -19,6 +19,7 @@ export const createQueryAlertType = (createOptions: CreateRuleOptions) => { lists, logger, mergeStrategy, + ignoreFields, ruleDataClient, version, ruleDataService, @@ -27,6 +28,7 @@ export const createQueryAlertType = (createOptions: CreateRuleOptions) => { lists, logger, mergeStrategy, + ignoreFields, ruleDataClient, ruleDataService, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts index f061240c4a6e..d50ab566c75c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts @@ -96,6 +96,7 @@ export type CreateSecurityRuleTypeFactory = (options: { lists: SetupPlugins['lists']; logger: Logger; mergeStrategy: ConfigType['alertMergeStrategy']; + ignoreFields: ConfigType['alertIgnoreFields']; ruleDataClient: IRuleDataClient; ruleDataService: IRuleDataPluginService; }) => < @@ -124,6 +125,7 @@ export interface CreateRuleOptions { lists: SetupPlugins['lists']; logger: Logger; mergeStrategy: ConfigType['alertMergeStrategy']; + ignoreFields: ConfigType['alertIgnoreFields']; ml?: SetupPlugins['ml']; ruleDataClient: IRuleDataClient; version: string; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts index 206f3ae59d24..5f392bed75f7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -43,6 +43,7 @@ describe('buildBulkBody', () => { ruleSO, doc, 'missingFields', + [], buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test @@ -114,6 +115,7 @@ describe('buildBulkBody', () => { ruleSO, doc, 'missingFields', + [], buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test @@ -199,6 +201,7 @@ describe('buildBulkBody', () => { ruleSO, doc, 'missingFields', + [], buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test @@ -270,6 +273,7 @@ describe('buildBulkBody', () => { ruleSO, doc, 'missingFields', + [], buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test @@ -338,6 +342,7 @@ describe('buildBulkBody', () => { ruleSO, doc, 'missingFields', + [], buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test @@ -405,6 +410,7 @@ describe('buildBulkBody', () => { ruleSO, doc, 'missingFields', + [], buildReasonMessage ); const expected: Omit & { someKey: string } = { @@ -468,6 +474,7 @@ describe('buildBulkBody', () => { ruleSO, doc, 'missingFields', + [], buildReasonMessage ); const expected: Omit & { someKey: string } = { @@ -712,6 +719,7 @@ describe('buildSignalFromEvent', () => { ruleSO, true, 'missingFields', + [], buildReasonMessage ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts index a4e812e8f111..f8e39964523d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts @@ -37,9 +37,10 @@ export const buildBulkBody = ( ruleSO: SavedObject, doc: SignalSourceHit, mergeStrategy: ConfigType['alertMergeStrategy'], + ignoreFields: ConfigType['alertIgnoreFields'], buildReasonMessage: BuildReasonMessage ): SignalHit => { - const mergedDoc = getMergeStrategy(mergeStrategy)({ doc }); + const mergedDoc = getMergeStrategy(mergeStrategy)({ doc, ignoreFields }); const rule = buildRuleWithOverrides(ruleSO, mergedDoc._source ?? {}); const timestamp = new Date().toISOString(); const reason = buildReasonMessage({ mergedDoc, rule }); @@ -76,11 +77,19 @@ export const buildSignalGroupFromSequence = ( ruleSO: SavedObject, outputIndex: string, mergeStrategy: ConfigType['alertMergeStrategy'], + ignoreFields: ConfigType['alertIgnoreFields'], buildReasonMessage: BuildReasonMessage ): WrappedSignalHit[] => { const wrappedBuildingBlocks = wrapBuildingBlocks( sequence.events.map((event) => { - const signal = buildSignalFromEvent(event, ruleSO, false, mergeStrategy, buildReasonMessage); + const signal = buildSignalFromEvent( + event, + ruleSO, + false, + mergeStrategy, + ignoreFields, + buildReasonMessage + ); signal.signal.rule.building_block_type = 'default'; return signal; }), @@ -146,9 +155,10 @@ export const buildSignalFromEvent = ( ruleSO: SavedObject, applyOverrides: boolean, mergeStrategy: ConfigType['alertMergeStrategy'], + ignoreFields: ConfigType['alertIgnoreFields'], buildReasonMessage: BuildReasonMessage ): SignalHit => { - const mergedEvent = getMergeStrategy(mergeStrategy)({ doc: event }); + const mergedEvent = getMergeStrategy(mergeStrategy)({ doc: event, ignoreFields }); const rule = applyOverrides ? buildRuleWithOverrides(ruleSO, mergedEvent._source ?? {}) : buildRuleWithoutOverrides(ruleSO); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 8bf0c986b9c2..55a184a1c0bc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -74,6 +74,7 @@ describe('searchAfterAndBulkCreate', () => { ruleSO, signalsIndex: DEFAULT_SIGNALS_INDEX, mergeStrategy: 'missingFields', + ignoreFields: [], }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 39728235db39..9af8680ec726 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -195,6 +195,7 @@ describe('signal_rule_alert_type', () => { ml: mlMock, lists: listMock.createSetup(), mergeStrategy: 'missingFields', + ignoreFields: [], ruleDataService, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 1c4efea0a1d5..68d60f7757e4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -82,6 +82,7 @@ export const signalRulesAlertType = ({ ml, lists, mergeStrategy, + ignoreFields, ruleDataService, }: { logger: Logger; @@ -91,6 +92,7 @@ export const signalRulesAlertType = ({ ml: SetupPlugins['ml']; lists: SetupPlugins['lists'] | undefined; mergeStrategy: ConfigType['alertMergeStrategy']; + ignoreFields: ConfigType['alertIgnoreFields']; ruleDataService: IRuleDataPluginService; }): SignalRuleAlertTypeDefinition => { return { @@ -275,12 +277,14 @@ export const signalRulesAlertType = ({ ruleSO: savedObject, signalsIndex: params.outputIndex, mergeStrategy, + ignoreFields, }); const wrapSequences = wrapSequencesFactory({ ruleSO: savedObject, signalsIndex: params.outputIndex, mergeStrategy, + ignoreFields, }); if (isMlRule(type)) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_all_fields_with_source.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_all_fields_with_source.test.ts index b900ea268fd6..6af82d3a7102 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_all_fields_with_source.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_all_fields_with_source.test.ts @@ -44,7 +44,7 @@ describe('merge_all_fields_with_source', () => { test('when source is "undefined", merged doc is "undefined"', () => { const _source: SignalSourceHit['_source'] = {}; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -53,7 +53,7 @@ describe('merge_all_fields_with_source', () => { foo: [], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -62,7 +62,7 @@ describe('merge_all_fields_with_source', () => { foo: 'value', }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -71,7 +71,7 @@ describe('merge_all_fields_with_source', () => { foo: ['value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -80,7 +80,7 @@ describe('merge_all_fields_with_source', () => { foo: ['value_1', 'value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -89,7 +89,7 @@ describe('merge_all_fields_with_source', () => { foo: { bar: 'some value' }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -98,7 +98,7 @@ describe('merge_all_fields_with_source', () => { foo: [{ bar: 'some value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -107,7 +107,7 @@ describe('merge_all_fields_with_source', () => { foo: [{ bar: 'some value' }, { foo: 'some other value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -133,7 +133,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -142,7 +142,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': 'value', }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -151,7 +151,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -160,7 +160,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['value_1', 'value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -169,7 +169,7 @@ describe('merge_all_fields_with_source', () => { foo: { bar: 'some value' }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -178,7 +178,7 @@ describe('merge_all_fields_with_source', () => { foo: [{ bar: 'some value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -187,7 +187,7 @@ describe('merge_all_fields_with_source', () => { foo: [{ bar: 'some value' }, { foo: 'some other value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -217,7 +217,7 @@ describe('merge_all_fields_with_source', () => { foo: { bar: [] }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -226,7 +226,7 @@ describe('merge_all_fields_with_source', () => { foo: { bar: 'value' }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -235,7 +235,7 @@ describe('merge_all_fields_with_source', () => { foo: { bar: ['value'] }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -244,7 +244,7 @@ describe('merge_all_fields_with_source', () => { foo: { bar: ['value_1', 'value_2'] }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -253,7 +253,7 @@ describe('merge_all_fields_with_source', () => { foo: { bar: { mars: 'some value' } }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -262,7 +262,7 @@ describe('merge_all_fields_with_source', () => { foo: { bar: [{ mars: 'some value' }] }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -271,7 +271,7 @@ describe('merge_all_fields_with_source', () => { foo: { bar: [{ mars: 'some value' }, { mars: 'some other value' }] }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -299,7 +299,7 @@ describe('merge_all_fields_with_source', () => { 'bar.foo': [], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -308,7 +308,7 @@ describe('merge_all_fields_with_source', () => { 'bar.foo': 'value', }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -317,7 +317,7 @@ describe('merge_all_fields_with_source', () => { 'bar.foo': ['value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -326,7 +326,7 @@ describe('merge_all_fields_with_source', () => { 'bar.foo': ['value_1', 'value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -335,7 +335,7 @@ describe('merge_all_fields_with_source', () => { foo: { bar: 'some value' }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -344,7 +344,7 @@ describe('merge_all_fields_with_source', () => { foo: [{ bar: 'some value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -353,7 +353,7 @@ describe('merge_all_fields_with_source', () => { foo: [{ bar: 'some value' }, { foo: 'some other value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -376,7 +376,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: 'other_value_1', @@ -389,7 +389,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: ['other_value_1', 'other_value_2'], @@ -402,7 +402,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: { zed: 'other_value_1' } }, }); @@ -413,7 +413,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }, { zed: 'other_value_2' }] }, }); @@ -440,7 +440,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: 'other_value_1', @@ -453,7 +453,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: ['other_value_1', 'other_value_2'] }, }); @@ -464,7 +464,7 @@ describe('merge_all_fields_with_source', () => { foo: [{ bar: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: 'other_value_1', @@ -477,7 +477,7 @@ describe('merge_all_fields_with_source', () => { foo: [{ bar: 'other_value_1' }, { bar: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: [{ bar: 'other_value_1' }, { bar: 'other_value_2' }], }); @@ -503,7 +503,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ 'foo.bar': 'other_value_1', }); @@ -514,7 +514,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -523,7 +523,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ 'foo.bar': { zed: 'other_value_1' }, }); @@ -534,7 +534,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); }); @@ -560,7 +560,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: ['other_value_1'] }, }); @@ -571,7 +571,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: ['other_value_1', 'other_value_2'] }, }); @@ -582,7 +582,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }] }, }); @@ -593,7 +593,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }, { zed: 'other_value_2' }] }, }); @@ -619,7 +619,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -628,7 +628,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -637,7 +637,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -646,7 +646,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); }); @@ -670,7 +670,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: ['other_value_1'] }, }); @@ -681,7 +681,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: ['other_value_1', 'other_value_2'] }, }); @@ -692,7 +692,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }] }, }); @@ -703,7 +703,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }, { zed: 'other_value_2' }] }, }); @@ -729,7 +729,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -738,7 +738,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -747,7 +747,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -756,7 +756,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); }); @@ -782,7 +782,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: ['other_value_1'], @@ -795,7 +795,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: ['other_value_1', 'other_value_2'], @@ -808,7 +808,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }], @@ -821,7 +821,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], @@ -849,7 +849,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -858,7 +858,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -867,7 +867,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -876,7 +876,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); }); @@ -902,7 +902,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -911,7 +911,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -920,7 +920,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: { zed: 'other_value_1' } }, }); @@ -931,7 +931,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }, { zed: 'other_value_2' }] }, }); @@ -957,7 +957,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -966,7 +966,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -975,7 +975,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ 'foo.bar': { mars: 'other_value_1' }, }); @@ -986,7 +986,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }, { mars: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ 'foo.bar': [{ mars: 'other_value_1' }, { mars: 'other_value_2' }], }); @@ -1014,7 +1014,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1023,7 +1023,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1032,7 +1032,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }] }, }); @@ -1043,7 +1043,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }, { zed: 'other_value_2' }] }, }); @@ -1069,7 +1069,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1078,7 +1078,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1087,7 +1087,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }] }, }); @@ -1098,7 +1098,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: [{ zed: 'other_value_1' }, { zed: 'other_value_2' }] }, }); @@ -1124,7 +1124,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1133,7 +1133,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1142,7 +1142,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -1151,7 +1151,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }, { mars: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); }); @@ -1175,7 +1175,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1184,7 +1184,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1193,7 +1193,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); @@ -1202,7 +1202,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }, { mars: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(fields); }); }); @@ -1228,7 +1228,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: 'value_1' }, 'foo.bar': 'other_value_1', @@ -1243,7 +1243,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['value_1', 'value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: 'value_1' }, // <--- We have duplicated value_1 twice which is a bug 'foo.bar': ['value_1', 'value_2'], // <-- We have merged the array value because we do not understand if we should or not @@ -1270,7 +1270,7 @@ describe('merge_all_fields_with_source', () => { 'bar.keyword': ['bar_other_value_keyword_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: 'foo_other_value_1', bar: 'bar_other_value_1', @@ -1291,7 +1291,7 @@ describe('merge_all_fields_with_source', () => { 'host.hostname.keyword': ['hostname_other_value_keyword_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ host: { hostname: 'hostname_other_value_1', @@ -1316,7 +1316,7 @@ describe('merge_all_fields_with_source', () => { 'foo.host.hostname.keyword': ['hostname_other_value_keyword_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { host: { @@ -1334,7 +1334,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar': ['other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: 'other_value_1', }); @@ -1354,7 +1354,7 @@ describe('merge_all_fields_with_source', () => { 'host.hostname.keyword': ['hostname_other_value_keyword_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ 'host.name': 'host_name_other_value_1', 'host.hostname': 'hostname_other_value_1', @@ -1373,7 +1373,7 @@ describe('merge_all_fields_with_source', () => { 'foo.host.hostname.keyword': ['hostname_other_value_keyword_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ 'foo.host.name': 'host_name_other_value_1', 'foo.host.hostname': 'hostname_other_value_1', @@ -1388,7 +1388,7 @@ describe('merge_all_fields_with_source', () => { 'foo.bar.zed': ['zed_other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: 'other_value_1', }); @@ -1415,7 +1415,7 @@ describe('merge_all_fields_with_source', () => { 'foo.mars': ['other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1435,7 +1435,7 @@ describe('merge_all_fields_with_source', () => { 'foo.zed.mars': ['other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -1450,7 +1450,7 @@ describe('merge_all_fields_with_source', () => { foo: [{ bar: ['single_value'], zed: ['single_value'] }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: 'single_value', zed: 'single_value' }, }); @@ -1469,10 +1469,132 @@ describe('merge_all_fields_with_source', () => { foo: [{ bar: ['single_value'], zed: ['single_value'] }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeAllFieldsWithSource({ doc })._source; + const merged = mergeAllFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: [{ bar: ['single_value'], zed: ['single_value'] }], }); }); }); + + /** + * Small set of tests to ensure that ignore fields are wired up at the strategy level + */ + describe('ignore fields', () => { + test('Does not merge an ignored field if it does not exist already in the _source', () => { + const _source: SignalSourceHit['_source'] = {}; + const fields: SignalSourceHit['fields'] = { + 'foo.bar': ['other_value_1'], + 'value.should.ignore': ['other_value_2'], // string value should ignore this + '_odd.value': ['other_value_2'], // Regex should ignore this value of: /[_]+/ + }; + const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; + const merged = mergeAllFieldsWithSource({ + doc, + ignoreFields: ['value.should.ignore', '/[_]+/'], + })._source; + expect(merged).toEqual({ + foo: { + bar: 'other_value_1', + }, + }); + }); + + test('Does merge fields when no matching happens', () => { + const _source: SignalSourceHit['_source'] = {}; + const fields: SignalSourceHit['fields'] = { + 'foo.bar': ['other_value_1'], + 'value.should.work': ['other_value_2'], + '_odd.value': ['other_value_2'], + }; + const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; + const merged = mergeAllFieldsWithSource({ + doc, + ignoreFields: ['other.string', '/[z]+/'], // Neither of these two should match anything + })._source; + expect(merged).toEqual({ + foo: { + bar: 'other_value_1', + }, + _odd: { + value: 'other_value_2', + }, + value: { + should: { + work: 'other_value_2', + }, + }, + }); + }); + + test('Does not update an ignored field and keeps the original value if it matches in the ignoreFields', () => { + const _source: SignalSourceHit['_source'] = { + 'value.should.ignore': ['value_1'], + '_odd.value': ['value_2'], + }; + const fields: SignalSourceHit['fields'] = { + 'foo.bar': ['other_value_1'], + 'value.should.ignore': ['other_value_2'], // string value should ignore this + '_odd.value': ['other_value_2'], // Regex should ignore this value of: /[_]+/ + }; + const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; + const merged = mergeAllFieldsWithSource({ + doc, + ignoreFields: ['value.should.ignore', '/[_]+/'], + })._source; + expect(merged).toEqual({ + foo: { + bar: 'other_value_1', + }, + 'value.should.ignore': ['value_1'], + '_odd.value': ['value_2'], + }); + }); + + test('Does not ignore anything when no matching happens and overwrites the expected fields', () => { + const _source: SignalSourceHit['_source'] = { + 'value.should.ignore': ['value_1'], + '_odd.value': ['value_2'], + }; + const fields: SignalSourceHit['fields'] = { + 'foo.bar': ['other_value_1'], + 'value.should.ignore': ['other_value_2'], + '_odd.value': ['other_value_2'], + }; + const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; + const merged = mergeAllFieldsWithSource({ + doc, + ignoreFields: ['nothing.to.match', '/[z]+/'], // these match nothing + })._source; + expect(merged).toEqual({ + foo: { + bar: 'other_value_1', + }, + 'value.should.ignore': ['other_value_2'], + '_odd.value': ['other_value_2'], + }); + }); + }); + + /** + * Test that the EQL bug workaround is wired up. Remove this once the bug is fixed. + */ + describe('Works around EQL bug 77152 (https://github.com/elastic/elasticsearch/issues/77152)', () => { + test('Does not merge field that contains _ignored', () => { + const _source: SignalSourceHit['_source'] = {}; + const fields: SignalSourceHit['fields'] = { + 'foo.bar': ['other_value_1'], + _ignored: ['other_value_1'], + }; + const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; + const merged = mergeAllFieldsWithSource({ + doc, + ignoreFields: [], + })._source; + expect(merged).toEqual({ + foo: { + bar: 'other_value_1', + }, + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_all_fields_with_source.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_all_fields_with_source.ts index da2eea9d2c61..ade83b88d526 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_all_fields_with_source.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_all_fields_with_source.ts @@ -23,14 +23,16 @@ import { isTypeObject } from '../utils/is_type_object'; * on this function and the general strategies. * * @param doc The document with "_source" and "fields" - * @param throwOnFailSafe Defaults to false, but if set to true it will cause a throw if the fail safe is triggered to indicate we need to add a new explicit test condition + * @param ignoreFields Any fields that we should ignore and never merge from "fields". If the value exists + * within doc._source it will be untouched and used. If the value does not exist within the doc._source, + * it will not be added from fields. * @returns The two merged together in one object where we can */ -export const mergeAllFieldsWithSource: MergeStrategyFunction = ({ doc }) => { +export const mergeAllFieldsWithSource: MergeStrategyFunction = ({ doc, ignoreFields }) => { const source = doc._source ?? {}; const fields = doc.fields ?? {}; const fieldEntries = Object.entries(fields); - const filteredEntries = filterFieldEntries(fieldEntries); + const filteredEntries = filterFieldEntries(fieldEntries, ignoreFields); const transformedSource = filteredEntries.reduce( (merged, [fieldsKey, fieldsValue]: [string, FieldsType]) => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_missing_fields_with_source.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_missing_fields_with_source.test.ts index 70d1e79580e8..612bff75792d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_missing_fields_with_source.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_missing_fields_with_source.test.ts @@ -44,7 +44,7 @@ describe('merge_missing_fields_with_source', () => { test('when source is "undefined", merged doc is "undefined"', () => { const _source: SignalSourceHit['_source'] = {}; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -53,7 +53,7 @@ describe('merge_missing_fields_with_source', () => { foo: [], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -62,7 +62,7 @@ describe('merge_missing_fields_with_source', () => { foo: 'value', }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -71,7 +71,7 @@ describe('merge_missing_fields_with_source', () => { foo: ['value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -80,7 +80,7 @@ describe('merge_missing_fields_with_source', () => { foo: ['value_1', 'value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -89,7 +89,7 @@ describe('merge_missing_fields_with_source', () => { foo: { bar: 'some value' }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -98,7 +98,7 @@ describe('merge_missing_fields_with_source', () => { foo: [{ bar: 'some value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -107,7 +107,7 @@ describe('merge_missing_fields_with_source', () => { foo: [{ bar: 'some value' }, { foo: 'some other value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -133,7 +133,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -142,7 +142,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': 'value', }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -151,7 +151,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -160,7 +160,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['value_1', 'value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -169,7 +169,7 @@ describe('merge_missing_fields_with_source', () => { foo: { bar: 'some value' }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -178,7 +178,7 @@ describe('merge_missing_fields_with_source', () => { foo: [{ bar: 'some value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -187,7 +187,7 @@ describe('merge_missing_fields_with_source', () => { foo: [{ bar: 'some value' }, { foo: 'some other value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -217,7 +217,7 @@ describe('merge_missing_fields_with_source', () => { foo: { bar: [] }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -226,7 +226,7 @@ describe('merge_missing_fields_with_source', () => { foo: { bar: 'value' }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -235,7 +235,7 @@ describe('merge_missing_fields_with_source', () => { foo: { bar: ['value'] }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -244,7 +244,7 @@ describe('merge_missing_fields_with_source', () => { foo: { bar: ['value_1', 'value_2'] }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -253,7 +253,7 @@ describe('merge_missing_fields_with_source', () => { foo: { bar: { mars: 'some value' } }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -262,7 +262,7 @@ describe('merge_missing_fields_with_source', () => { foo: { bar: [{ mars: 'some value' }] }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -271,7 +271,7 @@ describe('merge_missing_fields_with_source', () => { foo: { bar: [{ mars: 'some value' }, { mars: 'some other value' }] }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -299,7 +299,7 @@ describe('merge_missing_fields_with_source', () => { 'bar.foo': [], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -308,7 +308,7 @@ describe('merge_missing_fields_with_source', () => { 'bar.foo': 'value', }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -317,7 +317,7 @@ describe('merge_missing_fields_with_source', () => { 'bar.foo': ['value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -326,7 +326,7 @@ describe('merge_missing_fields_with_source', () => { 'bar.foo': ['value_1', 'value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -335,7 +335,7 @@ describe('merge_missing_fields_with_source', () => { foo: { bar: 'some value' }, }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -344,7 +344,7 @@ describe('merge_missing_fields_with_source', () => { foo: [{ bar: 'some value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -353,7 +353,7 @@ describe('merge_missing_fields_with_source', () => { foo: [{ bar: 'some value' }, { foo: 'some other value' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -376,7 +376,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: 'other_value_1', @@ -389,7 +389,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: { bar: ['other_value_1', 'other_value_2'], @@ -402,7 +402,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({}); }); @@ -411,7 +411,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({}); }); }); @@ -436,7 +436,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -445,7 +445,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -454,7 +454,7 @@ describe('merge_missing_fields_with_source', () => { foo: [{ bar: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -463,7 +463,7 @@ describe('merge_missing_fields_with_source', () => { foo: [{ bar: 'other_value_1' }, { bar: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -487,7 +487,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -496,7 +496,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -505,7 +505,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -514,7 +514,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -540,7 +540,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -549,7 +549,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -558,7 +558,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -567,7 +567,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -591,7 +591,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -600,7 +600,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -609,7 +609,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -618,7 +618,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -642,7 +642,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -651,7 +651,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -660,7 +660,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -669,7 +669,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -693,7 +693,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -702,7 +702,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -711,7 +711,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -720,7 +720,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -746,7 +746,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -755,7 +755,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -764,7 +764,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -773,7 +773,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -797,7 +797,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -806,7 +806,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -815,7 +815,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -824,7 +824,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -850,7 +850,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -859,7 +859,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -868,7 +868,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -877,7 +877,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -901,7 +901,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -910,7 +910,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -919,7 +919,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -928,7 +928,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }, { mars: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -954,7 +954,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -963,7 +963,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -972,7 +972,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -981,7 +981,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -1005,7 +1005,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1014,7 +1014,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1023,7 +1023,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1032,7 +1032,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ zed: 'other_value_1' }, { zed: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -1056,7 +1056,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1065,7 +1065,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1074,7 +1074,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1083,7 +1083,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }, { mars: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -1107,7 +1107,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1116,7 +1116,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1', 'other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1125,7 +1125,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1134,7 +1134,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': [{ mars: 'other_value_1' }, { mars: 'other_value_2' }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -1160,7 +1160,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1172,7 +1172,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['value_1', 'value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -1196,7 +1196,7 @@ describe('merge_missing_fields_with_source', () => { 'bar.keyword': ['bar_other_value_keyword_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1214,7 +1214,7 @@ describe('merge_missing_fields_with_source', () => { 'host.hostname.keyword': ['hostname_other_value_keyword_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1234,7 +1234,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.host.hostname.keyword': ['hostname_other_value_keyword_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1245,7 +1245,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar': ['other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: 'other_value_1', }); @@ -1265,7 +1265,7 @@ describe('merge_missing_fields_with_source', () => { 'host.hostname.keyword': ['hostname_other_value_keyword_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1281,7 +1281,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.host.hostname.keyword': ['hostname_other_value_keyword_1'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1293,7 +1293,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.bar.zed': ['zed_other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({ foo: 'other_value_1', }); @@ -1320,7 +1320,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.mars': ['other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); @@ -1340,7 +1340,7 @@ describe('merge_missing_fields_with_source', () => { 'foo.zed.mars': ['other_value_2'], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); @@ -1355,7 +1355,7 @@ describe('merge_missing_fields_with_source', () => { foo: [{ bar: ['single_value'], zed: ['single_value'] }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual({}); }); @@ -1372,8 +1372,82 @@ describe('merge_missing_fields_with_source', () => { foo: [{ bar: ['single_value'], zed: ['single_value'] }], }; const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; - const merged = mergeMissingFieldsWithSource({ doc })._source; + const merged = mergeMissingFieldsWithSource({ doc, ignoreFields: [] })._source; expect(merged).toEqual(_source); }); }); + + /** + * Small set of tests to ensure that ignore fields are wired up at the strategy level + */ + describe('ignore fields', () => { + test('Does not merge an ignored field if it does not exist already in the _source', () => { + const _source: SignalSourceHit['_source'] = {}; + const fields: SignalSourceHit['fields'] = { + 'foo.bar': ['other_value_1'], + 'value.should.ignore': ['other_value_2'], // string value should ignore this + '_odd.value': ['other_value_2'], // Regex should ignore this value of: /[_]+/ + }; + const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; + const merged = mergeMissingFieldsWithSource({ + doc, + ignoreFields: ['value.should.ignore', '/[_]+/'], + })._source; + expect(merged).toEqual({ + foo: { + bar: 'other_value_1', + }, + }); + }); + + test('Does merge fields when no matching happens', () => { + const _source: SignalSourceHit['_source'] = {}; + const fields: SignalSourceHit['fields'] = { + 'foo.bar': ['other_value_1'], + 'value.should.work': ['other_value_2'], + '_odd.value': ['other_value_2'], + }; + const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; + const merged = mergeMissingFieldsWithSource({ + doc, + ignoreFields: ['other.string', '/[z]+/'], // Neither of these two should match anything + })._source; + expect(merged).toEqual({ + foo: { + bar: 'other_value_1', + }, + _odd: { + value: 'other_value_2', + }, + value: { + should: { + work: 'other_value_2', + }, + }, + }); + }); + }); + + /** + * Test that the EQL bug workaround is wired up. Remove this once the bug is fixed. + */ + describe('Works around EQL bug 77152 (https://github.com/elastic/elasticsearch/issues/77152)', () => { + test('Does not merge field that contains _ignored', () => { + const _source: SignalSourceHit['_source'] = {}; + const fields: SignalSourceHit['fields'] = { + 'foo.bar': ['other_value_1'], + _ignored: ['other_value_1'], + }; + const doc: SignalSourceHit = { ...emptyEsResult(), _source, fields }; + const merged = mergeMissingFieldsWithSource({ + doc, + ignoreFields: [], + })._source; + expect(merged).toEqual({ + foo: { + bar: 'other_value_1', + }, + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_missing_fields_with_source.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_missing_fields_with_source.ts index b66c46ccbf0c..611a3ad87970 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_missing_fields_with_source.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_missing_fields_with_source.ts @@ -19,14 +19,16 @@ import { isNestedObject } from '../utils/is_nested_object'; * Merges only missing sections of "doc._source" with its "doc.fields" on a "best effort" basis. See ../README.md for more information * on this function and the general strategies. * @param doc The document with "_source" and "fields" - * @param throwOnFailSafe Defaults to false, but if set to true it will cause a throw if the fail safe is triggered to indicate we need to add a new explicit test condition + * @param ignoreFields Any fields that we should ignore and never merge from "fields". If the value exists + * within doc._source it will be untouched and used. If the value does not exist within the doc._source, + * it will not be added from fields. * @returns The two merged together in one object where we can */ -export const mergeMissingFieldsWithSource: MergeStrategyFunction = ({ doc }) => { +export const mergeMissingFieldsWithSource: MergeStrategyFunction = ({ doc, ignoreFields }) => { const source = doc._source ?? {}; const fields = doc.fields ?? {}; const fieldEntries = Object.entries(fields); - const filteredEntries = filterFieldEntries(fieldEntries); + const filteredEntries = filterFieldEntries(fieldEntries, ignoreFields); const transformedSource = filteredEntries.reduce( (merged, [fieldsKey, fieldsValue]: [string, FieldsType]) => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_no_fields.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_no_fields.ts index 6c2daf252671..5e26b619fbdf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_no_fields.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/strategies/merge_no_fields.ts @@ -10,6 +10,7 @@ import { MergeStrategyFunction } from '../types'; /** * Does nothing and does not merge source with fields * @param doc The doc to return and do nothing + * @param ignoreFields We do nothing with this value and ignore it if set * @returns The doc as a no operation and do nothing */ -export const mergeNoFields: MergeStrategyFunction = ({ doc }) => doc; +export const mergeNoFields: MergeStrategyFunction = ({ doc, ignoreFields }) => doc; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/types.ts index 1438d2844949..0b847064d5d6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/types.ts @@ -14,5 +14,13 @@ export type FieldsType = string[] | number[] | boolean[] | object[]; /** * The type of the merge strategy functions which must implement to be part of the strategy group + * @param doc The document to send in to merge + * @param ignoreFields Fields you want to ignore and not merge. */ -export type MergeStrategyFunction = ({ doc }: { doc: SignalSourceHit }) => SignalSourceHit; +export type MergeStrategyFunction = ({ + doc, + ignoreFields, +}: { + doc: SignalSourceHit; + ignoreFields: string[]; +}) => SignalSourceHit; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/filter_field_entries.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/filter_field_entries.test.ts index 9cc247829088..031a2013b462 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/filter_field_entries.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/filter_field_entries.test.ts @@ -27,7 +27,9 @@ describe('filter_field_entries', () => { test('returns a single valid fieldEntries as expected', () => { const fieldEntries: Array<[string, FieldsType]> = [['foo.bar', dummyValue]]; - expect(filterFieldEntries(fieldEntries)).toEqual(fieldEntries); + expect(filterFieldEntries(fieldEntries, [])).toEqual( + fieldEntries + ); }); test('removes invalid dotted entries', () => { @@ -37,7 +39,7 @@ describe('filter_field_entries', () => { ['..', dummyValue], ['foo..bar', dummyValue], ]; - expect(filterFieldEntries(fieldEntries)).toEqual([ + expect(filterFieldEntries(fieldEntries, [])).toEqual([ ['foo.bar', dummyValue], ]); }); @@ -49,7 +51,7 @@ describe('filter_field_entries', () => { ['bar.keyword', dummyValue], // <-- "bar.keyword" multi-field should be removed ['bar', dummyValue], ]; - expect(filterFieldEntries(fieldEntries)).toEqual([ + expect(filterFieldEntries(fieldEntries, [])).toEqual([ ['foo', dummyValue], ['bar', dummyValue], ]); @@ -62,7 +64,7 @@ describe('filter_field_entries', () => { ['host.hostname', dummyValue], ['host.hostname.keyword', dummyValue], // <-- multi-field should be removed ]; - expect(filterFieldEntries(fieldEntries)).toEqual([ + expect(filterFieldEntries(fieldEntries, [])).toEqual([ ['host.name', dummyValue], ['host.hostname', dummyValue], ]); @@ -75,9 +77,34 @@ describe('filter_field_entries', () => { ['foo.host.hostname', dummyValue], ['foo.host.hostname.keyword', dummyValue], // <-- multi-field should be removed ]; - expect(filterFieldEntries(fieldEntries)).toEqual([ + expect(filterFieldEntries(fieldEntries, [])).toEqual([ ['foo.host.name', dummyValue], ['foo.host.hostname', dummyValue], ]); }); + + test('ignores fields of "_ignore", for eql bug https://github.com/elastic/elasticsearch/issues/77152', () => { + const fieldEntries: Array<[string, FieldsType]> = [ + ['_ignored', dummyValue], + ['foo.host.hostname', dummyValue], + ]; + expect(filterFieldEntries(fieldEntries, [])).toEqual([ + ['foo.host.hostname', dummyValue], + ]); + }); + + test('ignores fields given strings and regular expressions in the ignoreFields list', () => { + const fieldEntries: Array<[string, FieldsType]> = [ + ['host.name', dummyValue], + ['user.name', dummyValue], // <-- string from ignoreFields should ignore this + ['host.hostname', dummyValue], + ['_odd.value', dummyValue], // <-- regular expression from ignoreFields should ignore this + ]; + expect( + filterFieldEntries(fieldEntries, ['user.name', '/[_]+/']) + ).toEqual([ + ['host.name', dummyValue], + ['host.hostname', dummyValue], + ]); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/filter_field_entries.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/filter_field_entries.ts index 221cdabc6284..4ee5fa1db52f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/filter_field_entries.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/filter_field_entries.ts @@ -9,6 +9,8 @@ import { isMultiField } from './is_multifield'; import { isInvalidKey } from './is_invalid_key'; import { isTypeObject } from './is_type_object'; import { FieldsType } from '../types'; +import { isIgnored } from './is_ignored'; +import { isEqlBug77152 } from './is_eql_bug_77152'; /** * Filters field entries by removing invalid field entries such as any invalid characters @@ -17,13 +19,18 @@ import { FieldsType } from '../types'; * those and don't try to merge those. * * @param fieldEntries The field entries to filter + * @param ignoreFields Array of fields to ignore. If a value starts and ends with "/", such as: "/[_]+/" then the field will be treated as a regular expression. + * If you have an object structure to ignore such as "{ a: { b: c: {} } } ", then you need to ignore it as the string "a.b.c" * @returns The field entries filtered */ export const filterFieldEntries = ( - fieldEntries: Array<[string, FieldsType]> + fieldEntries: Array<[string, FieldsType]>, + ignoreFields: string[] ): Array<[string, FieldsType]> => { return fieldEntries.filter(([fieldsKey, fieldsValue]: [string, FieldsType]) => { return ( + !isEqlBug77152(fieldsKey) && + !isIgnored(fieldsKey, ignoreFields) && !isInvalidKey(fieldsKey) && !isMultiField(fieldsKey, fieldEntries) && !isTypeObject(fieldsValue) // TODO: Look at not filtering this and instead transform it so it can be inserted correctly in the strategies which does an overwrite of everything from fields diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/index.ts index baf9efca511e..87b1097dd9bc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/index.ts @@ -7,6 +7,7 @@ export * from './array_in_path_exists'; export * from './filter_field_entries'; export * from './is_array_of_primitives'; +export * from './is_ignored'; export * from './is_invalid_key'; export * from './is_multifield'; export * from './is_nested_object'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_eql_bug_77152.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_eql_bug_77152.test.ts new file mode 100644 index 000000000000..47a56e096649 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_eql_bug_77152.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEqlBug77152 } from './is_eql_bug_77152'; + +/** + * @deprecated Remove this test once https://github.com/elastic/elasticsearch/issues/77152 is fixed. + */ +describe('is_eql_bug_77152', () => { + beforeAll(() => { + jest.resetAllMocks(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('returns true if it encounters the bug which is _ignored is returned in the fields', () => { + expect(isEqlBug77152('_ignored')).toEqual(true); + }); + + it('returns false if it encounters a normal field', () => { + expect(isEqlBug77152('some.field')).toEqual(false); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_eql_bug_77152.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_eql_bug_77152.ts new file mode 100644 index 000000000000..e9a642cd5c38 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_eql_bug_77152.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Ignores any field that is "_ignored". Customers are not allowed to have this field and more importantly this shows up as a bug + * from EQL as seen here: https://github.com/elastic/elasticsearch/issues/77152 + * Once this ticket is fixed, please remove this function. + * @param fieldsKey The fields key to match against "_ignored" + * @returns true if it is a "_ignored", otherwise false + * @deprecated Remove this once https://github.com/elastic/elasticsearch/issues/77152 is fixed. + */ +export const isEqlBug77152 = (fieldsKey: string): boolean => { + return fieldsKey === '_ignored'; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_ignored.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_ignored.test.ts new file mode 100644 index 000000000000..e4a7093ef127 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_ignored.test.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isIgnored } from './is_ignored'; + +describe('is_ignored', () => { + beforeAll(() => { + jest.resetAllMocks(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('string matching', () => { + test('it returns false if given an empty array', () => { + expect(isIgnored('simple.value', [])).toEqual(false); + }); + + test('it returns true if a simple string value matches', () => { + expect(isIgnored('simple.value', ['simple.value'])).toEqual(true); + }); + + test('it returns false if a simple string value does not match', () => { + expect(isIgnored('simple', ['simple.value'])).toEqual(false); + }); + + test('it returns true if a simple string value matches with two strings', () => { + expect(isIgnored('simple.value', ['simple.value', 'simple.second.value'])).toEqual(true); + }); + + test('it returns true if a simple string value matches the second string', () => { + expect(isIgnored('simple.second.value', ['simple.value', 'simple.second.value'])).toEqual( + true + ); + }); + + test('it returns false if a simple string value does not match two strings', () => { + expect(isIgnored('simple', ['simple.value', 'simple.second.value'])).toEqual(false); + }); + + test('it returns true if mixed with a regular expression in the list', () => { + expect(isIgnored('simple', ['simple', '/[_]+/'])).toEqual(true); + }); + }); + + describe('regular expression matching', () => { + test('it returns true if a simple regular expression matches', () => { + expect(isIgnored('_ignored', ['/[_]+/'])).toEqual(true); + }); + + test('it returns false if a simple regular expression does not match', () => { + expect(isIgnored('simple', ['/[_]+/'])).toEqual(false); + }); + + test('it returns true if a simple regular expression matches a longer string', () => { + expect(isIgnored('___ignored', ['/[_]+/'])).toEqual(true); + }); + + test('it returns true if mixed with regular stings', () => { + expect(isIgnored('___ignored', ['simple', '/[_]+/'])).toEqual(true); + }); + + test('it returns true with start anchor', () => { + expect(isIgnored('_ignored', ['simple', '/^[_]+/'])).toEqual(true); + }); + + test('it returns false with start anchor', () => { + expect(isIgnored('simple.something_', ['simple', '/^[_]+/'])).toEqual(false); + }); + + test('it returns true with end anchor', () => { + expect(isIgnored('something_', ['simple', '/[_]+$/'])).toEqual(true); + }); + + test('it returns false with end anchor', () => { + expect(isIgnored('_something', ['simple', '/[_]+$/'])).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_ignored.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_ignored.ts new file mode 100644 index 000000000000..a418ce735626 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/source_fields_merging/utils/is_ignored.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Matches against anything you want to ignore and if it matches that field is ignored. + * @param fieldsKey The fields key to match against + * @param ignoreFields Array of fields to ignore. If a value starts and ends with "/", such as: "/[_]+/" then the field will be treated as a regular expression. + * If you have an object structure to ignore such as "{ a: { b: c: {} } } ", then you need to ignore it as the string "a.b.c" + * @returns true if it is a field to ignore, otherwise false + */ +export const isIgnored = (fieldsKey: string, ignoreFields: string[]): boolean => { + return ignoreFields.some((ignoreField) => { + if (ignoreField.startsWith('/') && ignoreField.endsWith('/')) { + return new RegExp(ignoreField.slice(1, -1)).test(fieldsKey); + } else { + return fieldsKey === ignoreField; + } + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts index 19bdd58140a3..27220d80ebd6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts @@ -5,12 +5,7 @@ * 2.0. */ -import { - SearchAfterAndBulkCreateParams, - SignalSourceHit, - WrapHits, - WrappedSignalHit, -} from './types'; +import { SearchAfterAndBulkCreateParams, WrapHits, WrappedSignalHit } from './types'; import { generateId } from './utils'; import { buildBulkBody } from './build_bulk_body'; import { filterDuplicateSignals } from './filter_duplicate_signals'; @@ -20,10 +15,12 @@ export const wrapHitsFactory = ({ ruleSO, signalsIndex, mergeStrategy, + ignoreFields, }: { ruleSO: SearchAfterAndBulkCreateParams['ruleSO']; signalsIndex: string; mergeStrategy: ConfigType['alertMergeStrategy']; + ignoreFields: ConfigType['alertIgnoreFields']; }): WrapHits => (events, buildReasonMessage) => { const wrappedDocs: WrappedSignalHit[] = events.flatMap((doc) => [ { @@ -34,7 +31,7 @@ export const wrapHitsFactory = ({ String(doc._version), ruleSO.attributes.params.ruleId ?? '' ), - _source: buildBulkBody(ruleSO, doc as SignalSourceHit, mergeStrategy, buildReasonMessage), + _source: buildBulkBody(ruleSO, doc, mergeStrategy, ignoreFields, buildReasonMessage), }, ]); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts index 0ca4b9688f97..d4a4c55db1d2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts @@ -13,10 +13,12 @@ export const wrapSequencesFactory = ({ ruleSO, signalsIndex, mergeStrategy, + ignoreFields, }: { ruleSO: SearchAfterAndBulkCreateParams['ruleSO']; signalsIndex: string; mergeStrategy: ConfigType['alertMergeStrategy']; + ignoreFields: ConfigType['alertIgnoreFields']; }): WrapSequences => (sequences, buildReasonMessage) => sequences.reduce( (acc: WrappedSignalHit[], sequence) => [ @@ -26,6 +28,7 @@ export const wrapSequencesFactory = ({ ruleSO, signalsIndex, mergeStrategy, + ignoreFields, buildReasonMessage ), ], diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index d657d7e06b1a..4e4d0be5a741 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -252,6 +252,7 @@ export class Plugin implements IPlugin `--xpack.${key}.enabled=false`), ...(ssl ? [ diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/ignore_fields.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/ignore_fields.ts new file mode 100644 index 000000000000..409128523ea4 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/ignore_fields.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + createRule, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getEqlRuleForSignalTesting, + getSignalsById, + waitForRuleSuccessOrStatus, + waitForSignalsToBePresent, +} from '../../utils'; + +interface Ignore { + normal_constant?: string; + small_field?: string; + testing_ignored?: string; + testing_regex?: string; +} + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + /** + * See the config file (detection_engine_api_integration/common/config.ts) for which field values were added to be ignored + * for testing. The values should be in the config around the area of: + * --xpack.securitySolution.alertIgnoreFields=[testing.ignore_1,/[testingRegex] + * meaning that the ignore fields values should be the array: ["testing.ignore_1", "/[testingRegex]/"] + * + * This test exercises the ability to be able to ignore particular values within the fields API and merge strategies. + * These values can be defined in your kibana.yml file as "xpack.securitySolution.alertIgnoreFields". This is useful + * for users that find bugs or regressions within query languages or bugs within the merge strategies + * where one or more fields are causing problems and they need to turn disable that particular field. + * + * Ref: + * https://github.com/elastic/kibana/issues/110802 + * https://github.com/elastic/elasticsearch/issues/77152 + * + * Files ref: + * server/lib/detection_engine/signals/source_fields_merging/utils/is_ignored.ts + * server/lib/detection_engine/signals/source_fields_merging/utils/is_eql_bug_77152.ts + */ + describe('ignore_fields', () => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/security_solution/ignore_fields'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/security_solution/ignore_fields'); + }); + + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + }); + + it('should ignore the field of "testing_ignored"', async () => { + const rule = getEqlRuleForSignalTesting(['ignore_fields']); + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits + .map((hit) => (hit._source as Ignore).testing_ignored) + .sort(); + + // Value should be "undefined for all records" + expect(hits).to.eql([undefined, undefined, undefined, undefined]); + }); + + it('should ignore the field of "testing_regex"', async () => { + const rule = getEqlRuleForSignalTesting(['ignore_fields']); + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => (hit._source as Ignore).testing_regex).sort(); + + // Value should be "undefined for all records" + expect(hits).to.eql([undefined, undefined, undefined, undefined]); + }); + + it('should have the field of "normal_constant"', async () => { + const rule = getEqlRuleForSignalTesting(['ignore_fields']); + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits + .map((hit) => (hit._source as Ignore).normal_constant) + .sort(); + + // Value should be "constant_value for all records" + expect(hits).to.eql(['constant_value', 'constant_value', 'constant_value', 'constant_value']); + }); + + // TODO: Remove this test once https://github.com/elastic/elasticsearch/issues/77152 is fixed + it('should ignore the field of "_ignored" when using EQL and index the data', async () => { + const rule = getEqlRuleForSignalTesting(['ignore_fields']); + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => (hit._source as Ignore).small_field).sort(); + + // We just test a constant value to ensure this did not blow up on us and did index data. + expect(hits).to.eql([ + '1 indexed', + '2 large not indexed', + '3 large not indexed', + '4 large not indexed', + ]); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts index 27474fe563a3..41a3d084e084 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts @@ -48,6 +48,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./timestamps')); loadTestFile(require.resolve('./runtime')); loadTestFile(require.resolve('./throttle')); + loadTestFile(require.resolve('./ignore_fields')); }); // That split here enable us on using a different ciGroup to run the tests diff --git a/x-pack/test/functional/es_archives/security_solution/ignore_fields/data.json b/x-pack/test/functional/es_archives/security_solution/ignore_fields/data.json new file mode 100644 index 000000000000..7a33785c0bc4 --- /dev/null +++ b/x-pack/test/functional/es_archives/security_solution/ignore_fields/data.json @@ -0,0 +1,51 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "ignore_fields", + "source": { + "@timestamp": "2020-10-28T05:00:53.000Z", + "small_field": "1 indexed" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "ignore_fields", + "source": { + "@timestamp": "2020-10-28T05:01:53.000Z", + "small_field": "2 large not indexed" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "3", + "index": "ignore_fields", + "source": { + "@timestamp": "2020-10-28T05:02:53.000Z", + "small_field": "3 large not indexed" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "4", + "index": "ignore_fields", + "source": { + "@timestamp": "2020-10-28T05:03:53.000Z", + "small_field": "4 large not indexed" + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/security_solution/ignore_fields/mappings.json b/x-pack/test/functional/es_archives/security_solution/ignore_fields/mappings.json new file mode 100644 index 000000000000..e2c8ca3c2bc8 --- /dev/null +++ b/x-pack/test/functional/es_archives/security_solution/ignore_fields/mappings.json @@ -0,0 +1,41 @@ +{ + "type": "index", + "value": { + "index": "ignore_fields", + "mappings": { + "dynamic": "strict", + "properties": { + "@timestamp": { + "type": "date" + }, + "testing_ignored": { + "properties": { + "constant": { + "type": "constant_keyword", + "value": "constant_value" + } + } + }, + "testing_regex": { + "type": "constant_keyword", + "value": "constant_value" + }, + "normal_constant": { + "type": "constant_keyword", + "value": "constant_value" + }, + "small_field": { + "type": "keyword", + "ignore_above": 10 + } + } + }, + "settings": { + "index": { + "refresh_interval": "1s", + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} From d4c03eb9b4776a90d5b2c48aec55ef27084a3e79 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Fri, 3 Sep 2021 16:44:12 +0200 Subject: [PATCH 23/47] [Lens] Switch to SavedObjectClient.resolve (#110059) * Step 2: Update client code to use resolve() method instead of get() Following sharing Saved Objects developer guide: Step 2 This step demonstrates the changes to update client code to use the new SavedObjectsClient `resolve()` method instead of `get()`. * Step 3 Lens --- x-pack/plugins/lens/kibana.json | 3 +- .../lens/public/app_plugin/app.test.tsx | 31 ++++ x-pack/plugins/lens/public/app_plugin/app.tsx | 28 ++++ .../lens/public/app_plugin/mounter.tsx | 6 +- .../plugins/lens/public/app_plugin/types.ts | 3 + .../workspace_panel/workspace_panel.tsx | 10 +- .../lens/public/editor_frame_service/types.ts | 2 +- .../public/embeddable/embeddable.test.tsx | 56 ++++++- .../lens/public/embeddable/embeddable.tsx | 27 +++- .../public/embeddable/expression_wrapper.tsx | 64 +++++++- .../lens/public/lens_attribute_service.ts | 51 +++++-- x-pack/plugins/lens/public/mocks.tsx | 13 +- .../persistence/saved_object_store.test.ts | 21 +-- .../public/persistence/saved_object_store.ts | 22 +-- x-pack/plugins/lens/public/plugin.ts | 2 + .../state_management/init_middleware/index.ts | 8 +- .../init_middleware/load_initial.test.tsx | 143 ++++++++++++++---- .../init_middleware/load_initial.ts | 63 ++++++-- .../public/state_management/lens_slice.ts | 2 + .../lens/public/state_management/types.ts | 4 +- x-pack/plugins/lens/public/types.ts | 10 +- .../public/xy_visualization/visualization.tsx | 4 +- x-pack/plugins/lens/tsconfig.json | 1 + 23 files changed, 466 insertions(+), 108 deletions(-) diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index 2ec7a1962da8..f82f3366448d 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -24,7 +24,8 @@ "usageCollection", "taskManager", "globalSearch", - "savedObjectsTagging" + "savedObjectsTagging", + "spaces" ], "configPath": [ "xpack", diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 8cb4a7c4c843..5617b5b0edee 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -383,6 +383,9 @@ describe('Lens App', () => { savedObjectId: savedObjectId || 'aaa', })); services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({ + sharingSavedObjectProps: { + outcome: 'exactMatch', + }, savedObjectId: initialSavedObjectId ?? 'aaa', references: [], state: { @@ -1256,4 +1259,32 @@ describe('Lens App', () => { expect(defaultLeave).not.toHaveBeenCalled(); }); }); + it('should display a conflict callout if saved object conflicts', async () => { + const history = createMemoryHistory(); + const { services } = await mountWith({ + props: { + ...makeDefaultProps(), + history: { + ...history, + location: { + ...history.location, + search: '?_g=test', + }, + }, + }, + preloadedState: { + persistedDoc: defaultDoc, + sharingSavedObjectProps: { + outcome: 'conflict', + aliasTargetId: '2', + }, + }, + }); + expect(services.spaces.ui.components.getLegacyUrlConflict).toHaveBeenCalledWith({ + currentObjectId: '1234', + objectNoun: 'Lens visualization', + otherObjectId: '2', + otherObjectPath: '#/edit/2?_g=test', + }); + }); }); diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 63cb7d300254..ae2edaa1b98d 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -38,6 +38,7 @@ import { runSaveLensVisualization, } from './save_modal_container'; import { getLensInspectorService, LensInspector } from '../lens_inspector_service'; +import { getEditPath } from '../../common'; export type SaveProps = Omit & { returnToOrigin: boolean; @@ -70,6 +71,8 @@ export function App({ notifications, savedObjectsTagging, getOriginatingAppName, + spaces, + http, // Temporarily required until the 'by value' paradigm is default. dashboardFeatureFlag, } = lensAppServices; @@ -82,6 +85,7 @@ export function App({ const { persistedDoc, + sharingSavedObjectProps, isLinkedToOriginatingApp, searchSessionId, isLoading, @@ -166,6 +170,28 @@ export function App({ }); }, [onAppLeave, lastKnownDoc, isSaveable, persistedDoc, application.capabilities.visualize.save]); + const getLegacyUrlConflictCallout = useCallback(() => { + // This function returns a callout component *if* we have encountered a "legacy URL conflict" scenario + if (spaces && sharingSavedObjectProps?.outcome === 'conflict' && persistedDoc?.savedObjectId) { + // We have resolved to one object, but another object has a legacy URL alias associated with this ID/page. We should display a + // callout with a warning for the user, and provide a way for them to navigate to the other object. + const currentObjectId = persistedDoc.savedObjectId; + const otherObjectId = sharingSavedObjectProps?.aliasTargetId!; // This is always defined if outcome === 'conflict' + const otherObjectPath = http.basePath.prepend( + `${getEditPath(otherObjectId)}${history.location.search}` + ); + return spaces.ui.components.getLegacyUrlConflict({ + objectNoun: i18n.translate('xpack.lens.appName', { + defaultMessage: 'Lens visualization', + }), + currentObjectId, + otherObjectId, + otherObjectPath, + }); + } + return null; + }, [persistedDoc, sharingSavedObjectProps, spaces, http, history]); + // Sync Kibana breadcrumbs any time the saved document's title changes useEffect(() => { const isByValueMode = getIsByValueMode(); @@ -273,6 +299,8 @@ export function App({ title={persistedDoc?.title} lensInspector={lensInspector} /> + + {getLegacyUrlConflictCallout()} {(!isLoading || persistedDoc) && ( diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index 4ccf441799b1..8a3a848ffa20 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -7,6 +7,7 @@ import type { History } from 'history'; import type { OnSaveProps } from 'src/plugins/saved_objects/public'; +import { SpacesApi } from '../../../spaces/public'; import type { ApplicationStart, AppMountParameters, @@ -116,6 +117,8 @@ export interface LensAppServices { savedObjectsTagging?: SavedObjectTaggingPluginStart; getOriginatingAppName: () => string | undefined; presentationUtil: PresentationUtilPluginStart; + spaces: SpacesApi; + // Temporarily required until the 'by value' paradigm is default. dashboardFeatureFlag: DashboardFeatureFlagConfig; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 31705d6b9293..c34e3c413736 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -79,7 +79,7 @@ export interface WorkspacePanelProps { interface WorkspaceState { expressionBuildError?: Array<{ shortMessage: string; - longMessage: string; + longMessage: React.ReactNode; fixAction?: DatasourceFixAction; }>; expandError: boolean; @@ -416,10 +416,10 @@ export const VisualizationWrapper = ({ localState: WorkspaceState & { configurationValidationError?: Array<{ shortMessage: string; - longMessage: string; + longMessage: React.ReactNode; fixAction?: DatasourceFixAction; }>; - missingRefsErrors?: Array<{ shortMessage: string; longMessage: string }>; + missingRefsErrors?: Array<{ shortMessage: string; longMessage: React.ReactNode }>; }; ExpressionRendererComponent: ReactExpressionRendererType; application: ApplicationStart; @@ -454,7 +454,7 @@ export const VisualizationWrapper = ({ validationError: | { shortMessage: string; - longMessage: string; + longMessage: React.ReactNode; fixAction?: DatasourceFixAction; } | undefined @@ -499,7 +499,7 @@ export const VisualizationWrapper = ({ .map((validationError) => ( <>

diff --git a/x-pack/plugins/lens/public/editor_frame_service/types.ts b/x-pack/plugins/lens/public/editor_frame_service/types.ts index ebfd098b5fb1..9435faf37442 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/types.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/types.ts @@ -11,6 +11,6 @@ export type TableInspectorAdapter = Record; export interface ErrorMessage { shortMessage: string; - longMessage: string; + longMessage: React.ReactNode; type?: 'fixable' | 'critical'; } diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx index 74aac932a686..a0831e8a73b5 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx @@ -11,6 +11,7 @@ import { LensByReferenceInput, LensSavedObjectAttributes, LensEmbeddableInput, + ResolvedLensSavedObjectAttributes, } from './embeddable'; import { ReactExpressionRendererProps } from 'src/plugins/expressions/public'; import { Query, TimeRange, Filter, IndexPatternsContract } from 'src/plugins/data/public'; @@ -68,12 +69,17 @@ const options = { const attributeServiceMockFromSavedVis = (document: Document): LensAttributeService => { const core = coreMock.createStart(); const service = new AttributeService< - LensSavedObjectAttributes, + ResolvedLensSavedObjectAttributes, LensByValueInput, LensByReferenceInput >('lens', jest.fn(), core.i18n.Context, core.notifications.toasts, options); service.unwrapAttributes = jest.fn((input: LensByValueInput | LensByReferenceInput) => { - return Promise.resolve({ ...document } as LensSavedObjectAttributes); + return Promise.resolve({ + ...document, + sharingSavedObjectProps: { + outcome: 'exactMatch', + }, + } as ResolvedLensSavedObjectAttributes); }); service.wrapAttributes = jest.fn(); return service; @@ -86,7 +92,7 @@ describe('embeddable', () => { let trigger: { exec: jest.Mock }; let basePath: IBasePath; let attributeService: AttributeService< - LensSavedObjectAttributes, + ResolvedLensSavedObjectAttributes, LensByValueInput, LensByReferenceInput >; @@ -223,6 +229,50 @@ describe('embeddable', () => { expect(expressionRenderer).toHaveBeenCalledTimes(0); }); + it('should not render the vis if loaded saved object conflicts', async () => { + attributeService.unwrapAttributes = jest.fn( + (input: LensByValueInput | LensByReferenceInput) => { + return Promise.resolve({ + ...savedVis, + sharingSavedObjectProps: { + outcome: 'conflict', + errorJSON: '{targetType: "lens", sourceId: "1", targetSpace: "space"}', + aliasTargetId: '2', + }, + } as ResolvedLensSavedObjectAttributes); + } + ); + const embeddable = new Embeddable( + { + timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, + attributeService, + inspector: inspectorPluginMock.createStartContract(), + expressionRenderer, + basePath, + indexPatternService: {} as IndexPatternsContract, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, + getTrigger, + documentToExpression: () => + Promise.resolve({ + ast: { + type: 'expression', + chain: [ + { type: 'function', function: 'my', arguments: {} }, + { type: 'function', function: 'expression', arguments: {} }, + ], + }, + errors: undefined, + }), + }, + {} as LensEmbeddableInput + ); + await embeddable.initializeSavedVis({} as LensEmbeddableInput); + expect(expressionRenderer).toHaveBeenCalledTimes(0); + }); + it('should initialize output with deduped list of index patterns', async () => { attributeService = attributeServiceMockFromSavedVis({ ...savedVis, diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 172274b1f90b..7e87dd3076fa 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -41,7 +41,11 @@ import { ReferenceOrValueEmbeddable, } from '../../../../../src/plugins/embeddable/public'; import { Document, injectFilterReferences } from '../persistence'; -import { ExpressionWrapper, ExpressionWrapperProps } from './expression_wrapper'; +import { + ExpressionWrapper, + ExpressionWrapperProps, + savedObjectConflictError, +} from './expression_wrapper'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import { isLensBrushEvent, @@ -58,8 +62,12 @@ import { IBasePath } from '../../../../../src/core/public'; import { LensAttributeService } from '../lens_attribute_service'; import type { ErrorMessage } from '../editor_frame_service/types'; import { getLensInspectorService, LensInspector } from '../lens_inspector_service'; +import { SharingSavedObjectProps } from '../types'; export type LensSavedObjectAttributes = Omit; +export interface ResolvedLensSavedObjectAttributes extends LensSavedObjectAttributes { + sharingSavedObjectProps?: SharingSavedObjectProps; +} interface LensBaseEmbeddableInput extends EmbeddableInput { filters?: Filter[]; @@ -76,7 +84,7 @@ interface LensBaseEmbeddableInput extends EmbeddableInput { } export type LensByValueInput = { - attributes: LensSavedObjectAttributes; + attributes: ResolvedLensSavedObjectAttributes; } & LensBaseEmbeddableInput; export type LensByReferenceInput = SavedObjectEmbeddableInput & LensBaseEmbeddableInput; @@ -253,15 +261,18 @@ export class Embeddable } async initializeSavedVis(input: LensEmbeddableInput) { - const attributes: - | LensSavedObjectAttributes + const attrs: + | ResolvedLensSavedObjectAttributes | false = await this.deps.attributeService.unwrapAttributes(input).catch((e: Error) => { this.onFatalError(e); return false; }); - if (!attributes || this.isDestroyed) { + if (!attrs || this.isDestroyed) { return; } + + const { sharingSavedObjectProps, ...attributes } = attrs; + this.savedVis = { ...attributes, type: this.type, @@ -269,8 +280,12 @@ export class Embeddable }; const { ast, errors } = await this.deps.documentToExpression(this.savedVis); this.errors = errors; + if (sharingSavedObjectProps?.outcome === 'conflict') { + const conflictError = savedObjectConflictError(sharingSavedObjectProps.errorJSON!); + this.errors = this.errors ? [...this.errors, conflictError] : [conflictError]; + } this.expression = ast ? toExpression(ast) : null; - if (errors) { + if (this.errors) { this.logError('validation'); } await this.initializeOutput(); diff --git a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx index d57e1c450fea..1116b4a0d396 100644 --- a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx @@ -5,10 +5,20 @@ * 2.0. */ -import React from 'react'; +import React, { useState } from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon, EuiEmptyPrompt } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiIcon, + EuiEmptyPrompt, + EuiButtonEmpty, + EuiCallOut, + EuiSpacer, + EuiLink, +} from '@elastic/eui'; import { ExpressionRendererEvent, ReactExpressionRendererType, @@ -18,6 +28,7 @@ import type { KibanaExecutionContext } from 'src/core/public'; import { ExecutionContextSearch } from 'src/plugins/data/public'; import { DefaultInspectorAdapters, RenderMode } from 'src/plugins/expressions'; import classNames from 'classnames'; +import { i18n } from '@kbn/i18n'; import { getOriginalRequestErrorMessages } from '../editor_frame_service/error_helper'; import { ErrorMessage } from '../editor_frame_service/types'; import { LensInspector } from '../lens_inspector_service'; @@ -158,3 +169,52 @@ export function ExpressionWrapper({ ); } + +const SavedObjectConflictMessage = ({ json }: { json: string }) => { + const [expandError, setExpandError] = useState(false); + return ( + <> + + {i18n.translate('xpack.lens.embeddable.legacyURLConflict.documentationLinkText', { + defaultMessage: 'legacy URL alias', + })} + + ), + }} + /> + + {expandError ? ( + + ) : ( + setExpandError(true)}> + {i18n.translate('xpack.lens.embeddable.legacyURLConflict.expandError', { + defaultMessage: `Show more`, + })} + + )} + + ); +}; + +export const savedObjectConflictError = (json: string): ErrorMessage => ({ + shortMessage: i18n.translate('xpack.lens.embeddable.legacyURLConflict.shortMessage', { + defaultMessage: `You've encountered a URL conflict`, + }), + longMessage: , +}); diff --git a/x-pack/plugins/lens/public/lens_attribute_service.ts b/x-pack/plugins/lens/public/lens_attribute_service.ts index 39a1903c6d0c..09c98b3dcba7 100644 --- a/x-pack/plugins/lens/public/lens_attribute_service.ts +++ b/x-pack/plugins/lens/public/lens_attribute_service.ts @@ -9,47 +9,68 @@ import { CoreStart } from '../../../../src/core/public'; import { LensPluginStartDependencies } from './plugin'; import { AttributeService } from '../../../../src/plugins/embeddable/public'; import { - LensSavedObjectAttributes, + ResolvedLensSavedObjectAttributes, LensByValueInput, LensByReferenceInput, } from './embeddable/embeddable'; -import { SavedObjectIndexStore, Document } from './persistence'; +import { SavedObjectIndexStore } from './persistence'; import { checkForDuplicateTitle, OnSaveProps } from '../../../../src/plugins/saved_objects/public'; import { DOC_TYPE } from '../common'; export type LensAttributeService = AttributeService< - LensSavedObjectAttributes, + ResolvedLensSavedObjectAttributes, LensByValueInput, LensByReferenceInput >; -function documentToAttributes(doc: Document): LensSavedObjectAttributes { - delete doc.savedObjectId; - delete doc.type; - return { ...doc }; -} - export function getLensAttributeService( core: CoreStart, startDependencies: LensPluginStartDependencies ): LensAttributeService { const savedObjectStore = new SavedObjectIndexStore(core.savedObjects.client); return startDependencies.embeddable.getAttributeService< - LensSavedObjectAttributes, + ResolvedLensSavedObjectAttributes, LensByValueInput, LensByReferenceInput >(DOC_TYPE, { - saveMethod: async (attributes: LensSavedObjectAttributes, savedObjectId?: string) => { + saveMethod: async (attributes: ResolvedLensSavedObjectAttributes, savedObjectId?: string) => { + const { sharingSavedObjectProps, ...attributesToSave } = attributes; const savedDoc = await savedObjectStore.save({ - ...attributes, + ...attributesToSave, savedObjectId, type: DOC_TYPE, }); return { id: savedDoc.savedObjectId }; }, - unwrapMethod: async (savedObjectId: string): Promise => { - const attributes = documentToAttributes(await savedObjectStore.load(savedObjectId)); - return attributes; + unwrapMethod: async (savedObjectId: string): Promise => { + const { + saved_object: savedObject, + outcome, + alias_target_id: aliasTargetId, + } = await savedObjectStore.load(savedObjectId); + const { attributes, references, type, id } = savedObject; + const document = { + ...attributes, + references, + }; + + const sharingSavedObjectProps = { + aliasTargetId, + outcome, + errorJSON: + outcome === 'conflict' + ? JSON.stringify({ + targetType: type, + sourceId: id, + targetSpace: (await startDependencies.spaces.getActiveSpace()).id, + }) + : undefined, + }; + + return { + sharingSavedObjectProps, + ...document, + }; }, checkForDuplicateTitle: (props: OnSaveProps) => { const savedObjectsClient = core.savedObjects.client; diff --git a/x-pack/plugins/lens/public/mocks.tsx b/x-pack/plugins/lens/public/mocks.tsx index b2c8d3948b28..8fbd263fe909 100644 --- a/x-pack/plugins/lens/public/mocks.tsx +++ b/x-pack/plugins/lens/public/mocks.tsx @@ -24,11 +24,12 @@ import { LensAppServices } from './app_plugin/types'; import { DOC_TYPE, layerTypes } from '../common'; import { DataPublicPluginStart, esFilters, UI_SETTINGS } from '../../../../src/plugins/data/public'; import { inspectorPluginMock } from '../../../../src/plugins/inspector/public/mocks'; +import { spacesPluginMock } from '../../spaces/public/mocks'; import { dashboardPluginMock } from '../../../../src/plugins/dashboard/public/mocks'; import type { LensByValueInput, - LensSavedObjectAttributes, LensByReferenceInput, + ResolvedLensSavedObjectAttributes, } from './embeddable/embeddable'; import { mockAttributeService, @@ -352,7 +353,7 @@ export function makeDefaultServices( function makeAttributeService(): LensAttributeService { const attributeServiceMock = mockAttributeService< - LensSavedObjectAttributes, + ResolvedLensSavedObjectAttributes, LensByValueInput, LensByReferenceInput >( @@ -365,7 +366,12 @@ export function makeDefaultServices( core ); - attributeServiceMock.unwrapAttributes = jest.fn().mockResolvedValue(doc); + attributeServiceMock.unwrapAttributes = jest.fn().mockResolvedValue({ + ...doc, + sharingSavedObjectProps: { + outcome: 'exactMatch', + }, + }); attributeServiceMock.wrapAttributes = jest.fn().mockResolvedValue({ savedObjectId: ((doc as unknown) as LensByReferenceInput).savedObjectId, }); @@ -404,6 +410,7 @@ export function makeDefaultServices( remove: jest.fn(), clear: jest.fn(), }, + spaces: spacesPluginMock.createStartContract(), }; } diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts index ab0708d99f08..5a42ea054b4d 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts @@ -15,7 +15,7 @@ describe('LensStore', () => { bulkUpdate: jest.fn(([{ id }]: SavedObjectsBulkUpdateObject[]) => Promise.resolve({ savedObjects: [{ id }, { id }] }) ), - get: jest.fn(), + resolve: jest.fn(), }; return { @@ -142,15 +142,18 @@ describe('LensStore', () => { describe('load', () => { test('throws if an error is returned', async () => { const { client, store } = testStore(); - client.get = jest.fn(async () => ({ - id: 'Paul', - type: 'lens', - attributes: { - title: 'Hope clouds observation.', - visualizationType: 'dune', - state: '{ "datasource": { "giantWorms": true } }', + client.resolve = jest.fn(async () => ({ + outcome: 'exactMatch', + saved_object: { + id: 'Paul', + type: 'lens', + attributes: { + title: 'Hope clouds observation.', + visualizationType: 'dune', + state: '{ "datasource": { "giantWorms": true } }', + }, + error: new Error('shoot dang!'), }, - error: new Error('shoot dang!'), })); await expect(store.load('Paul')).rejects.toThrow('shoot dang!'); diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.ts index c87548daf53d..79d7b78f768a 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.ts @@ -9,9 +9,11 @@ import { SavedObjectAttributes, SavedObjectsClientContract, SavedObjectReference, + ResolvedSimpleSavedObject, } from 'kibana/public'; import { Query } from '../../../../../src/plugins/data/public'; import { DOC_TYPE, PersistableFilter } from '../../common'; +import { LensSavedObjectAttributes } from '../async_services'; export interface Document { savedObjectId?: string; @@ -37,7 +39,7 @@ export interface DocumentSaver { } export interface DocumentLoader { - load: (savedObjectId: string) => Promise; + load: (savedObjectId: string) => Promise; } export type SavedObjectStore = DocumentLoader & DocumentSaver; @@ -87,18 +89,16 @@ export class SavedObjectIndexStore implements SavedObjectStore { ).savedObjects[1]; } - async load(savedObjectId: string): Promise { - const { type, attributes, references, error } = await this.client.get(DOC_TYPE, savedObjectId); + async load(savedObjectId: string): Promise> { + const resolveResult = await this.client.resolve( + DOC_TYPE, + savedObjectId + ); - if (error) { - throw error; + if (resolveResult.saved_object.error) { + throw resolveResult.saved_object.error; } - return { - ...(attributes as SavedObjectAttributes), - references, - savedObjectId, - type, - } as Document; + return resolveResult; } } diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 95f2e13cbc46..26278f446c55 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -9,6 +9,7 @@ import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public'; import type { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import type { FieldFormatsSetup, FieldFormatsStart } from 'src/plugins/field_formats/public'; import { UsageCollectionSetup, UsageCollectionStart } from 'src/plugins/usage_collection/public'; +import { SpacesPluginStart } from '../../spaces/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { DashboardStart } from '../../../../src/plugins/dashboard/public'; @@ -100,6 +101,7 @@ export interface LensPluginStartDependencies { presentationUtil: PresentationUtilPluginStart; indexPatternFieldEditor: IndexPatternFieldEditorStart; inspector: InspectorStartContract; + spaces: SpacesPluginStart; usageCollection?: UsageCollectionStart; } diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/index.ts b/x-pack/plugins/lens/public/state_management/init_middleware/index.ts index bf13ca69e82c..256684c5dbc2 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/index.ts +++ b/x-pack/plugins/lens/public/state_management/init_middleware/index.ts @@ -19,13 +19,7 @@ export const initMiddleware = (storeDeps: LensStoreDeps) => (store: MiddlewareAP ); return (next: Dispatch) => (action: PayloadAction) => { if (lensSlice.actions.loadInitial.match(action)) { - return loadInitial( - store, - storeDeps, - action.payload.redirectCallback, - action.payload.initialInput, - action.payload.emptyState - ); + return loadInitial(store, storeDeps, action.payload); } else if (lensSlice.actions.navigateAway.match(action)) { return unsubscribeFromExternalContext(); } diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx index 79402b698af9..6d3b77c6476e 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx +++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx @@ -12,6 +12,7 @@ import { createMockDatasource, DatasourceMock, } from '../../mocks'; +import { Location, History } from 'history'; import { act } from 'react-dom/test-utils'; import { loadInitial } from './load_initial'; import { LensEmbeddableInput } from '../../embeddable'; @@ -65,7 +66,12 @@ describe('Mounter', () => { it('should initialize initial datasource', async () => { const services = makeDefaultServices(); - services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(defaultDoc); + services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({ + ...defaultDoc, + sharingSavedObjectProps: { + outcome: 'exactMatch', + }, + }); const lensStore = await makeLensStore({ data: services.data, @@ -79,8 +85,10 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - jest.fn(), - ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput + { + redirectCallback: jest.fn(), + initialInput: ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput, + } ); }); expect(mockDatasource.initialize).toHaveBeenCalled(); @@ -88,7 +96,12 @@ describe('Mounter', () => { it('should have initialized only the initial datasource and visualization', async () => { const services = makeDefaultServices(); - services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(defaultDoc); + services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({ + ...defaultDoc, + sharingSavedObjectProps: { + outcome: 'exactMatch', + }, + }); const lensStore = await makeLensStore({ data: services.data, preloadedState }); await act(async () => { @@ -99,7 +112,7 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - jest.fn() + { redirectCallback: jest.fn() } ); }); expect(mockDatasource.initialize).toHaveBeenCalled(); @@ -129,7 +142,7 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - jest.fn() + { redirectCallback: jest.fn() } ); expect(services.attributeService.unwrapAttributes).not.toHaveBeenCalled(); }); @@ -170,7 +183,11 @@ describe('Mounter', () => { const emptyState = getPreloadedState(storeDeps) as LensAppState; services.attributeService.unwrapAttributes = jest.fn(); await act(async () => { - await loadInitial(lensStore, storeDeps, jest.fn(), undefined, emptyState); + await loadInitial(lensStore, storeDeps, { + redirectCallback: jest.fn(), + initialInput: undefined, + emptyState, + }); }); expect(lensStore.getState()).toEqual({ @@ -189,20 +206,28 @@ describe('Mounter', () => { it('loads a document and uses query and filters if initial input is provided', async () => { const services = makeDefaultServices(); - services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(defaultDoc); + services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({ + ...defaultDoc, + sharingSavedObjectProps: { + outcome: 'exactMatch', + }, + }); + const storeDeps = { + lensServices: services, + datasourceMap, + visualizationMap, + }; + const emptyState = getPreloadedState(storeDeps) as LensAppState; const lensStore = await makeLensStore({ data: services.data, preloadedState }); await act(async () => { - await loadInitial( - lensStore, - { - lensServices: services, - datasourceMap, - visualizationMap, - }, - jest.fn(), - ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput - ); + await loadInitial(lensStore, storeDeps, { + redirectCallback: jest.fn(), + initialInput: ({ + savedObjectId: defaultSavedObjectId, + } as unknown) as LensEmbeddableInput, + emptyState, + }); }); expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({ @@ -235,8 +260,12 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - jest.fn(), - ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput + { + redirectCallback: jest.fn(), + initialInput: ({ + savedObjectId: defaultSavedObjectId, + } as unknown) as LensEmbeddableInput, + } ); }); @@ -248,8 +277,12 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - jest.fn(), - ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput + { + redirectCallback: jest.fn(), + initialInput: ({ + savedObjectId: defaultSavedObjectId, + } as unknown) as LensEmbeddableInput, + } ); }); @@ -263,8 +296,10 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - jest.fn(), - ({ savedObjectId: '5678' } as unknown) as LensEmbeddableInput + { + redirectCallback: jest.fn(), + initialInput: ({ savedObjectId: '5678' } as unknown) as LensEmbeddableInput, + } ); }); @@ -287,8 +322,12 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - redirectCallback, - ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput + { + redirectCallback, + initialInput: ({ + savedObjectId: defaultSavedObjectId, + } as unknown) as LensEmbeddableInput, + } ); }); expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({ @@ -298,6 +337,50 @@ describe('Mounter', () => { expect(redirectCallback).toHaveBeenCalled(); }); + it('redirects if saved object is an aliasMatch', async () => { + const services = makeDefaultServices(); + + const lensStore = makeLensStore({ data: services.data, preloadedState }); + + services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({ + ...defaultDoc, + sharingSavedObjectProps: { + outcome: 'aliasMatch', + aliasTargetId: 'id2', + }, + }); + + await act(async () => { + await loadInitial( + lensStore, + { + lensServices: services, + datasourceMap, + visualizationMap, + }, + { + redirectCallback: jest.fn(), + initialInput: ({ + savedObjectId: defaultSavedObjectId, + } as unknown) as LensEmbeddableInput, + history: { + location: { + search: '?search', + } as Location, + } as History, + } + ); + }); + expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({ + savedObjectId: defaultSavedObjectId, + }); + + expect(services.spaces.ui.redirectLegacyUrl).toHaveBeenCalledWith( + '#/edit/id2?search', + 'Lens visualization' + ); + }); + it('adds to the recently accessed list on load', async () => { const services = makeDefaultServices(); const lensStore = makeLensStore({ data: services.data, preloadedState }); @@ -309,8 +392,12 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - jest.fn(), - ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput + { + redirectCallback: jest.fn(), + initialInput: ({ + savedObjectId: defaultSavedObjectId, + } as unknown) as LensEmbeddableInput, + } ); }); diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts index 0be2bc9cfc00..8ae6e58019c9 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts +++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts @@ -8,8 +8,10 @@ import { MiddlewareAPI } from '@reduxjs/toolkit'; import { isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { History } from 'history'; import { LensAppState, setState } from '..'; import { updateLayer, updateVisualizationState, LensStoreDeps } from '..'; +import { SharingSavedObjectProps } from '../../types'; import { LensEmbeddableInput, LensByReferenceInput } from '../../embeddable/embeddable'; import { getInitialDatasourceId } from '../../utils'; import { initializeDatasources } from '../../editor_frame_service/editor_frame'; @@ -19,22 +21,50 @@ import { switchToSuggestion, } from '../../editor_frame_service/editor_frame/suggestion_helpers'; import { LensAppServices } from '../../app_plugin/types'; -import { getFullPath, LENS_EMBEDDABLE_TYPE } from '../../../common/constants'; +import { getEditPath, getFullPath, LENS_EMBEDDABLE_TYPE } from '../../../common/constants'; import { Document, injectFilterReferences } from '../../persistence'; export const getPersisted = async ({ initialInput, lensServices, + history, }: { initialInput: LensEmbeddableInput; lensServices: LensAppServices; -}): Promise<{ doc: Document } | undefined> => { - const { notifications, attributeService } = lensServices; + history?: History; +}): Promise< + { doc: Document; sharingSavedObjectProps: Omit } | undefined +> => { + const { notifications, spaces, attributeService } = lensServices; let doc: Document; try { - const attributes = await attributeService.unwrapAttributes(initialInput); - + const result = await attributeService.unwrapAttributes(initialInput); + if (!result) { + return { + doc: ({ + ...initialInput, + type: LENS_EMBEDDABLE_TYPE, + } as unknown) as Document, + sharingSavedObjectProps: { + outcome: 'exactMatch', + }, + }; + } + const { sharingSavedObjectProps, ...attributes } = result; + if (spaces && sharingSavedObjectProps?.outcome === 'aliasMatch' && history) { + // We found this object by a legacy URL alias from its old ID; redirect the user to the page with its new ID, preserving any URL hash + const newObjectId = sharingSavedObjectProps?.aliasTargetId; // This is always defined if outcome === 'aliasMatch' + const newPath = lensServices.http.basePath.prepend( + `${getEditPath(newObjectId)}${history.location.search}` + ); + await spaces.ui.redirectLegacyUrl( + newPath, + i18n.translate('xpack.lens.legacyUrlConflict.objectNoun', { + defaultMessage: 'Lens visualization', + }) + ); + } doc = { ...initialInput, ...attributes, @@ -43,6 +73,10 @@ export const getPersisted = async ({ return { doc, + sharingSavedObjectProps: { + aliasTargetId: sharingSavedObjectProps?.aliasTargetId, + outcome: sharingSavedObjectProps?.outcome, + }, }; } catch (e) { notifications.toasts.addDanger( @@ -62,9 +96,17 @@ export function loadInitial( embeddableEditorIncomingState, initialContext, }: LensStoreDeps, - redirectCallback: (savedObjectId?: string) => void, - initialInput?: LensEmbeddableInput, - emptyState?: LensAppState + { + redirectCallback, + initialInput, + emptyState, + history, + }: { + redirectCallback: (savedObjectId?: string) => void; + initialInput?: LensEmbeddableInput; + emptyState?: LensAppState; + history?: History; + } ) { const { getState, dispatch } = store; const { attributeService, notifications, data, dashboardFeatureFlag } = lensServices; @@ -146,11 +188,11 @@ export function loadInitial( redirectCallback(); }); } - getPersisted({ initialInput, lensServices }) + getPersisted({ initialInput, lensServices, history }) .then( (persisted) => { if (persisted) { - const { doc } = persisted; + const { doc, sharingSavedObjectProps } = persisted; if (attributeService.inputIsRefType(initialInput)) { lensServices.chrome.recentlyAccessed.add( getFullPath(initialInput.savedObjectId), @@ -190,6 +232,7 @@ export function loadInitial( dispatch( setState({ + sharingSavedObjectProps, query: doc.state.query, searchSessionId: dashboardFeatureFlag.allowByValueEmbeddables && diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index 85cb79f6ea5d..6cf0529b3457 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -6,6 +6,7 @@ */ import { createSlice, current, PayloadAction } from '@reduxjs/toolkit'; +import { History } from 'history'; import { LensEmbeddableInput } from '..'; import { TableInspectorAdapter } from '../editor_frame_service/types'; import { getInitialDatasourceId, getResolvedDateRange } from '../utils'; @@ -301,6 +302,7 @@ export const lensSlice = createSlice({ initialInput?: LensEmbeddableInput; redirectCallback: (savedObjectId?: string) => void; emptyState: LensAppState; + history: History; }> ) => state, }, diff --git a/x-pack/plugins/lens/public/state_management/types.ts b/x-pack/plugins/lens/public/state_management/types.ts index 7321f72386b4..33f311a982f0 100644 --- a/x-pack/plugins/lens/public/state_management/types.ts +++ b/x-pack/plugins/lens/public/state_management/types.ts @@ -13,8 +13,7 @@ import { Document } from '../persistence'; import { TableInspectorAdapter } from '../editor_frame_service/types'; import { DateRange } from '../../common'; import { LensAppServices } from '../app_plugin/types'; -import { DatasourceMap, VisualizationMap } from '../types'; - +import { DatasourceMap, VisualizationMap, SharingSavedObjectProps } from '../types'; export interface VisualizationState { activeId: string | null; state: unknown; @@ -44,6 +43,7 @@ export interface LensAppState extends EditorFrameState { savedQuery?: SavedQuery; searchSessionId: string; resolvedDateRange: DateRange; + sharingSavedObjectProps?: Omit; } export type DispatchSetState = ( diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 399e226a711d..844541cd2ad3 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -256,7 +256,7 @@ export interface Datasource { ) => | Array<{ shortMessage: string; - longMessage: string; + longMessage: React.ReactNode; fixAction?: { label: string; newState: () => Promise }; }> | undefined; @@ -729,7 +729,7 @@ export interface Visualization { ) => | Array<{ shortMessage: string; - longMessage: string; + longMessage: React.ReactNode; }> | undefined; @@ -813,3 +813,9 @@ export interface ILensInterpreterRenderHandlers extends IInterpreterRenderHandle | LensTableRowContextMenuEvent ) => void; } + +export interface SharingSavedObjectProps { + outcome?: 'aliasMatch' | 'exactMatch' | 'conflict'; + aliasTargetId?: string; + errorJSON?: string; +} diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 0a4b18f554f3..026c2827cedb 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -383,7 +383,7 @@ export const getXyVisualization = ({ const errors: Array<{ shortMessage: string; - longMessage: string; + longMessage: React.ReactNode; }> = []; // check if the layers in the state are compatible with this type of chart @@ -488,7 +488,7 @@ function validateLayersForDimension( | { valid: true } | { valid: false; - payload: { shortMessage: string; longMessage: string }; + payload: { shortMessage: string; longMessage: React.ReactNode }; } { // Multiple layers must be consistent: // * either a dimension is missing in ALL of them diff --git a/x-pack/plugins/lens/tsconfig.json b/x-pack/plugins/lens/tsconfig.json index 04d3838df206..16287ae596df 100644 --- a/x-pack/plugins/lens/tsconfig.json +++ b/x-pack/plugins/lens/tsconfig.json @@ -15,6 +15,7 @@ "../../../typings/**/*" ], "references": [ + { "path": "../spaces/tsconfig.json" }, { "path": "../../../src/core/tsconfig.json" }, { "path": "../task_manager/tsconfig.json" }, { "path": "../global_search/tsconfig.json"}, From 1f06cafa19e7a1479adf37eba2dc634360d75ebd Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Fri, 3 Sep 2021 17:08:56 +0200 Subject: [PATCH 24/47] [Reporting/Visualization] Migrate Visualize to V2 reporting (#110206) * added initial version of locator * removed unused params and added jest test * updated functional test to expect PDF reports to be available when vis is new * fix TS: remove unkown field * added some docs and removed unused code * AggsConfigOption -> AggsConfigSerialized * moved locator to common * fixed building of "create" path and updated test snapshots * updated import * update encoding behaviour * added time range from timefilter to locator params request * add index pattern and search id to URL params * reading index pattern from search source if it is there for the locator * remove "type" from locator params, update comments and test * removed duplicate identifier * remove unused type Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../usage_collector/get_usage_collector.ts | 2 +- src/plugins/visualizations/common/types.ts | 10 +- .../saved_visualization_references.test.ts | 8 +- src/plugins/visualizations/public/types.ts | 4 +- src/plugins/visualizations/public/vis.ts | 6 +- src/plugins/visualize/common/constants.ts | 13 ++ src/plugins/visualize/common/locator.test.ts | 133 ++++++++++++++++++ src/plugins/visualize/common/locator.ts | 133 ++++++++++++++++++ src/plugins/visualize/common/types.ts | 10 ++ .../visualize/public/application/types.ts | 10 +- .../utils/create_visualize_app_state.ts | 1 - .../application/utils/get_top_nav_config.tsx | 32 ++++- .../public/application/utils/stubs.ts | 1 - .../utils/use/use_visualize_app_state.test.ts | 2 - .../public/application/visualize_constants.ts | 13 +- src/plugins/visualize/public/plugin.ts | 8 +- src/plugins/visualize/tsconfig.json | 8 +- .../functional/apps/visualize/reporting.ts | 4 +- 18 files changed, 355 insertions(+), 43 deletions(-) create mode 100644 src/plugins/visualize/common/locator.test.ts create mode 100644 src/plugins/visualize/common/locator.ts create mode 100644 src/plugins/visualize/common/types.ts diff --git a/src/plugins/vis_types/vega/server/usage_collector/get_usage_collector.ts b/src/plugins/vis_types/vega/server/usage_collector/get_usage_collector.ts index 67fcb742c424..0e67af1c2e89 100644 --- a/src/plugins/vis_types/vega/server/usage_collector/get_usage_collector.ts +++ b/src/plugins/vis_types/vega/server/usage_collector/get_usage_collector.ts @@ -78,7 +78,7 @@ export const getStats = async ( const doTelemetry = ({ params }: SavedVisState) => { try { - const spec = parse(params.spec, { legacyRoot: false }); + const spec = parse(params.spec as string, { legacyRoot: false }); if (spec) { shouldPublishTelemetry = true; diff --git a/src/plugins/visualizations/common/types.ts b/src/plugins/visualizations/common/types.ts index 0bdae26f01f9..187e55566c9d 100644 --- a/src/plugins/visualizations/common/types.ts +++ b/src/plugins/visualizations/common/types.ts @@ -7,18 +7,20 @@ */ import { SavedObjectAttributes } from 'kibana/server'; -import { AggConfigOptions } from 'src/plugins/data/common'; +import type { SerializableRecord } from '@kbn/utility-types'; +import { AggConfigSerialized } from 'src/plugins/data/common'; export interface VisParams { [key: string]: any; } -export interface SavedVisState { +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type SavedVisState = { title: string; type: string; params: TVisParams; - aggs: AggConfigOptions[]; -} + aggs: AggConfigSerialized[]; +}; export interface VisualizationSavedObjectAttributes extends SavedObjectAttributes { description: string; diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/saved_visualization_references.test.ts b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/saved_visualization_references.test.ts index 867febd2544b..9c832414e7f0 100644 --- a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/saved_visualization_references.test.ts +++ b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/saved_visualization_references.test.ts @@ -115,7 +115,7 @@ describe('injectReferences', () => { }); test('injects references into context', () => { - const context = { + const context = ({ id: '1', title: 'test', savedSearchRefName: 'search_0', @@ -133,7 +133,7 @@ describe('injectReferences', () => { ], }, } as unknown) as SavedVisState, - } as VisSavedObject; + } as unknown) as VisSavedObject; const references = [ { name: 'search_0', @@ -182,7 +182,7 @@ describe('injectReferences', () => { }); test(`fails when it can't find the index pattern reference in the array`, () => { - const context = { + const context = ({ id: '1', title: 'test', visState: ({ @@ -196,7 +196,7 @@ describe('injectReferences', () => { ], }, } as unknown) as SavedVisState, - } as VisSavedObject; + } as unknown) as VisSavedObject; expect(() => injectReferences(context, [])).toThrowErrorMatchingInlineSnapshot( `"Could not find index pattern reference \\"control_0_index_pattern\\""` ); diff --git a/src/plugins/visualizations/public/types.ts b/src/plugins/visualizations/public/types.ts index c6eceb86b345..d68599c0724f 100644 --- a/src/plugins/visualizations/public/types.ts +++ b/src/plugins/visualizations/public/types.ts @@ -8,10 +8,10 @@ import { SavedObject } from '../../../plugins/saved_objects/public'; import { - AggConfigOptions, IAggConfigs, SearchSourceFields, TimefilterContract, + AggConfigSerialized, } from '../../../plugins/data/public'; import { ExpressionAstExpression } from '../../expressions/public'; @@ -24,7 +24,7 @@ export interface SavedVisState { title: string; type: string; params: VisParams; - aggs: AggConfigOptions[]; + aggs: AggConfigSerialized[]; } export interface ISavedVis { diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts index ff4e8a3794e0..dfab4ecfc3cd 100644 --- a/src/plugins/visualizations/public/vis.ts +++ b/src/plugins/visualizations/public/vis.ts @@ -26,7 +26,7 @@ import { IAggConfigs, IndexPattern, ISearchSource, - AggConfigOptions, + AggConfigSerialized, SearchSourceFields, } from '../../../plugins/data/public'; import { BaseVisType } from './vis_types'; @@ -34,7 +34,7 @@ import { VisParams } from '../common/types'; export interface SerializedVisData { expression?: string; - aggs: AggConfigOptions[]; + aggs: AggConfigSerialized[]; searchSource: SearchSourceFields; savedSearchId?: string; } @@ -194,7 +194,7 @@ export class Vis { } } - private initializeDefaultsFromSchemas(configStates: AggConfigOptions[], schemas: any) { + private initializeDefaultsFromSchemas(configStates: AggConfigSerialized[], schemas: any) { // Set the defaults for any schema which has them. If the defaults // for some reason has more then the max only set the max number // of defaults (not sure why a someone define more... diff --git a/src/plugins/visualize/common/constants.ts b/src/plugins/visualize/common/constants.ts index 5fe8ed7e095a..10a4498193e3 100644 --- a/src/plugins/visualize/common/constants.ts +++ b/src/plugins/visualize/common/constants.ts @@ -8,3 +8,16 @@ export const STATE_STORAGE_KEY = '_a'; export const GLOBAL_STATE_STORAGE_KEY = '_g'; + +export const APP_NAME = 'visualize'; + +export const VisualizeConstants = { + VISUALIZE_BASE_PATH: '/app/visualize', + LANDING_PAGE_PATH: '/', + WIZARD_STEP_1_PAGE_PATH: '/new', + WIZARD_STEP_2_PAGE_PATH: '/new/configure', + CREATE_PATH: '/create', + EDIT_PATH: '/edit', + EDIT_BY_VALUE_PATH: '/edit_by_value', + APP_ID: 'visualize', +}; diff --git a/src/plugins/visualize/common/locator.test.ts b/src/plugins/visualize/common/locator.test.ts new file mode 100644 index 000000000000..c08c6a910327 --- /dev/null +++ b/src/plugins/visualize/common/locator.test.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { VisualizeLocatorDefinition } from './locator'; +import { FilterStateStore } from '../../data/common'; + +describe('visualize locator', () => { + let definition: VisualizeLocatorDefinition; + + beforeEach(() => { + definition = new VisualizeLocatorDefinition(); + }); + + it('returns a location for "create" path', async () => { + const location = await definition.getLocation({}); + + expect(location.app).toMatchInlineSnapshot(`"visualize"`); + expect(location.path).toMatchInlineSnapshot(`"#/create?_g=()&_a=()"`); + expect(location.state).toMatchInlineSnapshot(`Object {}`); + }); + + it('returns a location for "edit" path', async () => { + const location = await definition.getLocation({ + visId: 'test', + vis: { + title: 'test', + type: 'test', + aggs: [], + params: {}, + }, + }); + + expect(location.app).toMatchInlineSnapshot(`"visualize"`); + expect(location.path).toMatchInlineSnapshot( + `"#/edit/test?_g=()&_a=(vis:(aggs:!(),params:(),title:test,type:test))&type=test"` + ); + expect(location.state).toMatchInlineSnapshot(`Object {}`); + }); + + it('creates a location with query, filters (global and app), refresh interval and time range', async () => { + const location = await definition.getLocation({ + visId: '123', + vis: { + title: 'test', + type: 'test', + aggs: [], + params: {}, + }, + timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, + refreshInterval: { pause: false, value: 300 }, + filters: [ + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'hi' }, + }, + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'hi' }, + $state: { + store: FilterStateStore.GLOBAL_STATE, + }, + }, + ], + query: { query: 'bye', language: 'kuery' }, + }); + + expect(location.app).toMatchInlineSnapshot(`"visualize"`); + + expect(location.path.match(/filters:/g)?.length).toBe(2); + expect(location.path.match(/refreshInterval:/g)?.length).toBe(1); + expect(location.path.match(/time:/g)?.length).toBe(1); + expect(location.path).toMatchInlineSnapshot( + `"#/edit/123?_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))&_a=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),query:(language:kuery,query:bye),vis:(aggs:!(),params:(),title:test,type:test))&type=test"` + ); + + expect(location.state).toMatchInlineSnapshot(`Object {}`); + }); + + it('creates a location with all values provided', async () => { + const indexPattern = 'indexPatternTest'; + const savedSearchId = 'savedSearchIdTest'; + const location = await definition.getLocation({ + visId: '123', + vis: { + title: 'test', + type: 'test', + aggs: [], + params: {}, + }, + timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, + refreshInterval: { pause: false, value: 300 }, + filters: [ + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'hi' }, + }, + ], + query: { query: 'bye', language: 'kuery' }, + linked: true, + uiState: { + fakeUIState: 'fakeUIState', + this: 'value contains a spaces that should be encoded', + }, + indexPattern, + savedSearchId, + }); + + expect(location.app).toMatchInlineSnapshot(`"visualize"`); + expect(location.path).toContain(indexPattern); + expect(location.path).toContain(savedSearchId); + expect(location.path).toMatchInlineSnapshot( + `"#/edit/123?_g=(filters:!(),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))&_a=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),linked:!t,query:(language:kuery,query:bye),uiState:(fakeUIState:fakeUIState,this:'value%20contains%20a%20spaces%20that%20should%20be%20encoded'),vis:(aggs:!(),params:(),title:test,type:test))&indexPattern=indexPatternTest&savedSearchId=savedSearchIdTest&type=test"` + ); + expect(location.state).toMatchInlineSnapshot(`Object {}`); + }); +}); diff --git a/src/plugins/visualize/common/locator.ts b/src/plugins/visualize/common/locator.ts new file mode 100644 index 000000000000..23fde918780f --- /dev/null +++ b/src/plugins/visualize/common/locator.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SerializableRecord, Serializable } from '@kbn/utility-types'; +import { omitBy } from 'lodash'; +import type { ParsedQuery } from 'query-string'; +import { stringify } from 'query-string'; +import rison from 'rison-node'; +import type { Filter, Query, RefreshInterval, TimeRange } from 'src/plugins/data/common'; +import type { LocatorDefinition, LocatorPublic } from 'src/plugins/share/common'; +import { isFilterPinned } from '../../data/common'; +import { url } from '../../kibana_utils/common'; +import { GLOBAL_STATE_STORAGE_KEY, STATE_STORAGE_KEY, VisualizeConstants } from './constants'; +import { PureVisState } from './types'; + +const removeEmptyKeys = (o: Record): Record => + omitBy(o, (v) => v == null); + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type VisualizeLocatorParams = { + /** + * The ID of the saved visualization to load. + */ + visId?: string; + + /** + * Global- and app-level filters to apply to data loaded by visualize. + */ + filters?: Filter[]; + + /** + * Time range to apply to data loaded by visualize. + */ + timeRange?: TimeRange; + + /** + * How frequently to poll for data. + */ + refreshInterval?: RefreshInterval; + + /** + * The query to use in to load data in visualize. + */ + query?: Query; + + /** + * UI state to be passed on to the current visualization. This value is opaque from the perspective of visualize. + */ + uiState?: SerializableRecord; + + /** + * Serialized visualization. + * + * @note This is required to navigate to "create" page (i.e., when no `visId` has been provided). + */ + vis?: PureVisState; + + /** + * Whether this visualization is linked a saved search. + */ + linked?: boolean; + + /** + * The saved search used as the source of the visualization. + */ + savedSearchId?: string; + + /** + * The saved search used as the source of the visualization. + */ + indexPattern?: string; +}; + +export type VisualizeAppLocator = LocatorPublic; + +export const VISUALIZE_APP_LOCATOR = 'VISUALIZE_APP_LOCATOR'; + +export class VisualizeLocatorDefinition implements LocatorDefinition { + id = VISUALIZE_APP_LOCATOR; + + public async getLocation({ + visId, + timeRange, + filters, + refreshInterval, + linked, + uiState, + query, + vis, + savedSearchId, + indexPattern, + }: VisualizeLocatorParams) { + let path = visId + ? `#${VisualizeConstants.EDIT_PATH}/${visId}` + : `#${VisualizeConstants.CREATE_PATH}`; + + const urlState: ParsedQuery = { + [GLOBAL_STATE_STORAGE_KEY]: rison.encode( + removeEmptyKeys({ + time: timeRange, + filters: filters?.filter((f) => isFilterPinned(f)), + refreshInterval, + }) + ), + [STATE_STORAGE_KEY]: rison.encode( + removeEmptyKeys({ + linked, + filters: filters?.filter((f) => !isFilterPinned(f)), + uiState, + query, + vis, + }) + ), + }; + + path += `?${stringify(url.encodeQuery(urlState), { encode: false, sort: false })}`; + + const otherParams = stringify({ type: vis?.type, savedSearchId, indexPattern }); + + if (otherParams) path += `&${otherParams}`; + + return { + app: VisualizeConstants.APP_ID, + path, + state: {}, + }; + } +} diff --git a/src/plugins/visualize/common/types.ts b/src/plugins/visualize/common/types.ts new file mode 100644 index 000000000000..189c44ba15cc --- /dev/null +++ b/src/plugins/visualize/common/types.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import type { SavedVisState } from 'src/plugins/visualizations/common/types'; + +export type PureVisState = SavedVisState; diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index f850aedc3336..7e9f69163f5a 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -9,6 +9,8 @@ import type { EventEmitter } from 'events'; import type { History } from 'history'; +import type { SerializableRecord } from '@kbn/utility-types'; + import type { CoreStart, PluginInitializerContext, @@ -19,7 +21,6 @@ import type { } from 'kibana/public'; import type { - SavedVisState, VisualizationsStart, Vis, VisualizeEmbeddableContract, @@ -45,11 +46,11 @@ import type { DashboardStart } from '../../../dashboard/public'; import type { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public'; import type { UsageCollectionStart } from '../../../usage_collection/public'; -export type PureVisState = SavedVisState; +import { PureVisState } from '../../common/types'; export interface VisualizeAppState { filters: Filter[]; - uiState: Record; + uiState: SerializableRecord; vis: PureVisState; query: Query; savedQuery?: string; @@ -103,6 +104,7 @@ export interface VisualizeServices extends CoreStart { savedObjectsTagging?: SavedObjectsTaggingApi; presentationUtil: PresentationUtilPluginStart; usageCollection?: UsageCollectionStart; + getKibanaVersion: () => string; } export interface SavedVisInstance { @@ -146,3 +148,5 @@ export interface EditorRenderProps { */ linked: boolean; } + +export { PureVisState }; diff --git a/src/plugins/visualize/public/application/utils/create_visualize_app_state.ts b/src/plugins/visualize/public/application/utils/create_visualize_app_state.ts index e288996fa6f3..10c573090da3 100644 --- a/src/plugins/visualize/public/application/utils/create_visualize_app_state.ts +++ b/src/plugins/visualize/public/application/utils/create_visualize_app_state.ts @@ -63,7 +63,6 @@ const pureTransitions = { function createVisualizeByValueAppState(stateDefaults: VisualizeAppState) { const initialState = migrateAppState({ ...stateDefaults, - ...stateDefaults, }); const stateContainer = createStateContainer( initialState, diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index ed361bbdb104..a4421d9535c7 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -7,8 +7,10 @@ */ import React from 'react'; +import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { METRIC_TYPE } from '@kbn/analytics'; +import { parse } from 'query-string'; import { Capabilities } from 'src/core/public'; import { TopNavMenuData } from 'src/plugins/navigation/public'; @@ -33,6 +35,7 @@ import { import { APP_NAME, VisualizeConstants } from '../visualize_constants'; import { getEditBreadcrumbs } from './breadcrumbs'; import { EmbeddableStateTransfer } from '../../../../embeddable/public'; +import { VISUALIZE_APP_LOCATOR, VisualizeLocatorParams } from '../../../common/locator'; interface VisualizeCapabilities { createShortUrl: boolean; @@ -95,6 +98,7 @@ export const getTopNavConfig = ( savedObjectsTagging, presentationUtil, usageCollection, + getKibanaVersion, }: VisualizeServices ) => { const { vis, embeddableHandler } = visInstance; @@ -279,6 +283,22 @@ export const getTopNavConfig = ( testId: 'shareTopNavButton', run: (anchorElement) => { if (share && !embeddableId) { + const currentState = stateContainer.getState(); + const searchParams = parse(history.location.search); + const params: VisualizeLocatorParams = { + visId: savedVis?.id, + filters: currentState.filters, + refreshInterval: undefined, + timeRange: data.query.timefilter.timefilter.getTime(), + uiState: currentState.uiState, + query: currentState.query, + vis: currentState.vis, + linked: currentState.linked, + indexPattern: + visInstance.savedSearch?.searchSource?.getField('index')?.id ?? + (searchParams.indexPattern as string), + savedSearchId: visInstance.savedSearch?.id ?? (searchParams.savedSearchId as string), + }; // TODO: support sharing in by-value mode share.toggleShareContextMenu({ anchorElement, @@ -288,7 +308,17 @@ export const getTopNavConfig = ( objectId: savedVis?.id, objectType: 'visualization', sharingData: { - title: savedVis?.title, + title: + savedVis?.title || + i18n.translate('visualize.reporting.defaultReportTitle', { + defaultMessage: 'Visualization [{date}]', + values: { date: moment().toISOString(true) }, + }), + locatorParams: { + id: VISUALIZE_APP_LOCATOR, + version: getKibanaVersion(), + params, + }, }, isDirty: hasUnappliedChanges || hasUnsavedChanges, showPublicUrlSwitch, diff --git a/src/plugins/visualize/public/application/utils/stubs.ts b/src/plugins/visualize/public/application/utils/stubs.ts index 41a017306dc0..086811df02ba 100644 --- a/src/plugins/visualize/public/application/utils/stubs.ts +++ b/src/plugins/visualize/public/application/utils/stubs.ts @@ -27,7 +27,6 @@ export const visualizeAppStateStub: VisualizeAppState = { { id: '1', enabled: true, - // @ts-expect-error type: 'avg', schema: 'metric', params: { field: 'total_quantity', customLabel: 'average items' }, diff --git a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts index b101a3c2feae..26f866d22ce4 100644 --- a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts +++ b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts @@ -157,7 +157,6 @@ describe('useVisualizeAppState', () => { }; it('should successfully update vis state and set up app state container', async () => { - // @ts-expect-error stateContainerGetStateMock.mockImplementation(() => state); const { result, waitForNextUpdate } = renderHook(() => useVisualizeAppState(mockServices, eventEmitter, savedVisInstance) @@ -204,7 +203,6 @@ describe('useVisualizeAppState', () => { it(`should add warning toast and redirect to the landing page if setting new vis state was not successful, e.x. invalid query params`, async () => { - // @ts-expect-error stateContainerGetStateMock.mockImplementation(() => state); // @ts-expect-error savedVisInstance.vis.setState.mockRejectedValue({ diff --git a/src/plugins/visualize/public/application/visualize_constants.ts b/src/plugins/visualize/public/application/visualize_constants.ts index 6e901882a936..19327ac940e9 100644 --- a/src/plugins/visualize/public/application/visualize_constants.ts +++ b/src/plugins/visualize/public/application/visualize_constants.ts @@ -6,15 +6,4 @@ * Side Public License, v 1. */ -export const APP_NAME = 'visualize'; - -export const VisualizeConstants = { - VISUALIZE_BASE_PATH: '/app/visualize', - LANDING_PAGE_PATH: '/', - WIZARD_STEP_1_PAGE_PATH: '/new', - WIZARD_STEP_2_PAGE_PATH: '/new/configure', - CREATE_PATH: '/create', - EDIT_PATH: '/edit', - EDIT_BY_VALUE_PATH: '/edit_by_value', - APP_ID: 'visualize', -}; +export { VisualizeConstants, APP_NAME } from '../../common/constants'; diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 00c3545034b3..d71e7fd81f1d 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -47,6 +47,7 @@ import type { UsageCollectionStart } from '../../usage_collection/public'; import { setVisEditorsRegistry, setUISettings, setUsageCollector } from './services'; import { createVisEditorsRegistry, VisEditorsRegistry } from './vis_editors_registry'; +import { VisualizeLocatorDefinition } from '../common/locator'; export interface VisualizePluginStartDependencies { data: DataPublicPluginStart; @@ -92,7 +93,7 @@ export class VisualizePlugin public setup( core: CoreSetup, - { home, urlForwarding, data }: VisualizePluginSetupDependencies + { home, urlForwarding, data, share }: VisualizePluginSetupDependencies ) { const { appMounted, @@ -209,6 +210,7 @@ export class VisualizePlugin savedObjectsTagging: pluginsStart.savedObjectsTaggingOss?.getTaggingApi(), presentationUtil: pluginsStart.presentationUtil, usageCollection: pluginsStart.usageCollection, + getKibanaVersion: () => this.initializerContext.env.packageInfo.version, }; params.element.classList.add('visAppWrapper'); @@ -241,6 +243,10 @@ export class VisualizePlugin }); } + if (share) { + share.url.locators.create(new VisualizeLocatorDefinition()); + } + return { visEditorsRegistry: this.visEditorsRegistry, } as VisualizePluginSetup; diff --git a/src/plugins/visualize/tsconfig.json b/src/plugins/visualize/tsconfig.json index 4dcf43dadf8b..3f1f7487085b 100644 --- a/src/plugins/visualize/tsconfig.json +++ b/src/plugins/visualize/tsconfig.json @@ -6,11 +6,7 @@ "declaration": true, "declarationMap": true }, - "include": [ - "common/**/*", - "public/**/*", - "server/**/*" - ], + "include": ["common/**/*", "public/**/*", "server/**/*", "../../../typings/**/*"], "references": [ { "path": "../../core/tsconfig.json" }, { "path": "../data/tsconfig.json" }, @@ -28,6 +24,6 @@ { "path": "../kibana_react/tsconfig.json" }, { "path": "../home/tsconfig.json" }, { "path": "../presentation_util/tsconfig.json" }, - { "path": "../discover/tsconfig.json" }, + { "path": "../discover/tsconfig.json" } ] } diff --git a/x-pack/test/functional/apps/visualize/reporting.ts b/x-pack/test/functional/apps/visualize/reporting.ts index c43747c346ca..1e629927ffb4 100644 --- a/x-pack/test/functional/apps/visualize/reporting.ts +++ b/x-pack/test/functional/apps/visualize/reporting.ts @@ -42,13 +42,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); describe('Print PDF button', () => { - it('is not available if new', async () => { + it('is available if new', async () => { await PageObjects.common.navigateToUrl('visualize', 'new', { useActualUrl: true }); await PageObjects.visualize.clickAggBasedVisualizations(); await PageObjects.visualize.clickAreaChart(); await PageObjects.visualize.clickNewSearch('ecommerce'); await PageObjects.reporting.openPdfReportingPanel(); - expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); }); it('becomes available when saved', async () => { From 7f6c6e44eaec4e881c8d107394a6c8e96ca993de Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Fri, 3 Sep 2021 16:32:10 +0100 Subject: [PATCH 25/47] [ML] Add API integration tests for start and stop datafeeds (#110961) * [ML] Add API integration tests for start and stop datafeeds * [ML] Edits to setup and clean-up steps following review --- .../apis/ml/jobs/force_start_datafeeds.ts | 249 ++++++++++++++++++ .../ml/jobs/force_start_datafeeds_spaces.ts | 126 +++++++++ .../api_integration/apis/ml/jobs/index.ts | 4 + .../apis/ml/jobs/stop_datafeeds.ts | 246 +++++++++++++++++ .../apis/ml/jobs/stop_datafeeds_spaces.ts | 123 +++++++++ 5 files changed, 748 insertions(+) create mode 100644 x-pack/test/api_integration/apis/ml/jobs/force_start_datafeeds.ts create mode 100644 x-pack/test/api_integration/apis/ml/jobs/force_start_datafeeds_spaces.ts create mode 100644 x-pack/test/api_integration/apis/ml/jobs/stop_datafeeds.ts create mode 100644 x-pack/test/api_integration/apis/ml/jobs/stop_datafeeds_spaces.ts diff --git a/x-pack/test/api_integration/apis/ml/jobs/force_start_datafeeds.ts b/x-pack/test/api_integration/apis/ml/jobs/force_start_datafeeds.ts new file mode 100644 index 000000000000..04ab308a0d7b --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/jobs/force_start_datafeeds.ts @@ -0,0 +1,249 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { JOB_STATE, DATAFEED_STATE } from '../../../../../plugins/ml/common/constants/states'; +import { MULTI_METRIC_JOB_CONFIG, SINGLE_METRIC_JOB_CONFIG, DATAFEED_CONFIG } from './common_jobs'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testSetupJobConfigs = [SINGLE_METRIC_JOB_CONFIG, MULTI_METRIC_JOB_CONFIG]; + + async function runStartDatafeedsRequest( + user: USER, + requestBody: object, + expectedResponsecode: number + ): Promise> { + const { body } = await supertest + .post('/api/ml/jobs/force_start_datafeeds') + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(expectedResponsecode); + + return body; + } + + const testDataList = [ + { + testTitle: 'as ML Poweruser', + jobIds: [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id], + user: USER.ML_POWERUSER, + requestBody: { + datafeedIds: [ + `datafeed-${SINGLE_METRIC_JOB_CONFIG.job_id}`, + `datafeed-${MULTI_METRIC_JOB_CONFIG.job_id}`, + ], + start: 1454803200000, // Starts in real-time from Feb 7 2016 00:00 + }, + expected: { + responseCode: 200, + responseBody: { + [`datafeed-${SINGLE_METRIC_JOB_CONFIG.job_id}`]: { started: true }, + [`datafeed-${MULTI_METRIC_JOB_CONFIG.job_id}`]: { started: true }, + }, + }, + }, + ]; + + const invalidTestDataList = [ + { + testTitle: 'as ML Poweruser with datafeed ID that does not exist', + jobIds: [SINGLE_METRIC_JOB_CONFIG.job_id], + user: USER.ML_POWERUSER, + requestBody: { + datafeedIds: [`invalid-datafeed-${SINGLE_METRIC_JOB_CONFIG.job_id}`], + start: 1454803200000, // Feb 7 2016 00:00 + }, + expected: { + responseCode: 200, + responseBody: { + [`invalid-datafeed-${SINGLE_METRIC_JOB_CONFIG.job_id}`]: { started: false }, + }, + }, + }, + ]; + + const testDataListUnauthorized = [ + { + testTitle: 'as ML Unauthorized user', + user: USER.ML_UNAUTHORIZED, + jobIds: [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id], + requestBody: { + datafeedIds: [ + `datafeed-${SINGLE_METRIC_JOB_CONFIG.job_id}`, + `datafeed-${MULTI_METRIC_JOB_CONFIG.job_id}`, + ], + start: 1454803200000, // Feb 7 2016 00:00 + end: 1455235200000, // Feb 12 2016 00:00 + }, + expected: { + responseCode: 403, + error: 'Forbidden', + }, + }, + { + testTitle: 'as ML Viewer', + user: USER.ML_VIEWER, + jobIds: [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id], + requestBody: { + datafeedIds: [ + `datafeed-${SINGLE_METRIC_JOB_CONFIG.job_id}`, + `datafeed-${MULTI_METRIC_JOB_CONFIG.job_id}`, + ], + start: 1454803200000, // Feb 7 2016 00:00 + end: 1455235200000, // Feb 12 2016 00:00 + }, + expected: { + responseCode: 403, + error: 'Forbidden', + }, + }, + ]; + + describe('force_start_datafeeds', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + for (const job of testSetupJobConfigs) { + const datafeedId = `datafeed-${job.job_id}`; + await ml.api.createAnomalyDetectionJob(job); + await ml.api.createDatafeed({ + ...DATAFEED_CONFIG, + datafeed_id: datafeedId, + job_id: job.job_id, + }); + } + }); + + after(async () => { + for (const job of testSetupJobConfigs) { + await ml.api.deleteAnomalyDetectionJobES(job.job_id); + } + await ml.api.cleanMlIndices(); + }); + + describe('rejects requests for unauthorized users', function () { + for (const testData of testDataListUnauthorized) { + describe('fails to force start supplied datafeed IDs', function () { + it(`${testData.testTitle}`, async () => { + const body = await runStartDatafeedsRequest( + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + + expect(body).to.have.property('error').eql(testData.expected.error); + + // check jobs are still closed + for (const id of testData.jobIds) { + await ml.api.waitForJobState(id, JOB_STATE.CLOSED); + } + + // check datafeeds are still stopped + for (const id of testData.requestBody.datafeedIds) { + await ml.api.waitForDatafeedState(id, DATAFEED_STATE.STOPPED); + } + }); + }); + } + }); + + describe('starts datafeeds with supplied IDs', function () { + for (const testData of testDataList) { + it(`${testData.testTitle}`, async () => { + const body = await runStartDatafeedsRequest( + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + + const expectedResponse = testData.expected.responseBody; + const expectedRspDatafeedIds = Object.keys(expectedResponse).sort((a, b) => + a.localeCompare(b) + ); + const actualRspDatafeedIds = Object.keys(body).sort((a, b) => a.localeCompare(b)); + + expect(actualRspDatafeedIds).to.have.length(expectedRspDatafeedIds.length); + expect(actualRspDatafeedIds).to.eql(expectedRspDatafeedIds); + + // check jobs are open + for (const id of testData.jobIds) { + await ml.api.waitForJobState(id, JOB_STATE.OPENED); + } + + // check datafeeds have started + for (const id of testData.requestBody.datafeedIds) { + await ml.api.waitForDatafeedState(id, DATAFEED_STATE.STARTED); + } + }); + } + }); + + describe('succeeds with datafeed already started', function () { + for (const testData of testDataList) { + it(`${testData.testTitle}`, async () => { + const body = await runStartDatafeedsRequest( + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + + const expectedResponse = testData.expected.responseBody; + const expectedRspDatafeedIds = Object.keys(expectedResponse).sort((a, b) => + a.localeCompare(b) + ); + const actualRspDatafeedIds = Object.keys(body).sort((a, b) => a.localeCompare(b)); + + expect(actualRspDatafeedIds).to.have.length(expectedRspDatafeedIds.length); + expect(actualRspDatafeedIds).to.eql(expectedRspDatafeedIds); + + // check jobs are still open + for (const id of testData.jobIds) { + await ml.api.waitForJobState(id, JOB_STATE.OPENED); + } + + // check datafeeds are still started + for (const id of testData.requestBody.datafeedIds) { + await ml.api.waitForDatafeedState(id, DATAFEED_STATE.STARTED); + } + }); + } + }); + + describe('returns expected response for invalid request', function () { + for (const testData of invalidTestDataList) { + it(`${testData.testTitle}`, async () => { + const body = await runStartDatafeedsRequest( + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + const expectedResponse = testData.expected.responseBody; + const expectedRspDatafeedIds = Object.keys(expectedResponse).sort((a, b) => + a.localeCompare(b) + ); + const actualRspDatafeedIds = Object.keys(body).sort((a, b) => a.localeCompare(b)); + + expect(actualRspDatafeedIds).to.have.length(expectedRspDatafeedIds.length); + expect(actualRspDatafeedIds).to.eql(expectedRspDatafeedIds); + + expectedRspDatafeedIds.forEach((id) => { + expect(body[id].started).to.eql(expectedResponse[id].started); + }); + }); + } + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/jobs/force_start_datafeeds_spaces.ts b/x-pack/test/api_integration/apis/ml/jobs/force_start_datafeeds_spaces.ts new file mode 100644 index 000000000000..1ebc6c5b7842 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/jobs/force_start_datafeeds_spaces.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { DATAFEED_STATE } from '../../../../../plugins/ml/common/constants/states'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + const spacesService = getService('spaces'); + const supertest = getService('supertestWithoutAuth'); + + const idSpace1 = 'space1'; + const idSpace2 = 'space2'; + const jobIdSpace1 = `fq_single_${idSpace1}`; + const jobIdSpace2 = `fq_single_${idSpace2}`; + const datafeedIdSpace1 = `datafeed-${jobIdSpace1}`; + const datafeedIdSpace2 = `datafeed-${jobIdSpace2}`; + const startMs = 1454803200000; // Feb 7 2016 00:00 + const endMs = 1455235200000; // Feb 12 2016 00:00 + + async function runRequest( + space: string, + expectedStatusCode: number, + datafeedIds: string[], + start: number, + end: number + ): Promise> { + const { body } = await supertest + .post(`/s/${space}/api/ml/jobs/force_start_datafeeds`) + .auth( + USER.ML_POWERUSER_ALL_SPACES, + ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER_ALL_SPACES) + ) + .set(COMMON_REQUEST_HEADERS) + .send({ datafeedIds, start, end }) + .expect(expectedStatusCode); + + return body; + } + + describe('force_start_datafeeds with spaces', function () { + before(async () => { + await spacesService.create({ id: idSpace1, name: 'space_one', disabledFeatures: [] }); + await spacesService.create({ id: idSpace2, name: 'space_two', disabledFeatures: [] }); + + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + beforeEach(async () => { + const jobConfigSpace1 = ml.commonConfig.getADFqSingleMetricJobConfig(jobIdSpace1); + const datafeedConfigSpace1 = ml.commonConfig.getADFqDatafeedConfig(jobIdSpace1); + await ml.api.createAnomalyDetectionJob(jobConfigSpace1, idSpace1); + await ml.api.createDatafeed( + { + ...datafeedConfigSpace1, + datafeed_id: datafeedIdSpace1, + job_id: jobIdSpace1, + }, + idSpace1 + ); + const jobConfigSpace2 = ml.commonConfig.getADFqSingleMetricJobConfig(jobIdSpace2); + const datafeedConfigSpace2 = ml.commonConfig.getADFqDatafeedConfig(jobIdSpace2); + await ml.api.createAnomalyDetectionJob(jobConfigSpace2, idSpace2); + await ml.api.createDatafeed( + { + ...datafeedConfigSpace2, + datafeed_id: datafeedIdSpace2, + job_id: jobIdSpace2, + }, + idSpace2 + ); + }); + + afterEach(async () => { + await ml.api.closeAnomalyDetectionJob(jobIdSpace1); + await ml.api.closeAnomalyDetectionJob(jobIdSpace2); + await ml.api.deleteAnomalyDetectionJobES(jobIdSpace1); + await ml.api.deleteAnomalyDetectionJobES(jobIdSpace2); + await ml.api.cleanMlIndices(); + await ml.testResources.cleanMLSavedObjects(); + }); + + after(async () => { + await spacesService.delete(idSpace1); + await spacesService.delete(idSpace2); + }); + + it('should start single datafeed from same space', async () => { + const body = await runRequest(idSpace1, 200, [datafeedIdSpace1], startMs, endMs); + expect(body).to.eql({ [datafeedIdSpace1]: { started: true } }); + await ml.api.waitForDatafeedState(datafeedIdSpace1, DATAFEED_STATE.STARTED); + }); + + it('should not start single datafeed from different space', async () => { + const body = await runRequest(idSpace2, 200, [datafeedIdSpace1], startMs, endMs); + expect(body).to.eql({ [datafeedIdSpace1]: { error: 'Job has no datafeed', started: false } }); + await ml.api.waitForDatafeedState(datafeedIdSpace1, DATAFEED_STATE.STOPPED); + }); + + it('should only start datafeed from same space when called with a list of datafeeds', async () => { + const body = await runRequest( + idSpace1, + 200, + [datafeedIdSpace1, datafeedIdSpace2], + startMs, + endMs + ); + expect(body).to.eql({ + [datafeedIdSpace1]: { started: true }, + [datafeedIdSpace2]: { error: 'Job has no datafeed', started: false }, + }); + await ml.api.waitForDatafeedState(datafeedIdSpace1, DATAFEED_STATE.STARTED); + await ml.api.waitForDatafeedState(datafeedIdSpace2, DATAFEED_STATE.STOPPED); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/jobs/index.ts b/x-pack/test/api_integration/apis/ml/jobs/index.ts index 4c52f2ef862c..91368251ff2d 100644 --- a/x-pack/test/api_integration/apis/ml/jobs/index.ts +++ b/x-pack/test/api_integration/apis/ml/jobs/index.ts @@ -18,5 +18,9 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./close_jobs_spaces')); loadTestFile(require.resolve('./delete_jobs_spaces')); loadTestFile(require.resolve('./datafeed_preview')); + loadTestFile(require.resolve('./force_start_datafeeds')); + loadTestFile(require.resolve('./force_start_datafeeds_spaces')); + loadTestFile(require.resolve('./stop_datafeeds')); + loadTestFile(require.resolve('./stop_datafeeds_spaces')); }); } diff --git a/x-pack/test/api_integration/apis/ml/jobs/stop_datafeeds.ts b/x-pack/test/api_integration/apis/ml/jobs/stop_datafeeds.ts new file mode 100644 index 000000000000..593dfdd2fdfe --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/jobs/stop_datafeeds.ts @@ -0,0 +1,246 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { JOB_STATE, DATAFEED_STATE } from '../../../../../plugins/ml/common/constants/states'; +import { MULTI_METRIC_JOB_CONFIG, SINGLE_METRIC_JOB_CONFIG, DATAFEED_CONFIG } from './common_jobs'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testSetupJobConfigs = [SINGLE_METRIC_JOB_CONFIG, MULTI_METRIC_JOB_CONFIG]; + + async function runStopDatafeedsRequest( + user: USER, + requestBody: object, + expectedResponsecode: number + ): Promise> { + const { body } = await supertest + .post('/api/ml/jobs/stop_datafeeds') + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(expectedResponsecode); + + return body; + } + + const testDataList = [ + { + testTitle: 'as ML Poweruser', + jobIds: [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id], + user: USER.ML_POWERUSER, + requestBody: { + datafeedIds: [ + `datafeed-${SINGLE_METRIC_JOB_CONFIG.job_id}`, + `datafeed-${MULTI_METRIC_JOB_CONFIG.job_id}`, + ], + }, + expected: { + responseCode: 200, + responseBody: { + [`datafeed-${SINGLE_METRIC_JOB_CONFIG.job_id}`]: { stopped: true }, + [`datafeed-${MULTI_METRIC_JOB_CONFIG.job_id}`]: { stopped: true }, + }, + }, + }, + ]; + + const invalidTestDataList = [ + { + testTitle: 'as ML Poweruser with datafeed ID that does not exist', + jobIds: [SINGLE_METRIC_JOB_CONFIG.job_id], + user: USER.ML_POWERUSER, + requestBody: { + datafeedIds: [`invalid-datafeed-${SINGLE_METRIC_JOB_CONFIG.job_id}`], + }, + expected: { + responseCode: 200, + responseBody: { + [`invalid-datafeed-${SINGLE_METRIC_JOB_CONFIG.job_id}`]: { stopped: false }, + }, + }, + }, + ]; + + const testDataListUnauthorized = [ + { + testTitle: 'as ML Unauthorized user', + user: USER.ML_UNAUTHORIZED, + jobIds: [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id], + requestBody: { + datafeedIds: [ + `datafeed-${SINGLE_METRIC_JOB_CONFIG.job_id}`, + `datafeed-${MULTI_METRIC_JOB_CONFIG.job_id}`, + ], + }, + expected: { + responseCode: 403, + error: 'Forbidden', + }, + }, + { + testTitle: 'as ML Viewer', + user: USER.ML_VIEWER, + jobIds: [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id], + requestBody: { + datafeedIds: [ + `datafeed-${SINGLE_METRIC_JOB_CONFIG.job_id}`, + `datafeed-${MULTI_METRIC_JOB_CONFIG.job_id}`, + ], + }, + expected: { + responseCode: 403, + error: 'Forbidden', + }, + }, + ]; + + describe('stop_datafeeds', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + for (const job of testSetupJobConfigs) { + const datafeedId = `datafeed-${job.job_id}`; + await ml.api.createAnomalyDetectionJob(job); + await ml.api.openAnomalyDetectionJob(job.job_id); + await ml.api.createDatafeed({ + ...DATAFEED_CONFIG, + datafeed_id: datafeedId, + job_id: job.job_id, + }); + await ml.api.startDatafeed(datafeedId, { start: '0' }); + await ml.api.waitForDatafeedState(datafeedId, DATAFEED_STATE.STARTED); + } + }); + + after(async () => { + for (const job of testSetupJobConfigs) { + await ml.api.deleteAnomalyDetectionJobES(job.job_id); + } + await ml.api.cleanMlIndices(); + }); + + describe('rejects requests for unauthorized users', function () { + for (const testData of testDataListUnauthorized) { + describe('fails to stop supplied datafeed IDs', function () { + it(`${testData.testTitle}`, async () => { + const body = await runStopDatafeedsRequest( + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + + expect(body).to.have.property('error').eql(testData.expected.error); + + // check jobs are still opened + for (const id of testData.jobIds) { + await ml.api.waitForJobState(id, JOB_STATE.OPENED); + } + + // check datafeeds are still started + for (const id of testData.requestBody.datafeedIds) { + await ml.api.waitForDatafeedState(id, DATAFEED_STATE.STARTED); + } + }); + }); + } + }); + + describe('succeeds for ML Poweruser with datafeed started', function () { + for (const testData of testDataList) { + it(`${testData.testTitle}`, async () => { + const body = await runStopDatafeedsRequest( + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + + const expectedResponse = testData.expected.responseBody; + const expectedRspDatafeedIds = Object.keys(expectedResponse).sort((a, b) => + a.localeCompare(b) + ); + const actualRspDatafeedIds = Object.keys(body).sort((a, b) => a.localeCompare(b)); + + expect(actualRspDatafeedIds).to.have.length(expectedRspDatafeedIds.length); + expect(actualRspDatafeedIds).to.eql(expectedRspDatafeedIds); + + // check datafeeds have stopped + for (const id of testData.requestBody.datafeedIds) { + await ml.api.waitForDatafeedState(id, DATAFEED_STATE.STOPPED, 4 * 60 * 1000); + } + + // check jobs are still open + for (const id of testData.jobIds) { + await ml.api.waitForJobState(id, JOB_STATE.OPENED); + } + }); + } + }); + + describe('succeeds for ML Poweruser with datafeed already stopped', function () { + for (const testData of testDataList) { + it(`${testData.testTitle}`, async () => { + const body = await runStopDatafeedsRequest( + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + + const expectedResponse = testData.expected.responseBody; + const expectedRspDatafeedIds = Object.keys(expectedResponse).sort((a, b) => + a.localeCompare(b) + ); + const actualRspDatafeedIds = Object.keys(body).sort((a, b) => a.localeCompare(b)); + + expect(actualRspDatafeedIds).to.have.length(expectedRspDatafeedIds.length); + expect(actualRspDatafeedIds).to.eql(expectedRspDatafeedIds); + + // check datafeeds have stopped + for (const id of testData.requestBody.datafeedIds) { + await ml.api.waitForDatafeedState(id, DATAFEED_STATE.STOPPED, 4 * 60 * 1000); + } + + // check jobs are still open + for (const id of testData.jobIds) { + await ml.api.waitForJobState(id, JOB_STATE.OPENED); + } + }); + } + }); + + describe('returns expected response for invalid request', function () { + for (const testData of invalidTestDataList) { + it(`${testData.testTitle}`, async () => { + const body = await runStopDatafeedsRequest( + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + const expectedResponse = testData.expected.responseBody; + const expectedRspDatafeedIds = Object.keys(expectedResponse).sort((a, b) => + a.localeCompare(b) + ); + const actualRspDatafeedIds = Object.keys(body).sort((a, b) => a.localeCompare(b)); + + expect(actualRspDatafeedIds).to.have.length(expectedRspDatafeedIds.length); + expect(actualRspDatafeedIds).to.eql(expectedRspDatafeedIds); + + expectedRspDatafeedIds.forEach((id) => { + expect(body[id].stopped).to.eql(expectedResponse[id].stopped); + }); + }); + } + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/jobs/stop_datafeeds_spaces.ts b/x-pack/test/api_integration/apis/ml/jobs/stop_datafeeds_spaces.ts new file mode 100644 index 000000000000..0e1ac038dc96 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/jobs/stop_datafeeds_spaces.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { DATAFEED_STATE } from '../../../../../plugins/ml/common/constants/states'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + const spacesService = getService('spaces'); + const supertest = getService('supertestWithoutAuth'); + + const idSpace1 = 'space1'; + const idSpace2 = 'space2'; + const jobIdSpace1 = `fq_single_${idSpace1}`; + const jobIdSpace2 = `fq_single_${idSpace2}`; + const datafeedIdSpace1 = `datafeed-${jobIdSpace1}`; + const datafeedIdSpace2 = `datafeed-${jobIdSpace2}`; + + async function runRequest( + space: string, + expectedStatusCode: number, + datafeedIds: string[] + ): Promise> { + const { body } = await supertest + .post(`/s/${space}/api/ml/jobs/stop_datafeeds`) + .auth( + USER.ML_POWERUSER_ALL_SPACES, + ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER_ALL_SPACES) + ) + .set(COMMON_REQUEST_HEADERS) + .send({ datafeedIds }) + .expect(expectedStatusCode); + + return body; + } + + describe('stop_datafeeds with spaces', function () { + before(async () => { + await spacesService.create({ id: idSpace1, name: 'space_one', disabledFeatures: [] }); + await spacesService.create({ id: idSpace2, name: 'space_two', disabledFeatures: [] }); + + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + beforeEach(async () => { + const jobConfigSpace1 = ml.commonConfig.getADFqSingleMetricJobConfig(jobIdSpace1); + const datafeedConfigSpace1 = ml.commonConfig.getADFqDatafeedConfig(jobIdSpace1); + await ml.api.createAnomalyDetectionJob(jobConfigSpace1, idSpace1); + await ml.api.openAnomalyDetectionJob(jobIdSpace1); + await ml.api.createDatafeed( + { + ...datafeedConfigSpace1, + datafeed_id: datafeedIdSpace1, + job_id: jobIdSpace1, + }, + idSpace1 + ); + await ml.api.startDatafeed(datafeedIdSpace1, { start: '0' }); + await ml.api.waitForDatafeedState(datafeedIdSpace1, DATAFEED_STATE.STARTED); + + const jobConfigSpace2 = ml.commonConfig.getADFqSingleMetricJobConfig(jobIdSpace2); + const datafeedConfigSpace2 = ml.commonConfig.getADFqDatafeedConfig(jobIdSpace2); + await ml.api.createAnomalyDetectionJob(jobConfigSpace2, idSpace2); + await ml.api.openAnomalyDetectionJob(jobIdSpace2); + await ml.api.createDatafeed( + { + ...datafeedConfigSpace2, + datafeed_id: datafeedIdSpace2, + job_id: jobIdSpace2, + }, + idSpace2 + ); + await ml.api.startDatafeed(datafeedIdSpace2, { start: '0' }); + await ml.api.waitForDatafeedState(datafeedIdSpace2, DATAFEED_STATE.STARTED); + }); + + afterEach(async () => { + await ml.api.closeAnomalyDetectionJob(jobIdSpace1); + await ml.api.closeAnomalyDetectionJob(jobIdSpace2); + await ml.api.deleteAnomalyDetectionJobES(jobIdSpace1); + await ml.api.deleteAnomalyDetectionJobES(jobIdSpace2); + await ml.api.cleanMlIndices(); + await ml.testResources.cleanMLSavedObjects(); + }); + + after(async () => { + await spacesService.delete(idSpace1); + await spacesService.delete(idSpace2); + }); + + it('should stop single datafeed from same space', async () => { + const body = await runRequest(idSpace1, 200, [datafeedIdSpace1]); + expect(body).to.eql({ [datafeedIdSpace1]: { stopped: true } }); + await ml.api.waitForDatafeedState(datafeedIdSpace1, DATAFEED_STATE.STOPPED); + }); + + it('should not stop single datafeed from different space', async () => { + const body = await runRequest(idSpace2, 200, [datafeedIdSpace1]); + expect(body).to.eql({ [datafeedIdSpace1]: { stopped: false } }); + await ml.api.waitForDatafeedState(datafeedIdSpace1, DATAFEED_STATE.STARTED); + }); + + it('should only stop datafeed from same space when called with a list of datafeeds', async () => { + const body = await runRequest(idSpace1, 200, [datafeedIdSpace1, datafeedIdSpace2]); + expect(body).to.eql({ + [datafeedIdSpace1]: { stopped: true }, + [datafeedIdSpace2]: { stopped: false }, + }); + await ml.api.waitForDatafeedState(datafeedIdSpace1, DATAFEED_STATE.STOPPED); + await ml.api.waitForDatafeedState(datafeedIdSpace2, DATAFEED_STATE.STARTED); + }); + }); +}; From 634ce7f83b9189ce26dc5405f72f00709bdd2dba Mon Sep 17 00:00:00 2001 From: Nick Partridge Date: Fri, 3 Sep 2021 10:57:36 -0500 Subject: [PATCH 26/47] [ML] fix heatmap label colors (#110515) (#111140) --- .../public/application/explorer/swimlane_container.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 0c150773d22b..bc00c6a258a0 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -280,10 +280,10 @@ export const SwimlaneContainer: FC = ({ return { x: selection.times.map((v) => v * 1000), y: selection.lanes }; }, [selection, swimlaneData, swimlaneType]); - const swimLaneConfig: HeatmapSpec['config'] = useMemo(() => { + const swimLaneConfig = useMemo(() => { if (!showSwimlane) return {}; - return { + const config: HeatmapSpec['config'] = { onBrushEnd: (e: HeatmapBrushEvent) => { if (!e.cells.length) return; @@ -318,7 +318,7 @@ export const SwimlaneContainer: FC = ({ yAxisLabel: { visible: true, width: Y_AXIS_LABEL_WIDTH, - fill: euiTheme.euiTextSubduedColor, + textColor: euiTheme.euiTextSubduedColor, padding: Y_AXIS_LABEL_PADDING, formatter: (laneLabel: string) => { return laneLabel === '' ? EMPTY_FIELD_VALUE_LABEL : laneLabel; @@ -327,7 +327,7 @@ export const SwimlaneContainer: FC = ({ }, xAxisLabel: { visible: true, - fill: euiTheme.euiTextSubduedColor, + textColor: euiTheme.euiTextSubduedColor, formatter: (v: number) => { timeBuckets.setInterval(`${swimlaneData.interval}s`); const scaledDateFormat = timeBuckets.getScaledDateFormat(); @@ -346,6 +346,8 @@ export const SwimlaneContainer: FC = ({ ...(showLegend ? { maxLegendHeight: LEGEND_HEIGHT } : {}), timeZone: 'UTC', }; + + return config; }, [ showSwimlane, swimlaneType, From b6ab15e9f44b2527d3acba6fb99ccccaa9104390 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Fri, 3 Sep 2021 17:59:59 +0200 Subject: [PATCH 27/47] Remove kibana.defaultAppId setting (#109798) * Remove kibana.defaultAppId setting * Fix typings * Remove plugin dependency * Use proper navigation method to get to home * Default route for home * Address discover new routing code * Make non existing /kibana URLs working * Fix space awareness * Remove documentation * Remove the setting from docker file * Make defaultRoute forward work properly * Add forward_url tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/setup/settings.asciidoc | 6 --- .../resources/base/bin/kibana-docker | 1 - .../apps/not_found/not_found_route.tsx | 5 +- .../public/application/components/home_app.js | 13 +---- src/plugins/home/public/plugin.ts | 21 +------- src/plugins/kibana_legacy/config.ts | 15 ------ src/plugins/kibana_legacy/kibana.json | 2 +- src/plugins/kibana_legacy/public/index.ts | 4 +- src/plugins/kibana_legacy/public/mocks.ts | 3 -- src/plugins/kibana_legacy/public/plugin.ts | 12 +---- src/plugins/kibana_legacy/server/index.ts | 49 ----------------- src/plugins/url_forwarding/kibana.json | 3 +- .../public/forward_app/forward_app.test.ts | 54 +++++++++++++++++++ .../public/forward_app/forward_app.ts | 15 ++---- src/plugins/url_forwarding/public/mocks.ts | 1 - .../public/navigate_to_default_app.ts | 39 -------------- src/plugins/url_forwarding/public/plugin.ts | 35 +----------- test/functional/apps/home/_home.js | 2 +- 18 files changed, 70 insertions(+), 210 deletions(-) delete mode 100644 src/plugins/kibana_legacy/config.ts delete mode 100644 src/plugins/kibana_legacy/server/index.ts create mode 100644 src/plugins/url_forwarding/public/forward_app/forward_app.test.ts delete mode 100644 src/plugins/url_forwarding/public/navigate_to_default_app.ts diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 203339be638a..78b776c85c93 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -295,12 +295,6 @@ is an alternative to `elasticsearch.username` and `elasticsearch.password`. | `interpreter.enableInVisualize` | Enables use of interpreter in Visualize. *Default: `true`* -| `kibana.defaultAppId:` - | deprecated:[7.9.0,This setting will be removed in Kibana 8.0.] - Instead, use the <>. - + - The default application to load. *Default: `"home"`* - |[[kibana-index]] `kibana.index:` | deprecated:[7.11.0,This setting will be removed in 8.0.] Multitenancy by changing `kibana.index` will not be supported starting in 8.0. See diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index 4bb89e1b7e60..adf0be3b5aa5 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -76,7 +76,6 @@ kibana_vars=( interpreter.enableInVisualize kibana.autocompleteTerminateAfter kibana.autocompleteTimeout - kibana.defaultAppId kibana.index logging.appenders logging.appenders.console diff --git a/src/plugins/discover/public/application/apps/not_found/not_found_route.tsx b/src/plugins/discover/public/application/apps/not_found/not_found_route.tsx index ff515f27201a..cd16a820cc8f 100644 --- a/src/plugins/discover/public/application/apps/not_found/not_found_route.tsx +++ b/src/plugins/discover/public/application/apps/not_found/not_found_route.tsx @@ -28,10 +28,7 @@ export function NotFoundRoute(props: NotFoundRouteProps) { useEffect(() => { const path = window.location.hash.substr(1); getUrlTracker().restorePreviousUrl(); - const { navigated } = urlForwarding.navigateToLegacyKibanaUrl(path); - if (!navigated) { - urlForwarding.navigateToDefaultApp(); - } + urlForwarding.navigateToLegacyKibanaUrl(path); const bannerMessage = i18n.translate('discover.noMatchRoute.bannerTitleText', { defaultMessage: 'Page not found', diff --git a/src/plugins/home/public/application/components/home_app.js b/src/plugins/home/public/application/components/home_app.js index ef70dac20d3c..94413e6af390 100644 --- a/src/plugins/home/public/application/components/home_app.js +++ b/src/plugins/home/public/application/components/home_app.js @@ -12,19 +12,10 @@ import PropTypes from 'prop-types'; import { Home } from './home'; import { TutorialDirectory } from './tutorial_directory'; import { Tutorial } from './tutorial/tutorial'; -import { HashRouter as Router, Switch, Route } from 'react-router-dom'; +import { HashRouter as Router, Switch, Route, Redirect } from 'react-router-dom'; import { getTutorial } from '../load_tutorials'; import { replaceTemplateStrings } from './tutorial/replace_template_strings'; import { getServices } from '../kibana_services'; -import useMount from 'react-use/lib/useMount'; - -const RedirectToDefaultApp = () => { - useMount(() => { - const { urlForwarding } = getServices(); - urlForwarding.navigateToDefaultApp(); - }); - return null; -}; export function HomeApp({ directories, solutions }) { const { @@ -78,7 +69,7 @@ export function HomeApp({ directories, solutions }) { hasUserIndexPattern={() => indexPatternService.hasUserIndexPattern()} /> - + diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index 7dd1d8728ad7..f6a1566b267a 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -14,7 +14,6 @@ import { PluginInitializerContext, } from 'kibana/public'; import { i18n } from '@kbn/i18n'; -import { first } from 'rxjs/operators'; import { EnvironmentService, @@ -137,27 +136,9 @@ export class HomePublicPlugin }; } - public start( - { application: { capabilities, currentAppId$ }, http }: CoreStart, - { urlForwarding }: HomePluginStartDependencies - ) { + public start({ application: { capabilities } }: CoreStart) { this.featuresCatalogueRegistry.start({ capabilities }); - // If the home app is the initial location when loading Kibana... - if ( - window.location.pathname === http.basePath.prepend(HOME_APP_BASE_PATH) && - window.location.hash === '' - ) { - // ...wait for the app to mount initially and then... - currentAppId$.pipe(first()).subscribe((appId) => { - if (appId === 'home') { - // ...navigate to default app set by `kibana.defaultAppId`. - // This doesn't do anything as along as the default settings are kept. - urlForwarding.navigateToDefaultApp({ overwriteHash: false }); - } - }); - } - return { featureCatalogue: this.featuresCatalogueRegistry }; } } diff --git a/src/plugins/kibana_legacy/config.ts b/src/plugins/kibana_legacy/config.ts deleted file mode 100644 index 91083a554bce..000000000000 --- a/src/plugins/kibana_legacy/config.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { schema, TypeOf } from '@kbn/config-schema'; - -export const configSchema = schema.object({ - defaultAppId: schema.string({ defaultValue: 'home' }), -}); - -export type ConfigSchema = TypeOf; diff --git a/src/plugins/kibana_legacy/kibana.json b/src/plugins/kibana_legacy/kibana.json index b1d7b10f9527..afca886ad937 100644 --- a/src/plugins/kibana_legacy/kibana.json +++ b/src/plugins/kibana_legacy/kibana.json @@ -1,7 +1,7 @@ { "id": "kibanaLegacy", "version": "kibana", - "server": true, + "server": false, "ui": true, "owner": { "name": "Vis Editors", diff --git a/src/plugins/kibana_legacy/public/index.ts b/src/plugins/kibana_legacy/public/index.ts index fa04b192cd17..13271532881c 100644 --- a/src/plugins/kibana_legacy/public/index.ts +++ b/src/plugins/kibana_legacy/public/index.ts @@ -9,11 +9,9 @@ // TODO: https://github.com/elastic/kibana/issues/110891 /* eslint-disable @kbn/eslint/no_export_all */ -import { PluginInitializerContext } from 'kibana/public'; import { KibanaLegacyPlugin } from './plugin'; -export const plugin = (initializerContext: PluginInitializerContext) => - new KibanaLegacyPlugin(initializerContext); +export const plugin = () => new KibanaLegacyPlugin(); export * from './plugin'; diff --git a/src/plugins/kibana_legacy/public/mocks.ts b/src/plugins/kibana_legacy/public/mocks.ts index 9f79daf0f350..510e59c7ff19 100644 --- a/src/plugins/kibana_legacy/public/mocks.ts +++ b/src/plugins/kibana_legacy/public/mocks.ts @@ -14,9 +14,6 @@ export type Start = jest.Mocked>; const createSetupContract = (): Setup => ({}); const createStartContract = (): Start => ({ - config: { - defaultAppId: 'home', - }, loadFontAwesome: jest.fn(), loadAngularBootstrap: jest.fn(), }); diff --git a/src/plugins/kibana_legacy/public/plugin.ts b/src/plugins/kibana_legacy/public/plugin.ts index af22ceadaa9e..e5244c110ad2 100644 --- a/src/plugins/kibana_legacy/public/plugin.ts +++ b/src/plugins/kibana_legacy/public/plugin.ts @@ -6,18 +6,15 @@ * Side Public License, v 1. */ -import { PluginInitializerContext, CoreStart, CoreSetup } from 'kibana/public'; -import { ConfigSchema } from '../config'; +import { CoreStart, CoreSetup } from 'kibana/public'; import { injectHeaderStyle } from './utils/inject_header_style'; export class KibanaLegacyPlugin { - constructor(private readonly initializerContext: PluginInitializerContext) {} - public setup(core: CoreSetup<{}, KibanaLegacyStart>) { return {}; } - public start({ application, http: { basePath }, uiSettings }: CoreStart) { + public start({ uiSettings }: CoreStart) { injectHeaderStyle(uiSettings); return { /** @@ -35,11 +32,6 @@ export class KibanaLegacyPlugin { const { initAngularBootstrap } = await import('./angular_bootstrap'); initAngularBootstrap(); }, - /** - * @deprecated - * Just exported for wiring up with dashboard mode, should not be used. - */ - config: this.initializerContext.config.get(), }; } } diff --git a/src/plugins/kibana_legacy/server/index.ts b/src/plugins/kibana_legacy/server/index.ts deleted file mode 100644 index 90c9c2888c9d..000000000000 --- a/src/plugins/kibana_legacy/server/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { CoreSetup, CoreStart, PluginConfigDescriptor } from 'kibana/server'; -import { get } from 'lodash'; - -import { configSchema, ConfigSchema } from '../config'; - -export const config: PluginConfigDescriptor = { - exposeToBrowser: { - defaultAppId: true, - }, - schema: configSchema, - deprecations: ({ renameFromRoot }) => [ - // TODO: Remove deprecation once defaultAppId is deleted - renameFromRoot('kibana.defaultAppId', 'kibana_legacy.defaultAppId', { silent: true }), - (completeConfig, rootPath, addDeprecation) => { - if ( - get(completeConfig, 'kibana.defaultAppId') === undefined && - get(completeConfig, 'kibana_legacy.defaultAppId') === undefined - ) { - return; - } - addDeprecation({ - message: `kibana.defaultAppId is deprecated and will be removed in 8.0. Please use the \`defaultRoute\` advanced setting instead`, - correctiveActions: { - manualSteps: [ - 'Go to Stack Management > Advanced Settings', - 'Update the "defaultRoute" setting under the General section', - 'Remove "kibana.defaultAppId" from the kibana.yml config file', - ], - }, - }); - }, - ], -}; - -class Plugin { - public setup(core: CoreSetup) {} - - public start(core: CoreStart) {} -} - -export const plugin = () => new Plugin(); diff --git a/src/plugins/url_forwarding/kibana.json b/src/plugins/url_forwarding/kibana.json index a8b0571230b7..3e48cf73de5e 100644 --- a/src/plugins/url_forwarding/kibana.json +++ b/src/plugins/url_forwarding/kibana.json @@ -6,6 +6,5 @@ "owner": { "name": "Vis Editors", "githubTeam": "kibana-vis-editors" - }, - "requiredPlugins": ["kibanaLegacy"] + } } diff --git a/src/plugins/url_forwarding/public/forward_app/forward_app.test.ts b/src/plugins/url_forwarding/public/forward_app/forward_app.test.ts new file mode 100644 index 000000000000..c45bde0d6789 --- /dev/null +++ b/src/plugins/url_forwarding/public/forward_app/forward_app.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import type { Location } from 'history'; +import type { AppMountParameters, CoreSetup, ScopedHistory } from 'kibana/public'; +import { coreMock } from '../../../../core/public/mocks'; +import type { UrlForwardingStart } from '../plugin'; +import { createLegacyUrlForwardApp } from './forward_app'; + +function createAppMountParams(hash: string): AppMountParameters { + return { + history: { + location: { + hash, + } as Location, + } as ScopedHistory, + } as AppMountParameters; +} + +describe('forward_app', () => { + let coreSetup: CoreSetup<{}, UrlForwardingStart>; + let coreStart: ReturnType; + + beforeEach(() => { + coreSetup = coreMock.createSetup({ basePath: '/base/path' }); + coreStart = coreMock.createStart({ basePath: '/base/path' }); + coreSetup.getStartServices = () => Promise.resolve([coreStart, {}, {} as any]); + }); + + it('should forward to defaultRoute if hash is not a known redirect', async () => { + coreStart.uiSettings.get.mockImplementation((key) => { + if (key === 'defaultRoute') return '/app/defaultApp'; + throw new Error('Mock implementation missing'); + }); + + const app = createLegacyUrlForwardApp(coreSetup, [ + { legacyAppId: 'discover', newAppId: 'discover', rewritePath: (p) => p }, + ]); + await app.mount(createAppMountParams('#/foobar')); + expect(coreStart.application.navigateToUrl).toHaveBeenCalledWith('/base/path/app/defaultApp'); + }); + + it('should not forward to defaultRoute if hash path is a known redirect', async () => { + const app = createLegacyUrlForwardApp(coreSetup, [ + { legacyAppId: 'discover', newAppId: 'discover', rewritePath: (p) => p }, + ]); + await app.mount(createAppMountParams('#/discover')); + expect(coreStart.application.navigateToUrl).not.toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/url_forwarding/public/forward_app/forward_app.ts b/src/plugins/url_forwarding/public/forward_app/forward_app.ts index 96c4fab5f333..3a66e207f8c2 100644 --- a/src/plugins/url_forwarding/public/forward_app/forward_app.ts +++ b/src/plugins/url_forwarding/public/forward_app/forward_app.ts @@ -23,23 +23,18 @@ export const createLegacyUrlForwardApp = ( async mount(params: AppMountParameters) { const hash = params.history.location.hash.substr(1); - if (!hash) { - const [, , kibanaLegacyStart] = await core.getStartServices(); - kibanaLegacyStart.navigateToDefaultApp(); - } - const [ { application, + uiSettings, http: { basePath }, }, ] = await core.getStartServices(); - const result = await navigateToLegacyKibanaUrl(hash, forwards, basePath, application); - - if (!result.navigated) { - const [, , kibanaLegacyStart] = await core.getStartServices(); - kibanaLegacyStart.navigateToDefaultApp(); + const { navigated } = navigateToLegacyKibanaUrl(hash, forwards, basePath, application); + if (!navigated) { + const defaultRoute = uiSettings.get('defaultRoute'); + application.navigateToUrl(basePath.prepend(defaultRoute)); } return () => {}; diff --git a/src/plugins/url_forwarding/public/mocks.ts b/src/plugins/url_forwarding/public/mocks.ts index 67b521b9d697..582bb004b655 100644 --- a/src/plugins/url_forwarding/public/mocks.ts +++ b/src/plugins/url_forwarding/public/mocks.ts @@ -17,7 +17,6 @@ const createSetupContract = (): Setup => ({ const createStartContract = (): Start => ({ getForwards: jest.fn(), - navigateToDefaultApp: jest.fn(), navigateToLegacyKibanaUrl: jest.fn(), }); diff --git a/src/plugins/url_forwarding/public/navigate_to_default_app.ts b/src/plugins/url_forwarding/public/navigate_to_default_app.ts deleted file mode 100644 index 0c934ac9c684..000000000000 --- a/src/plugins/url_forwarding/public/navigate_to_default_app.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ApplicationStart, IBasePath } from 'kibana/public'; -import { ForwardDefinition } from './plugin'; - -export function navigateToDefaultApp( - defaultAppId: string, - forwards: ForwardDefinition[], - application: ApplicationStart, - basePath: IBasePath, - currentAppId: string | undefined, - overwriteHash: boolean -) { - // navigate to the respective path in the legacy kibana plugin by default (for unmigrated plugins) - let targetAppId = 'kibana'; - let targetAppPath = `#/${defaultAppId}`; - - // try to find an existing redirect for the target path if possible - // this avoids having to load the legacy app just to get redirected to a core application again afterwards - const relevantForward = forwards.find((forward) => defaultAppId.startsWith(forward.legacyAppId)); - if (relevantForward) { - targetAppPath = relevantForward.rewritePath(`/${defaultAppId}`); - targetAppId = relevantForward.newAppId; - } - - // when the correct app is already loaded, just set the hash to the right value - // otherwise use navigateToApp (or setting href in case of kibana app) - if (currentAppId !== targetAppId) { - application.navigateToApp(targetAppId, { path: targetAppPath, replace: true }); - } else if (overwriteHash) { - window.location.hash = targetAppPath; - } -} diff --git a/src/plugins/url_forwarding/public/plugin.ts b/src/plugins/url_forwarding/public/plugin.ts index 1151e853f28b..ee56ba73eb24 100644 --- a/src/plugins/url_forwarding/public/plugin.ts +++ b/src/plugins/url_forwarding/public/plugin.ts @@ -7,9 +7,6 @@ */ import { CoreStart, CoreSetup } from 'kibana/public'; -import { KibanaLegacyStart } from 'src/plugins/kibana_legacy/public'; -import { Subscription } from 'rxjs'; -import { navigateToDefaultApp } from './navigate_to_default_app'; import { createLegacyUrlForwardApp } from './forward_app'; import { navigateToLegacyKibanaUrl } from './forward_app/navigate_to_legacy_kibana_url'; @@ -21,8 +18,6 @@ export interface ForwardDefinition { export class UrlForwardingPlugin { private forwardDefinitions: ForwardDefinition[] = []; - private currentAppId: string | undefined; - private currentAppIdSubscription: Subscription | undefined; public setup(core: CoreSetup<{}, UrlForwardingStart>) { core.application.register(createLegacyUrlForwardApp(core, this.forwardDefinitions)); @@ -71,30 +66,8 @@ export class UrlForwardingPlugin { }; } - public start( - { application, http: { basePath }, uiSettings }: CoreStart, - { kibanaLegacy }: { kibanaLegacy: KibanaLegacyStart } - ) { - this.currentAppIdSubscription = application.currentAppId$.subscribe((currentAppId) => { - this.currentAppId = currentAppId; - }); + public start({ application, http: { basePath } }: CoreStart) { return { - /** - * Navigates to the app defined as kibana.defaultAppId. - * This takes redirects into account and uses the right mechanism to navigate. - */ - navigateToDefaultApp: ( - { overwriteHash }: { overwriteHash: boolean } = { overwriteHash: true } - ) => { - navigateToDefaultApp( - kibanaLegacy.config.defaultAppId, - this.forwardDefinitions, - application, - basePath, - this.currentAppId, - overwriteHash - ); - }, /** * Resolves the provided hash using the registered forwards and navigates to the target app. * If a navigation happened, `{ navigated: true }` will be returned. @@ -111,12 +84,6 @@ export class UrlForwardingPlugin { getForwards: () => this.forwardDefinitions, }; } - - public stop() { - if (this.currentAppIdSubscription) { - this.currentAppIdSubscription.unsubscribe(); - } - } } export type UrlForwardingSetup = ReturnType; diff --git a/test/functional/apps/home/_home.js b/test/functional/apps/home/_home.js index 24e672463964..e3ca3f676111 100644 --- a/test/functional/apps/home/_home.js +++ b/test/functional/apps/home/_home.js @@ -26,7 +26,7 @@ export default function ({ getService, getPageObjects }) { }); it('clicking on console on homepage should take you to console app', async () => { - await PageObjects.common.navigateToUrl('home'); + await PageObjects.common.navigateToApp('home'); await testSubjects.click('homeDevTools'); const url = await browser.getCurrentUrl(); expect(url.includes('/app/dev_tools#/console')).to.be(true); From a2c848e1d2edc367113241aad8d5b5fc7d40d7a0 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Fri, 3 Sep 2021 12:00:51 -0400 Subject: [PATCH 28/47] [Cases] Fix connector information disappearing (#110914) * Move intialization to use effect * Fixing fields can't get test working * Fix tests Co-authored-by: Christos Nasikas --- .../jira/use_get_fields_by_issue_type.tsx | 2 +- .../connectors/jira/use_get_issue_types.tsx | 3 +- .../components/edit_connector/index.test.tsx | 80 ++++++++++++++----- .../components/edit_connector/index.tsx | 53 ++++++++---- 4 files changed, 98 insertions(+), 40 deletions(-) diff --git a/x-pack/plugins/cases/public/components/connectors/jira/use_get_fields_by_issue_type.tsx b/x-pack/plugins/cases/public/components/connectors/jira/use_get_fields_by_issue_type.tsx index a4958d91c88a..686df1022a0d 100644 --- a/x-pack/plugins/cases/public/components/connectors/jira/use_get_fields_by_issue_type.tsx +++ b/x-pack/plugins/cases/public/components/connectors/jira/use_get_fields_by_issue_type.tsx @@ -57,8 +57,8 @@ export const useGetFieldsByIssueType = ({ }); if (!didCancel.current) { - setIsLoading(false); setFields(res.data ?? {}); + setIsLoading(false); if (res.status && res.status === 'error') { toastNotifications.addDanger({ title: i18n.FIELDS_API_ERROR, diff --git a/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue_types.tsx b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue_types.tsx index 447491d2a2ff..0d7073f3bf2d 100644 --- a/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue_types.tsx +++ b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue_types.tsx @@ -56,13 +56,14 @@ export const useGetIssueTypes = ({ }); if (!didCancel.current) { - setIsLoading(false); const asOptions = (res.data ?? []).map((type) => ({ text: type.name ?? '', value: type.id ?? '', })); + setIssueTypes(res.data ?? []); handleIssueType(asOptions); + setIsLoading(false); if (res.status && res.status === 'error') { toastNotifications.addDanger({ title: i18n.ISSUE_TYPES_API_ERROR, diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx index fb45bf6ac3ae..eec84cdd8d90 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx @@ -7,16 +7,14 @@ import React from 'react'; import { mount } from 'enzyme'; -import { waitFor } from '@testing-library/react'; +import { render, waitFor, screen } from '@testing-library/react'; import { EditConnector, EditConnectorProps } from './index'; -import { getFormMock, useFormMock } from '../__mock__/form'; import { TestProviders } from '../../common/mock'; import { connectorsMock } from '../../containers/configure/mock'; import { basicCase, basicPush, caseUserActions } from '../../containers/mock'; import { useKibana } from '../../common/lib/kibana'; -jest.mock('../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'); jest.mock('../../common/lib/kibana'); const useKibanaMock = useKibana as jest.Mocked; @@ -50,17 +48,32 @@ const defaultProps: EditConnectorProps = { }; describe('EditConnector ', () => { - const sampleConnector = '123'; - const formHookMock = getFormMock({ connectorId: sampleConnector }); beforeEach(() => { jest.clearAllMocks(); - useFormMock.mockImplementation(() => ({ form: formHookMock })); useKibanaMock().services.triggersActionsUi.actionTypeRegistry.get = jest.fn().mockReturnValue({ actionTypeTitle: '.servicenow', iconClass: 'logoSecurity', }); }); + it('Renders servicenow connector from case initially', async () => { + const serviceNowProps = { + ...defaultProps, + caseData: { + ...defaultProps.caseData, + connector: { ...defaultProps.caseData.connector, id: 'servicenow-1' }, + }, + }; + + render( + + + + ); + + expect(await screen.findByText('My Connector')).toBeInTheDocument(); + }); + it('Renders no connector, and then edit', async () => { const wrapper = mount( @@ -98,58 +111,81 @@ describe('EditConnector ', () => { expect(wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().exists()).toBeTruthy(); wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click'); - await waitFor(() => expect(onSubmit.mock.calls[0][0]).toBe(sampleConnector)); + await waitFor(() => expect(onSubmit.mock.calls[0][0]).toBe('resilient-2')); }); it('Revert to initial external service on error', async () => { onSubmit.mockImplementation((connector, onSuccess, onError) => { onError(new Error('An error has occurred')); }); + + const props = { + ...defaultProps, + caseData: { + ...defaultProps.caseData, + connector: { ...defaultProps.caseData.connector, id: 'servicenow-1' }, + }, + }; + const wrapper = mount( - + ); - wrapper.find('[data-test-subj="connector-edit"] button').simulate('click'); + wrapper.find('[data-test-subj="connector-edit"] button').simulate('click'); wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click'); - wrapper.update(); - - expect(wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().exists()).toBeTruthy(); + await waitFor(() => { + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click'); + wrapper.update(); + expect( + wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().exists() + ).toBeTruthy(); + wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click'); + }); - wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click'); await waitFor(() => { wrapper.update(); - expect(formHookMock.setFieldValue).toHaveBeenCalledWith('connectorId', 'none'); + expect(wrapper.find(`[data-test-subj="edit-connectors-submit"]`).exists()).toBeFalsy(); }); + + /** + * If an error is being throw on submit the selected connector should + * be reverted to the initial one. In our test the initial one is the .servicenow-1 + * connector. The title of the .servicenow-1 connector is My Connector. + */ + expect(wrapper.text().includes('My Connector')).toBeTruthy(); }); it('Resets selector on cancel', async () => { const props = { ...defaultProps, + caseData: { + ...defaultProps.caseData, + connector: { ...defaultProps.caseData.connector, id: 'servicenow-1' }, + }, }; + const wrapper = mount( ); - wrapper.find('[data-test-subj="connector-edit"] button').simulate('click'); + wrapper.find('[data-test-subj="connector-edit"] button').simulate('click'); wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); wrapper.update(); wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click'); wrapper.update(); - wrapper.find(`[data-test-subj="edit-connectors-cancel"]`).last().simulate('click'); + await waitFor(() => { wrapper.update(); - expect(formHookMock.setFieldValue).toBeCalledWith( - 'connectorId', - defaultProps.caseData.connector.id - ); + expect(wrapper.find(`[data-test-subj="edit-connectors-submit"]`).exists()).toBeFalsy(); }); + + expect(wrapper.text().includes('My Connector')).toBeTruthy(); }); it('Renders loading spinner', async () => { diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.tsx index df7855fb9ce3..70845f28e1f6 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useReducer } from 'react'; +import React, { useCallback, useEffect, useReducer } from 'react'; import deepEqual from 'fast-deep-equal'; import { EuiText, @@ -144,17 +144,43 @@ export const EditConnector = React.memo( { ...initialState, fields: caseFields } ); + useEffect(() => { + // Initialize the current connector with the connector information attached to the case if we can find that + // connector in the retrieved connectors from the API call + if (!isLoading) { + dispatch({ + type: 'SET_CURRENT_CONNECTOR', + payload: getConnectorById(caseData.connector.id, connectors), + }); + + // Set the fields initially to whatever is present in the case, this should match with + // the latest user action for an update connector as well + dispatch({ + type: 'SET_FIELDS', + payload: caseFields, + }); + } + }, [caseData.connector.id, connectors, isLoading, caseFields]); + + /** + * There is a race condition with this callback. At some point during the initial mounting of this component, this + * callback will be called. There are a couple problems with this: + * + * 1. If the call occurs before the above useEffect does its dispatches (aka while the connectors are still loading) this will + * result in setting the current connector to null when in fact we might have a valid connector. It could also + * cause issues when setting the fields because if there are no user actions then the getConnectorFieldsFromUserActions + * will return null even when the caseData.connector.fields is valid and populated. + * + * 2. If the call occurs after the above useEffect then the currentConnector should === newConnectorId + * + * As far as I know dispatch is synchronous so if the useEffect runs first it should successfully set currentConnector. If + * onChangeConnector runs first and sets stuff to null, then when useEffect runs it'll switch everything back to what we need it to be + * initially. + */ const onChangeConnector = useCallback( (newConnectorId) => { - // Init - if (currentConnector == null) { - dispatch({ - type: 'SET_CURRENT_CONNECTOR', - payload: getConnectorById(newConnectorId, connectors), - }); - } - // change connect on dropdown action - else if (currentConnector.id !== newConnectorId) { + // change connector on dropdown action + if (currentConnector?.id !== newConnectorId) { dispatch({ type: 'SET_CURRENT_CONNECTOR', payload: getConnectorById(newConnectorId, connectors), @@ -163,14 +189,9 @@ export const EditConnector = React.memo( type: 'SET_FIELDS', payload: getConnectorFieldsFromUserActions(newConnectorId, userActions ?? []), }); - } else if (fields === null) { - dispatch({ - type: 'SET_FIELDS', - payload: getConnectorFieldsFromUserActions(newConnectorId, userActions ?? []), - }); } }, - [currentConnector, fields, userActions, connectors] + [currentConnector, userActions, connectors] ); const onFieldsChange = useCallback( From d840ea6da9315e1ae33982e13804cee75d8ae857 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Fri, 3 Sep 2021 09:31:00 -0700 Subject: [PATCH 29/47] Update sync workflow to only run on elastic/kibana (#111171) Signed-off-by: Tyler Smalley --- .github/workflows/sync-main-branch.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/sync-main-branch.yml b/.github/workflows/sync-main-branch.yml index 63465602e843..3f2ef967d3ee 100644 --- a/.github/workflows/sync-main-branch.yml +++ b/.github/workflows/sync-main-branch.yml @@ -9,6 +9,7 @@ jobs: sync_latest_from_upstream: runs-on: ubuntu-latest name: Sync latest commits from master branch + if: github.repository == "elastic/kibana" steps: - name: Checkout target repo From c9440bb5ffadf85469e417e2e1548b15cf0cc2fa Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Fri, 3 Sep 2021 09:32:33 -0700 Subject: [PATCH 30/47] Revert "Update sync workflow to only run on elastic/kibana (#111171)" (#111181) This reverts commit d840ea6da9315e1ae33982e13804cee75d8ae857. --- .github/workflows/sync-main-branch.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/sync-main-branch.yml b/.github/workflows/sync-main-branch.yml index 3f2ef967d3ee..63465602e843 100644 --- a/.github/workflows/sync-main-branch.yml +++ b/.github/workflows/sync-main-branch.yml @@ -9,7 +9,6 @@ jobs: sync_latest_from_upstream: runs-on: ubuntu-latest name: Sync latest commits from master branch - if: github.repository == "elastic/kibana" steps: - name: Checkout target repo From cb27ba01c1d6540aeb54e0fb2303351cc2ce0794 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Fri, 3 Sep 2021 09:58:31 -0700 Subject: [PATCH 31/47] Update sync workflow to only run on elastic/kibana (#111183) Signed-off-by: Tyler Smalley --- .github/workflows/sync-main-branch.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/sync-main-branch.yml b/.github/workflows/sync-main-branch.yml index 63465602e843..971ff0b9a635 100644 --- a/.github/workflows/sync-main-branch.yml +++ b/.github/workflows/sync-main-branch.yml @@ -9,6 +9,7 @@ jobs: sync_latest_from_upstream: runs-on: ubuntu-latest name: Sync latest commits from master branch + if: github.repository == 'elastic/kibana' steps: - name: Checkout target repo From df43d253c85fbd6f0afb1eae99948211ef43bed6 Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Fri, 3 Sep 2021 19:10:29 +0200 Subject: [PATCH 32/47] [Expressions] Align renderMode with the embeddable viewMode (#110199) * Add preview view mode to the embeddable * Rename display render mode to view * Extract no interactivity render mode to a separate flag --- src/plugins/embeddable/common/types.ts | 1 + .../common/expression_renderers/types.ts | 11 ++++++++--- src/plugins/expressions/public/loader.ts | 1 + .../public/react_expression_renderer.tsx | 1 + src/plugins/expressions/public/render.ts | 7 ++++++- src/plugins/expressions/public/types/index.ts | 1 + .../public/__stories__/render.tsx | 3 ++- .../public/embeddable/visualize_embeddable.ts | 1 + .../functions/external/saved_lens.ts | 1 - .../renderers/__stories__/render.tsx | 3 ++- .../plugins/canvas/public/lib/create_handlers.ts | 3 ++- .../markdown_editor/plugins/lens/processor.tsx | 2 +- .../components/table_basic.test.tsx | 16 ++++++++-------- .../lens/public/embeddable/embeddable.test.tsx | 10 ++++++++-- .../lens/public/embeddable/embeddable.tsx | 1 + .../public/embeddable/expression_wrapper.tsx | 3 +++ .../lens/public/pie_visualization/expression.tsx | 1 + .../pie_visualization/render_function.test.tsx | 6 +++--- .../public/pie_visualization/render_function.tsx | 5 ++--- .../public/xy_visualization/expression.test.tsx | 10 +++++----- .../lens/public/xy_visualization/expression.tsx | 8 +++++--- 21 files changed, 62 insertions(+), 33 deletions(-) diff --git a/src/plugins/embeddable/common/types.ts b/src/plugins/embeddable/common/types.ts index 22d8672e59a3..430b59bc6224 100644 --- a/src/plugins/embeddable/common/types.ts +++ b/src/plugins/embeddable/common/types.ts @@ -12,6 +12,7 @@ import { PersistableStateService } from '../../kibana_utils/common'; export enum ViewMode { EDIT = 'edit', + PREVIEW = 'preview', VIEW = 'view', } diff --git a/src/plugins/expressions/common/expression_renderers/types.ts b/src/plugins/expressions/common/expression_renderers/types.ts index 239cff6143ae..8547c1a1bec9 100644 --- a/src/plugins/expressions/common/expression_renderers/types.ts +++ b/src/plugins/expressions/common/expression_renderers/types.ts @@ -53,12 +53,11 @@ export type AnyExpressionRenderDefinition = ExpressionRenderDefinition; * This value can be set from a consumer embedding an expression renderer and is accessible * from within the active render function as part of the handlers. * The following modes are supported: - * * display (default): The chart is rendered in a container with the main purpose of viewing the chart (e.g. in a container like dashboard or canvas) + * * view (default): The chart is rendered in a container with the main purpose of viewing the chart (e.g. in a container like dashboard or canvas) * * preview: The chart is rendered in very restricted space (below 100px width and height) and should only show a rough outline * * edit: The chart is rendered within an editor and configuration elements within the chart should be displayed - * * noInteractivity: The chart is rendered in a non-interactive environment and should not provide any affordances for interaction like brushing */ -export type RenderMode = 'noInteractivity' | 'edit' | 'preview' | 'display'; +export type RenderMode = 'edit' | 'preview' | 'view'; export interface IInterpreterRenderHandlers { /** @@ -71,6 +70,12 @@ export interface IInterpreterRenderHandlers { event: (event: any) => void; hasCompatibleActions?: (event: any) => Promise; getRenderMode: () => RenderMode; + + /** + * The chart is rendered in a non-interactive environment and should not provide any affordances for interaction like brushing. + */ + isInteractive: () => boolean; + isSyncColorsEnabled: () => boolean; /** * This uiState interface is actually `PersistedState` from the visualizations plugin, diff --git a/src/plugins/expressions/public/loader.ts b/src/plugins/expressions/public/loader.ts index d0f60eb29406..3ab7473d8d73 100644 --- a/src/plugins/expressions/public/loader.ts +++ b/src/plugins/expressions/public/loader.ts @@ -53,6 +53,7 @@ export class ExpressionLoader { ); this.renderHandler = new ExpressionRenderHandler(element, { + interactive: params?.interactive, onRenderError: params && params.onRenderError, renderMode: params?.renderMode, syncColors: params?.syncColors, diff --git a/src/plugins/expressions/public/react_expression_renderer.tsx b/src/plugins/expressions/public/react_expression_renderer.tsx index 719af1b39f89..2640be16eae4 100644 --- a/src/plugins/expressions/public/react_expression_renderer.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.tsx @@ -171,6 +171,7 @@ export const ReactExpressionRenderer = ({ }, [ hasCustomRenderErrorHandler, onEvent, + expressionLoaderOptions.interactive, expressionLoaderOptions.renderMode, expressionLoaderOptions.syncColors, ]); diff --git a/src/plugins/expressions/public/render.ts b/src/plugins/expressions/public/render.ts index e7a00867c100..e9a65d1e8f12 100644 --- a/src/plugins/expressions/public/render.ts +++ b/src/plugins/expressions/public/render.ts @@ -21,6 +21,7 @@ export interface ExpressionRenderHandlerParams { onRenderError?: RenderErrorHandlerFnType; renderMode?: RenderMode; syncColors?: boolean; + interactive?: boolean; hasCompatibleActions?: (event: ExpressionRendererEvent) => Promise; } @@ -54,6 +55,7 @@ export class ExpressionRenderHandler { onRenderError, renderMode, syncColors, + interactive, hasCompatibleActions = async () => false, }: ExpressionRenderHandlerParams = {} ) { @@ -90,11 +92,14 @@ export class ExpressionRenderHandler { this.eventsSubject.next(data); }, getRenderMode: () => { - return renderMode || 'display'; + return renderMode || 'view'; }, isSyncColorsEnabled: () => { return syncColors || false; }, + isInteractive: () => { + return interactive ?? true; + }, hasCompatibleActions, }; } diff --git a/src/plugins/expressions/public/types/index.ts b/src/plugins/expressions/public/types/index.ts index 5a2198bb4f2e..172f322f8892 100644 --- a/src/plugins/expressions/public/types/index.ts +++ b/src/plugins/expressions/public/types/index.ts @@ -44,6 +44,7 @@ export interface IExpressionLoaderParams { customRenderers?: []; uiState?: unknown; inspectorAdapters?: Adapters; + interactive?: boolean; onRenderError?: RenderErrorHandlerFnType; searchSessionId?: string; renderMode?: RenderMode; diff --git a/src/plugins/presentation_util/public/__stories__/render.tsx b/src/plugins/presentation_util/public/__stories__/render.tsx index 2588d2e3294a..76725d08956b 100644 --- a/src/plugins/presentation_util/public/__stories__/render.tsx +++ b/src/plugins/presentation_util/public/__stories__/render.tsx @@ -11,8 +11,9 @@ import React, { useRef, useEffect } from 'react'; import { ExpressionRenderDefinition, IInterpreterRenderHandlers } from 'src/plugins/expressions'; export const defaultHandlers: IInterpreterRenderHandlers = { - getRenderMode: () => 'display', + getRenderMode: () => 'view', isSyncColorsEnabled: () => false, + isInteractive: () => true, done: action('done'), onDestroy: action('onDestroy'), reload: action('reload'), diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index b71542a8beee..78230a896196 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -402,6 +402,7 @@ export class VisualizeEmbeddable searchSessionId: this.input.searchSessionId, syncColors: this.input.syncColors, uiState: this.vis.uiState, + interactive: !this.input.disableTriggers, inspectorAdapters: this.inspectorAdapters, executionContext: context, }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts index bd844dd3335e..082a69a874ca 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts @@ -85,7 +85,6 @@ export function savedLens(): ExpressionFunctionDefinition< title: args.title === null ? undefined : args.title, disableTriggers: true, palette: args.palette, - renderMode: 'noInteractivity', }, embeddableType: EmbeddableTypes.lens, generatedAt: Date.now(), diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx index d87c24b1b7e8..643d7cdedc50 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx @@ -13,8 +13,9 @@ export const defaultHandlers: RendererHandlers = { destroy: () => action('destroy'), getElementId: () => 'element-id', getFilter: () => 'filter', - getRenderMode: () => 'display', + getRenderMode: () => 'view', isSyncColorsEnabled: () => false, + isInteractive: () => true, onComplete: (fn) => undefined, onEmbeddableDestroyed: action('onEmbeddableDestroyed'), onEmbeddableInputChange: action('onEmbeddableInputChange'), diff --git a/x-pack/plugins/canvas/public/lib/create_handlers.ts b/x-pack/plugins/canvas/public/lib/create_handlers.ts index dfc4387bcbf9..3734b1bf5305 100644 --- a/x-pack/plugins/canvas/public/lib/create_handlers.ts +++ b/x-pack/plugins/canvas/public/lib/create_handlers.ts @@ -26,8 +26,9 @@ export const createBaseHandlers = (): IInterpreterRenderHandlers => ({ update() {}, event() {}, onDestroy() {}, - getRenderMode: () => 'display', + getRenderMode: () => 'view', isSyncColorsEnabled: () => false, + isInteractive: () => true, }); export const createHandlers = (baseHandlers = createBaseHandlers()): RendererHandlers => ({ diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/processor.tsx b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/processor.tsx index 18315b1611c5..d0e816a06c7d 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/processor.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/processor.tsx @@ -48,7 +48,7 @@ const LensMarkDownRendererComponent: React.FC = ({ style={{ height: LENS_VISUALIZATION_HEIGHT }} timeRange={timeRange} attributes={attributes} - renderMode="display" + renderMode="view" /> diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx index a0d137b90e84..c156b870e7aa 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx @@ -151,7 +151,7 @@ describe('DatatableComponent', () => { dispatchEvent={onDispatchEvent} getType={jest.fn()} rowHasRowClickTriggerActions={[false, false, false]} - renderMode="display" + renderMode="view" paletteService={chartPluginMock.createPaletteRegistry()} uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} /> @@ -427,7 +427,7 @@ describe('DatatableComponent', () => { formatFactory={() => ({ convert: (x) => x } as IFieldFormat)} dispatchEvent={onDispatchEvent} getType={jest.fn()} - renderMode="display" + renderMode="view" paletteService={chartPluginMock.createPaletteRegistry()} uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} /> @@ -457,7 +457,7 @@ describe('DatatableComponent', () => { formatFactory={() => ({ convert: (x) => x } as IFieldFormat)} dispatchEvent={onDispatchEvent} getType={jest.fn()} - renderMode="display" + renderMode="view" paletteService={chartPluginMock.createPaletteRegistry()} uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} /> @@ -485,7 +485,7 @@ describe('DatatableComponent', () => { formatFactory={() => ({ convert: (x) => x } as IFieldFormat)} dispatchEvent={onDispatchEvent} getType={jest.fn()} - renderMode="display" + renderMode="view" paletteService={chartPluginMock.createPaletteRegistry()} uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} /> @@ -546,7 +546,7 @@ describe('DatatableComponent', () => { formatFactory={() => ({ convert: (x) => x } as IFieldFormat)} dispatchEvent={onDispatchEvent} getType={jest.fn()} - renderMode="display" + renderMode="view" paletteService={chartPluginMock.createPaletteRegistry()} uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} /> @@ -581,7 +581,7 @@ describe('DatatableComponent', () => { formatFactory={() => ({ convert: (x) => x } as IFieldFormat)} dispatchEvent={onDispatchEvent} getType={jest.fn()} - renderMode="display" + renderMode="view" paletteService={chartPluginMock.createPaletteRegistry()} uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} /> @@ -616,7 +616,7 @@ describe('DatatableComponent', () => { formatFactory={() => ({ convert: (x) => x } as IFieldFormat)} dispatchEvent={onDispatchEvent} getType={jest.fn()} - renderMode="display" + renderMode="view" paletteService={chartPluginMock.createPaletteRegistry()} uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} /> @@ -650,7 +650,7 @@ describe('DatatableComponent', () => { formatFactory={() => ({ convert: (x) => x } as IFieldFormat)} dispatchEvent={onDispatchEvent} getType={jest.fn()} - renderMode="display" + renderMode="view" paletteService={chartPluginMock.createPaletteRegistry()} uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} /> diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx index a0831e8a73b5..be20118ba294 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx @@ -566,7 +566,8 @@ describe('embeddable', () => { timeRange, query, filters, - renderMode: 'noInteractivity', + renderMode: 'view', + disableTriggers: true, } as LensEmbeddableInput; const embeddable = new Embeddable( @@ -599,7 +600,12 @@ describe('embeddable', () => { await embeddable.initializeSavedVis(input); embeddable.render(mountpoint); - expect(expressionRenderer.mock.calls[0][0].renderMode).toEqual('noInteractivity'); + expect(expressionRenderer.mock.calls[0][0]).toEqual( + expect.objectContaining({ + interactive: false, + renderMode: 'view', + }) + ); }); it('should merge external context with query and filters of the saved object', async () => { diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 7e87dd3076fa..d10423c76686 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -361,6 +361,7 @@ export class Embeddable searchSessionId={this.externalSearchContext.searchSessionId} handleEvent={this.handleEvent} onData$={this.updateActiveData} + interactive={!input.disableTriggers} renderMode={input.renderMode} syncColors={input.syncColors} hasCompatibleActions={this.hasCompatibleActions} diff --git a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx index 1116b4a0d396..c827fe74cc52 100644 --- a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx @@ -38,6 +38,7 @@ export interface ExpressionWrapperProps { expression: string | null; errors: ErrorMessage[] | undefined; variables?: Record; + interactive?: boolean; searchContext: ExecutionContextSearch; searchSessionId?: string; handleEvent: (event: ExpressionRendererEvent) => void; @@ -113,6 +114,7 @@ export function ExpressionWrapper({ searchContext, variables, handleEvent, + interactive, searchSessionId, onData$, renderMode, @@ -137,6 +139,7 @@ export function ExpressionWrapper({ padding="s" variables={variables} expression={expression} + interactive={interactive} searchContext={searchContext} searchSessionId={searchSessionId} onData$={onData$} diff --git a/x-pack/plugins/lens/public/pie_visualization/expression.tsx b/x-pack/plugins/lens/public/pie_visualization/expression.tsx index c1b9f4c799e6..c947d50d5b91 100644 --- a/x-pack/plugins/lens/public/pie_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/expression.tsx @@ -44,6 +44,7 @@ export const getPieRenderer = (dependencies: { {...config} formatFactory={dependencies.formatFactory} chartsThemeService={dependencies.chartsThemeService} + interactive={handlers.isInteractive()} paletteService={dependencies.paletteService} onClickValue={onClickValue} renderMode={handlers.getRenderMode()} diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx index 93f16c49061e..209d7ff652ea 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx @@ -77,7 +77,7 @@ describe('PieVisualization component', () => { onClickValue: jest.fn(), chartsThemeService, paletteService: chartPluginMock.createPaletteRegistry(), - renderMode: 'display' as const, + renderMode: 'view' as const, syncColors: false, }; } @@ -302,10 +302,10 @@ describe('PieVisualization component', () => { `); }); - test('does not set click listener on noInteractivity render mode', () => { + test('does not set click listener on non-interactive mode', () => { const defaultArgs = getDefaultArgs(); const component = shallow( - + ); expect(component.find(Settings).first().prop('onElementClick')).toBeUndefined(); }); diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 41b96ff4324a..a0a845dc9600 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -55,6 +55,7 @@ export function PieComponent( props: PieExpressionProps & { formatFactory: FormatFactory; chartsThemeService: ChartsPluginSetup['theme']; + interactive?: boolean; paletteService: PaletteRegistry; onClickValue: (data: LensFilterEvent['data']) => void; renderMode: RenderMode; @@ -289,9 +290,7 @@ export function PieComponent( } legendPosition={legendPosition || Position.Right} legendMaxDepth={nestedLegend ? undefined : 1 /* Color is based only on first layer */} - onElementClick={ - props.renderMode !== 'noInteractivity' ? onElementClickHandler : undefined - } + onElementClick={props.interactive ?? true ? onElementClickHandler : undefined} legendAction={getLegendAction(firstTable, onClickValue)} theme={{ ...chartTheme, diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx index a41ad59ebee9..3994aadd9a98 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx @@ -480,7 +480,7 @@ describe('xy_expression', () => { defaultProps = { formatFactory: getFormatSpy, timeZone: 'UTC', - renderMode: 'display', + renderMode: 'view', chartsThemeService, chartsActiveCursorService, paletteService, @@ -1064,11 +1064,11 @@ describe('xy_expression', () => { }); }); - test('onBrushEnd is not set on noInteractivity mode', () => { + test('onBrushEnd is not set on non-interactive mode', () => { const { args, data } = sampleArgs(); const wrapper = mountWithIntl( - + ); expect(wrapper.find(Settings).first().prop('onBrushEnd')).toBeUndefined(); @@ -1334,11 +1334,11 @@ describe('xy_expression', () => { }); }); - test('onElementClick is not triggering event on noInteractivity mode', () => { + test('onElementClick is not triggering event on non-interactive mode', () => { const { args, data } = sampleArgs(); const wrapper = mountWithIntl( - + ); expect(wrapper.find(Settings).first().prop('onElementClick')).toBeUndefined(); diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index f8dff65969d5..75d14d9b48ee 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -93,6 +93,7 @@ export type XYChartRenderProps = XYChartProps & { formatFactory: FormatFactory; timeZone: string; minInterval: number | undefined; + interactive?: boolean; onClickValue: (data: LensFilterEvent['data']) => void; onSelectRange: (data: LensBrushEvent['data']) => void; renderMode: RenderMode; @@ -160,6 +161,7 @@ export const getXyChartRenderer = (dependencies: { paletteService={dependencies.paletteService} timeZone={dependencies.timeZone} minInterval={calculateMinInterval(config)} + interactive={handlers.isInteractive()} onClickValue={onClickValue} onSelectRange={onSelectRange} renderMode={handlers.getRenderMode()} @@ -233,7 +235,7 @@ export function XYChart({ minInterval, onClickValue, onSelectRange, - renderMode, + interactive = true, syncColors, }: XYChartRenderProps) { const { @@ -528,8 +530,8 @@ export function XYChart({ }} rotation={shouldRotate ? 90 : 0} xDomain={xDomain} - onBrushEnd={renderMode !== 'noInteractivity' ? brushHandler : undefined} - onElementClick={renderMode !== 'noInteractivity' ? clickHandler : undefined} + onBrushEnd={interactive ? brushHandler : undefined} + onElementClick={interactive ? clickHandler : undefined} legendAction={getLegendAction( filteredLayers, data.tables, From df22a8e19744e83c478791a7a4ad6828d081db1d Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Fri, 3 Sep 2021 19:12:42 +0200 Subject: [PATCH 33/47] Change Graph ownership (#111157) --- .github/CODEOWNERS | 3 ++- x-pack/plugins/graph/kibana.json | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9a09ea1de694..0949c5d74243 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -16,10 +16,11 @@ /src/plugins/discover/ @elastic/kibana-data-discovery /x-pack/plugins/discover_enhanced/ @elastic/kibana-data-discovery /test/functional/apps/discover/ @elastic/kibana-data-discovery +/x-pack/plugins/graph/ @elastic/kibana-data-discovery +/x-pack/test/functional/apps/graph @elastic/kibana-data-discovery # Vis Editors /x-pack/plugins/lens/ @elastic/kibana-vis-editors -/x-pack/plugins/graph/ @elastic/kibana-vis-editors /src/plugins/advanced_settings/ @elastic/kibana-vis-editors /src/plugins/charts/ @elastic/kibana-vis-editors /src/plugins/management/ @elastic/kibana-vis-editors diff --git a/x-pack/plugins/graph/kibana.json b/x-pack/plugins/graph/kibana.json index 463893e2425a..cc732e67995b 100644 --- a/x-pack/plugins/graph/kibana.json +++ b/x-pack/plugins/graph/kibana.json @@ -9,7 +9,7 @@ "configPath": ["xpack", "graph"], "requiredBundles": ["kibanaUtils", "kibanaReact", "home"], "owner": { - "name": "Vis Editors", - "githubTeam": "kibana-vis-editors" + "name": "Data Discovery", + "githubTeam": "kibana-data-discovery" } } From 167b876a8adb27104d9fd09cd8eea4413a137bc4 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Fri, 3 Sep 2021 12:12:48 -0500 Subject: [PATCH 34/47] [ML] Fix issue with AD data points not showing up because missing indices (#110899) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../application/services/anomaly_explorer_charts_service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts b/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts index 7a81a7ecb1e3..32c6aeb6b755 100644 --- a/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts +++ b/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts @@ -590,7 +590,7 @@ export class AnomalyExplorerChartsService { return mlResultsService .getMetricData( Array.isArray(config.datafeedConfig.indices) - ? config.datafeedConfig.indices[0] + ? config.datafeedConfig.indices.join() : config.datafeedConfig.indices, entityFields, datafeedQuery, @@ -777,7 +777,7 @@ export class AnomalyExplorerChartsService { return mlResultsService .getEventDistributionData( Array.isArray(config.datafeedConfig.indices) - ? config.datafeedConfig.indices[0] + ? config.datafeedConfig.indices.join() : config.datafeedConfig.indices, splitField, filterField, From a9d73311e01b9d1cabea033608b6268c2bf501a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Fri, 3 Sep 2021 20:06:36 +0100 Subject: [PATCH 35/47] [Docs] Logging settings: add explanation of `appenders`, `loggers` and `root` (#111013) --- docs/settings/logging-settings.asciidoc | 28 ++++++++++++++++++------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/docs/settings/logging-settings.asciidoc b/docs/settings/logging-settings.asciidoc index aa38d54305ee..77f3bd90a911 100644 --- a/docs/settings/logging-settings.asciidoc +++ b/docs/settings/logging-settings.asciidoc @@ -4,6 +4,16 @@ Logging settings ++++ +{kib} relies on three high-level entities to set the logging service: appenders, loggers, and root. + +- Appenders define where log messages are displayed (stdout or console) and their layout (`pattern` or `json`). They also allow you to specify if you want the logs stored and, if so, where (file on the disk). +- Loggers define what logging settings, such as the level of verbosity and the appenders, to apply to a particular context. Each log entry context provides information about the service or plugin that emits it and any of its sub-parts, for example, `metrics.ops` or `elasticsearch.query`. +- Root is a logger that applies to all the log entries in {kib}. + +Refer to the <> for common configuration use cases. To learn more about possible configuration values, go to {kibana-ref}/logging-service.html[{kib}'s Logging service]. + +[[log-settings-compatibility]] +==== Backwards compatibility Compatibility with the legacy logging system is assured until the end of the `v7` version. All log messages handled by `root` context (default) are forwarded to the legacy logging service. The logging configuration is validated against the predefined schema and if there are @@ -12,10 +22,12 @@ any issues with it, {kib} will fail to start with the detailed error message. NOTE: When you switch to the new logging configuration, you will start seeing duplicate log entries in both formats. These will be removed when the `default` appender is no longer required. +[[log-settings-examples]] +==== Examples Here are some configuration examples for the most common logging use cases: [[log-to-file-example]] -==== Log to a file +===== Log to a file Log the default log format to a file instead of to stdout (the default). @@ -33,10 +45,10 @@ logging: ---- [[log-in-json-ECS-example]] -==== Log in json format +===== Log in JSON format -Log the default log format to json layout instead of pattern (the default). -With `json` layout log messages will be formatted as JSON strings in https://www.elastic.co/guide/en/ecs/current/ecs-reference.html[ECS format] that includes a timestamp, log level, logger, message text and any other metadata that may be associated with the log message itself +Log the default log format to JSON layout instead of pattern (the default). +With `json` layout, log messages will be formatted as JSON strings in https://www.elastic.co/guide/en/ecs/current/ecs-reference.html[ECS format] that includes a timestamp, log level, logger, message text and any other metadata that may be associated with the log message itself. [source,yaml] ---- @@ -51,7 +63,7 @@ logging: ---- [[log-with-meta-to-stdout]] -==== Log with meta to stdout +===== Log with meta to stdout Include `%meta` in your pattern layout: @@ -69,7 +81,7 @@ logging: ---- [[log-elasticsearch-queries]] -==== Log {es} queries +===== Log {es} queries [source,yaml] -- @@ -89,7 +101,7 @@ logging: -- [[change-overall-log-level]] -==== Change overall log level. +===== Change overall log level [source,yaml] ---- @@ -99,7 +111,7 @@ logging: ---- [[customize-specific-log-records]] -==== Customize specific log records +===== Customize specific log records Here is a detailed configuration example that can be used to configure _loggers_, _appenders_ and _layouts_: [source,yaml] From 9b20c8086720b48912377bea628296b209bc8b80 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Fri, 3 Sep 2021 15:17:06 -0400 Subject: [PATCH 36/47] Fix ML alert not allowed in Uptime app. (#111180) --- x-pack/plugins/uptime/server/kibana.index.ts | 30 ++++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts index c303c7827333..3b1001daf051 100644 --- a/x-pack/plugins/uptime/server/kibana.index.ts +++ b/x-pack/plugins/uptime/server/kibana.index.ts @@ -46,7 +46,11 @@ export const initServerWithKibana = ( management: { insightsAndAlerting: ['triggersActions'], }, - alerting: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'], + alerting: [ + 'xpack.uptime.alerts.tls', + 'xpack.uptime.alerts.monitorStatus', + 'xpack.uptime.alerts.durationAnomaly', + ], privileges: { all: { app: ['uptime', 'kibana'], @@ -58,10 +62,18 @@ export const initServerWithKibana = ( }, alerting: { rule: { - all: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'], + all: [ + 'xpack.uptime.alerts.tls', + 'xpack.uptime.alerts.monitorStatus', + 'xpack.uptime.alerts.durationAnomaly', + ], }, alert: { - all: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'], + all: [ + 'xpack.uptime.alerts.tls', + 'xpack.uptime.alerts.monitorStatus', + 'xpack.uptime.alerts.durationAnomaly', + ], }, }, management: { @@ -79,10 +91,18 @@ export const initServerWithKibana = ( }, alerting: { rule: { - read: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'], + read: [ + 'xpack.uptime.alerts.tls', + 'xpack.uptime.alerts.monitorStatus', + 'xpack.uptime.alerts.durationAnomaly', + ], }, alert: { - read: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'], + read: [ + 'xpack.uptime.alerts.tls', + 'xpack.uptime.alerts.monitorStatus', + 'xpack.uptime.alerts.durationAnomaly', + ], }, }, management: { From c6aa4f625cc804fde7b78d86f552a8ac0c2f55d7 Mon Sep 17 00:00:00 2001 From: Dmitry Shevchenko Date: Fri, 3 Sep 2021 21:38:45 +0200 Subject: [PATCH 37/47] Fix exceptions page table pagination (#111000) --- .../src/typescript_types/index.ts | 2 +- .../src/use_exception_lists/index.ts | 123 +++++++++--------- .../found_exception_list_schema.mock.ts | 2 +- .../hooks/use_exception_lists.test.ts | 82 ++++++------ .../rules/all/exceptions/columns.tsx | 3 +- .../rules/all/exceptions/exceptions_table.tsx | 46 +++++-- .../rules/all/exceptions/types.ts | 13 ++ 7 files changed, 156 insertions(+), 115 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/types.ts diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts index 1909bcb1bcc2..31f763101c25 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts @@ -40,7 +40,7 @@ export interface UseExceptionListsProps { http: HttpStart; namespaceTypes: NamespaceType[]; notifications: NotificationsStart; - pagination?: Pagination; + initialPagination?: Pagination; showTrustedApps: boolean; showEventFilters: boolean; } diff --git a/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts b/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts index 0bd4c6c70566..722a7918c412 100644 --- a/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts +++ b/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { ExceptionListSchema, UseExceptionListsProps, @@ -17,7 +17,19 @@ import { fetchExceptionLists } from '@kbn/securitysolution-list-api'; import { getFilters } from '@kbn/securitysolution-list-utils'; export type Func = () => void; -export type ReturnExceptionLists = [boolean, ExceptionListSchema[], Pagination, Func | null]; +export type ReturnExceptionLists = [ + loading: boolean, + exceptionLists: ExceptionListSchema[], + pagination: Pagination, + setPagination: React.Dispatch>, + fetchLists: Func | null +]; + +const DEFAULT_PAGINATION = { + page: 1, + perPage: 20, + total: 0, +}; /** * Hook for fetching ExceptionLists @@ -29,17 +41,13 @@ export type ReturnExceptionLists = [boolean, ExceptionListSchema[], Pagination, * @param notifications kibana service for displaying toasters * @param showTrustedApps boolean - include/exclude trusted app lists * @param showEventFilters boolean - include/exclude event filters lists - * @param pagination + * @param initialPagination * */ export const useExceptionLists = ({ errorMessage, http, - pagination = { - page: 1, - perPage: 20, - total: 0, - }, + initialPagination = DEFAULT_PAGINATION, filterOptions = {}, namespaceTypes, notifications, @@ -47,9 +55,9 @@ export const useExceptionLists = ({ showEventFilters = false, }: UseExceptionListsProps): ReturnExceptionLists => { const [exceptionLists, setExceptionLists] = useState([]); - const [paginationInfo, setPagination] = useState(pagination); + const [pagination, setPagination] = useState(initialPagination); const [loading, setLoading] = useState(true); - const fetchExceptionListsRef = useRef(null); + const abortCtrlRef = useRef(); const namespaceTypesAsString = useMemo(() => namespaceTypes.join(','), [namespaceTypes]); const filters = useMemo( @@ -58,66 +66,57 @@ export const useExceptionLists = ({ [namespaceTypes, filterOptions, showTrustedApps, showEventFilters] ); - useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); + const fetchData = useCallback(async (): Promise => { + try { + setLoading(true); - const fetchData = async (): Promise => { - try { - setLoading(true); + abortCtrlRef.current = new AbortController(); - const { page, per_page: perPage, total, data } = await fetchExceptionLists({ - filters, - http, - namespaceTypes: namespaceTypesAsString, - pagination: { - page: pagination.page, - perPage: pagination.perPage, - }, - signal: abortCtrl.signal, - }); + const { page, per_page: perPage, total, data } = await fetchExceptionLists({ + filters, + http, + namespaceTypes: namespaceTypesAsString, + pagination: { + page: pagination.page, + perPage: pagination.perPage, + }, + signal: abortCtrlRef.current.signal, + }); - if (isSubscribed) { - setPagination({ - page, - perPage, - total, - }); - setExceptionLists(data); - setLoading(false); - } - } catch (error) { - if (isSubscribed) { - notifications.toasts.addError(error, { - title: errorMessage, - }); - setExceptionLists([]); - setPagination({ - page: 1, - perPage: 20, - total: 0, - }); - setLoading(false); - } + setPagination({ + page, + perPage, + total, + }); + setExceptionLists(data); + setLoading(false); + } catch (error) { + if (error.name !== 'AbortError') { + notifications.toasts.addError(error, { + title: errorMessage, + }); + setExceptionLists([]); + setPagination(DEFAULT_PAGINATION); + setLoading(false); } - }; - - fetchData(); - - fetchExceptionListsRef.current = fetchData; - return (): void => { - isSubscribed = false; - abortCtrl.abort(); - }; + } }, [ errorMessage, - notifications, - pagination.page, - pagination.perPage, filters, - namespaceTypesAsString, http, + namespaceTypesAsString, + notifications.toasts, + pagination.page, + pagination.perPage, ]); - return [loading, exceptionLists, paginationInfo, fetchExceptionListsRef.current]; + useEffect(() => { + fetchData(); + + return (): void => { + abortCtrlRef.current?.abort(); + }; + }, [fetchData]); + + return [loading, exceptionLists, pagination, setPagination, fetchData]; }; diff --git a/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.mock.ts index e3611120348f..e5e41b5fe4a8 100644 --- a/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.mock.ts @@ -12,6 +12,6 @@ import { getExceptionListSchemaMock } from './exception_list_schema.mock'; export const getFoundExceptionListSchemaMock = (): FoundExceptionListSchema => ({ data: [getExceptionListSchemaMock()], page: 1, - per_page: 1, + per_page: 20, total: 1, }); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts index 4987de321c55..810fcaa15494 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts @@ -41,13 +41,13 @@ describe('useExceptionLists', () => { errorMessage: 'Uh oh', filterOptions: {}, http: mockKibanaHttpService, - namespaceTypes: ['single', 'agnostic'], - notifications: mockKibanaNotificationsService, - pagination: { + initialPagination: { page: 1, perPage: 20, total: 0, }, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, showEventFilters: false, showTrustedApps: false, }) @@ -62,7 +62,8 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, - null, + expect.any(Function), + expect.any(Function), ]); }); }); @@ -77,13 +78,13 @@ describe('useExceptionLists', () => { errorMessage: 'Uh oh', filterOptions: {}, http: mockKibanaHttpService, - namespaceTypes: ['single', 'agnostic'], - notifications: mockKibanaNotificationsService, - pagination: { + initialPagination: { page: 1, perPage: 20, total: 0, }, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, showEventFilters: false, showTrustedApps: false, }) @@ -100,10 +101,11 @@ describe('useExceptionLists', () => { expectedListItemsResult, { page: 1, - perPage: 1, + perPage: 20, total: 1, }, - result.current[3], + expect.any(Function), + expect.any(Function), ]); }); }); @@ -117,13 +119,13 @@ describe('useExceptionLists', () => { errorMessage: 'Uh oh', filterOptions: {}, http: mockKibanaHttpService, - namespaceTypes: ['single', 'agnostic'], - notifications: mockKibanaNotificationsService, - pagination: { + initialPagination: { page: 1, perPage: 20, total: 0, }, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, showEventFilters: false, showTrustedApps: true, }) @@ -153,13 +155,13 @@ describe('useExceptionLists', () => { errorMessage: 'Uh oh', filterOptions: {}, http: mockKibanaHttpService, - namespaceTypes: ['single', 'agnostic'], - notifications: mockKibanaNotificationsService, - pagination: { + initialPagination: { page: 1, perPage: 20, total: 0, }, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, showEventFilters: false, showTrustedApps: false, }) @@ -189,13 +191,13 @@ describe('useExceptionLists', () => { errorMessage: 'Uh oh', filterOptions: {}, http: mockKibanaHttpService, - namespaceTypes: ['single', 'agnostic'], - notifications: mockKibanaNotificationsService, - pagination: { + initialPagination: { page: 1, perPage: 20, total: 0, }, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, showEventFilters: true, showTrustedApps: false, }) @@ -225,13 +227,13 @@ describe('useExceptionLists', () => { errorMessage: 'Uh oh', filterOptions: {}, http: mockKibanaHttpService, - namespaceTypes: ['single', 'agnostic'], - notifications: mockKibanaNotificationsService, - pagination: { + initialPagination: { page: 1, perPage: 20, total: 0, }, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, showEventFilters: false, showTrustedApps: false, }) @@ -264,13 +266,13 @@ describe('useExceptionLists', () => { name: 'Sample Endpoint', }, http: mockKibanaHttpService, - namespaceTypes: ['single', 'agnostic'], - notifications: mockKibanaNotificationsService, - pagination: { + initialPagination: { page: 1, perPage: 20, total: 0, }, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, showEventFilters: false, showTrustedApps: false, }) @@ -302,9 +304,9 @@ describe('useExceptionLists', () => { errorMessage, filterOptions, http, + initialPagination, namespaceTypes, notifications, - pagination, showEventFilters, showTrustedApps, }) => @@ -312,9 +314,9 @@ describe('useExceptionLists', () => { errorMessage, filterOptions, http, + initialPagination, namespaceTypes, notifications, - pagination, showEventFilters, showTrustedApps, }), @@ -323,13 +325,13 @@ describe('useExceptionLists', () => { errorMessage: 'Uh oh', filterOptions: {}, http: mockKibanaHttpService, - namespaceTypes: ['single'], - notifications: mockKibanaNotificationsService, - pagination: { + initialPagination: { page: 1, perPage: 20, total: 0, }, + namespaceTypes: ['single'], + notifications: mockKibanaNotificationsService, showEventFilters: false, showTrustedApps: false, }, @@ -344,13 +346,13 @@ describe('useExceptionLists', () => { errorMessage: 'Uh oh', filterOptions: {}, http: mockKibanaHttpService, - namespaceTypes: ['single', 'agnostic'], - notifications: mockKibanaNotificationsService, - pagination: { + initialPagination: { page: 1, perPage: 20, total: 0, }, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, showEventFilters: false, showTrustedApps: false, }); @@ -372,13 +374,13 @@ describe('useExceptionLists', () => { errorMessage: 'Uh oh', filterOptions: {}, http: mockKibanaHttpService, - namespaceTypes: ['single', 'agnostic'], - notifications: mockKibanaNotificationsService, - pagination: { + initialPagination: { page: 1, perPage: 20, total: 0, }, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, showEventFilters: false, showTrustedApps: false, }) @@ -390,8 +392,8 @@ describe('useExceptionLists', () => { expect(typeof result.current[3]).toEqual('function'); - if (result.current[3] != null) { - result.current[3](); + if (result.current[4] != null) { + result.current[4](); } // NOTE: Only need one call here because hook already initilaized await waitForNextUpdate(); @@ -411,13 +413,13 @@ describe('useExceptionLists', () => { errorMessage: 'Uh oh', filterOptions: {}, http: mockKibanaHttpService, - namespaceTypes: ['single', 'agnostic'], - notifications: mockKibanaNotificationsService, - pagination: { + initialPagination: { page: 1, perPage: 20, total: 0, }, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, showEventFilters: false, showTrustedApps: false, }) diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx index 582ca0252604..1ef3c3d3c541 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx @@ -15,8 +15,9 @@ import { FormatUrl } from '../../../../../../common/components/link_to'; import * as i18n from './translations'; import { ExceptionListInfo } from './use_all_exception_lists'; import { ExceptionOverflowDisplay } from './exceptions_overflow_display'; +import { ExceptionsTableItem } from './types'; -export type AllExceptionListsColumns = EuiBasicTableColumn; +export type AllExceptionListsColumns = EuiBasicTableColumn; export const getAllExceptionListsColumns = ( onExport: (arg: { id: string; listId: string; namespaceType: NamespaceType }) => () => void, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx index 206976e6c0c1..23bf634cb108 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx @@ -7,6 +7,7 @@ import React, { useMemo, useEffect, useCallback, useState } from 'react'; import { + CriteriaWithPagination, EuiBasicTable, EuiEmptyPrompt, EuiLoadingContent, @@ -37,6 +38,7 @@ import { SecurityPageName } from '../../../../../../../common/constants'; import { useUserData } from '../../../../../components/user_info'; import { userHasPermissions } from '../../helpers'; import { useListsConfig } from '../../../../../containers/detection_engine/lists/use_lists_config'; +import { ExceptionsTableItem } from './types'; export type Func = () => Promise; @@ -74,7 +76,13 @@ export const ExceptionListsTable = React.memo(() => { exceptionReferenceModalInitialState ); const [filters, setFilters] = useState(undefined); - const [loadingExceptions, exceptions, pagination, refreshExceptions] = useExceptionLists({ + const [ + loadingExceptions, + exceptions, + pagination, + setPagination, + refreshExceptions, + ] = useExceptionLists({ errorMessage: i18n.ERROR_EXCEPTION_LISTS, filterOptions: filters, http, @@ -125,7 +133,7 @@ export const ExceptionListsTable = React.memo(() => { try { setDeletingListIds((ids) => [...ids, id]); if (refreshExceptions != null) { - await refreshExceptions(); + refreshExceptions(); } if (exceptionsListsRef[id] != null && exceptionsListsRef[id].rules.length === 0) { @@ -153,7 +161,7 @@ export const ExceptionListsTable = React.memo(() => { } catch (error) { handleDeleteError(error); } finally { - setDeletingListIds((ids) => [...ids.filter((_id) => _id !== id)]); + setDeletingListIds((ids) => ids.filter((_id) => _id !== id)); } }, [ @@ -326,11 +334,27 @@ export const ExceptionListsTable = React.memo(() => { setExportDownload({}); }, []); - const tableItems = (exceptionListsWithRuleRefs ?? []).map((item) => ({ - ...item, - isDeleting: deletingListIds.includes(item.id), - isExporting: exportingListIds.includes(item.id), - })); + const tableItems = useMemo( + () => + (exceptionListsWithRuleRefs ?? []).map((item) => ({ + ...item, + isDeleting: deletingListIds.includes(item.id), + isExporting: exportingListIds.includes(item.id), + })), + [deletingListIds, exceptionListsWithRuleRefs, exportingListIds] + ); + + const handlePaginationChange = useCallback( + (criteria: CriteriaWithPagination) => { + const { index, size } = criteria.page; + setPagination((currentPagination) => ({ + ...currentPagination, + perPage: size, + page: index + 1, + })); + }, + [setPagination] + ); return ( <> @@ -367,14 +391,14 @@ export const ExceptionListsTable = React.memo(() => { numberSelectedItems={0} onRefresh={handleRefresh} /> - data-test-subj="exceptions-table" columns={exceptionsColumns} isSelectable={hasPermissions} itemId="id" items={tableItems} noItemsMessage={emptyPrompt} - onChange={() => {}} + onChange={handlePaginationChange} pagination={paginationMemo} /> @@ -400,3 +424,5 @@ export const ExceptionListsTable = React.memo(() => { ); }); + +ExceptionListsTable.displayName = 'ExceptionListsTable'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/types.ts new file mode 100644 index 000000000000..d7cbb924071f --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExceptionListInfo } from './use_all_exception_lists'; + +export interface ExceptionsTableItem extends ExceptionListInfo { + isDeleting: boolean; + isExporting: boolean; +} From 7c4e4f507beadd5e5cdd3db46410cb50ef4982bc Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 3 Sep 2021 21:17:46 +0100 Subject: [PATCH 38/47] chore(NA): replace babel config files by inline presets by default in the jsts_transpiler rule (#110620) * chore(NA): replace babel config files by inline presets by default in the js_ts_transpiler rule * chore(NA): update @kbn/ace build to exclude worker file * chore(NA): remove config file support Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/elastic-datemath/.babelrc | 3 --- packages/kbn-ace/.babelrc | 6 ----- packages/kbn-ace/BUILD.bazel | 2 ++ packages/kbn-alerts/.babelrc | 4 --- packages/kbn-alerts/.babelrc.browser | 4 --- packages/kbn-alerts/BUILD.bazel | 2 +- packages/kbn-analytics/.babelrc | 3 --- packages/kbn-analytics/.babelrc.browser | 3 --- packages/kbn-analytics/BUILD.bazel | 2 +- packages/kbn-apm-config-loader/.babelrc | 3 --- packages/kbn-apm-utils/.babelrc | 3 --- packages/kbn-babel-code-parser/.babelrc | 3 --- packages/kbn-cli-dev-mode/.babelrc | 3 --- packages/kbn-config-schema/.babelrc | 3 --- packages/kbn-config/.babelrc | 3 --- packages/kbn-crypto/.babelrc | 3 --- packages/kbn-dev-utils/.babelrc | 3 --- packages/kbn-docs-utils/.babelrc | 3 --- packages/kbn-es-archiver/.babelrc | 3 --- packages/kbn-es-query/.babelrc | 3 --- packages/kbn-es-query/.babelrc.browser | 3 --- packages/kbn-es-query/BUILD.bazel | 2 +- packages/kbn-es/.babelrc | 3 --- packages/kbn-field-types/.babelrc | 3 --- packages/kbn-i18n/.babelrc | 3 --- packages/kbn-i18n/.babelrc.browser | 3 --- packages/kbn-i18n/BUILD.bazel | 2 +- packages/kbn-interpreter/.babelrc | 3 --- packages/kbn-io-ts-utils/.babelrc | 3 --- packages/kbn-legacy-logging/.babelrc | 3 --- packages/kbn-logging/.babelrc | 3 --- packages/kbn-mapbox-gl/.babelrc | 3 --- packages/kbn-monaco/.babelrc | 3 --- packages/kbn-monaco/.babelrc.browser | 3 --- packages/kbn-monaco/BUILD.bazel | 2 +- packages/kbn-optimizer/.babelrc | 4 --- packages/kbn-plugin-generator/.babelrc | 3 --- packages/kbn-plugin-helpers/.babelrc | 3 --- packages/kbn-rule-data-utils/.babelrc | 3 --- .../.babelrc | 4 --- .../.babelrc.browser | 4 --- .../BUILD.bazel | 2 +- .../kbn-securitysolution-es-utils/.babelrc | 4 --- .../kbn-securitysolution-hook-utils/.babelrc | 4 --- .../.babelrc.browser | 4 --- .../BUILD.bazel | 2 +- .../.babelrc | 4 --- .../.babelrc.browser | 4 --- .../BUILD.bazel | 2 +- .../.babelrc | 4 --- .../.babelrc.browser | 4 --- .../BUILD.bazel | 2 +- .../kbn-securitysolution-io-ts-types/.babelrc | 4 --- .../.babelrc.browser | 4 --- .../BUILD.bazel | 2 +- .../kbn-securitysolution-io-ts-utils/.babelrc | 4 --- .../.babelrc.browser | 4 --- .../BUILD.bazel | 2 +- .../kbn-securitysolution-list-api/.babelrc | 4 --- .../.babelrc.browser | 4 --- .../kbn-securitysolution-list-api/BUILD.bazel | 2 +- .../.babelrc | 4 --- .../.babelrc.browser | 4 --- .../BUILD.bazel | 2 +- .../kbn-securitysolution-list-hooks/.babelrc | 4 --- .../.babelrc.browser | 4 --- .../BUILD.bazel | 2 +- .../kbn-securitysolution-list-utils/.babelrc | 4 --- .../.babelrc.browser | 4 --- .../BUILD.bazel | 2 +- packages/kbn-securitysolution-t-grid/.babelrc | 4 --- .../.babelrc.browser | 4 --- .../kbn-securitysolution-t-grid/BUILD.bazel | 2 +- packages/kbn-securitysolution-utils/.babelrc | 4 --- packages/kbn-server-http-tools/.babelrc | 3 --- packages/kbn-server-route-repository/.babelrc | 3 --- packages/kbn-std/.babelrc | 3 --- packages/kbn-storybook/.babelrc | 3 --- packages/kbn-telemetry-tools/.babelrc | 3 --- packages/kbn-test/.babelrc | 4 --- packages/kbn-tinymath/babel.config.js | 19 -------------- .../kbn-typed-react-router-config/.babelrc | 3 --- .../.babelrc.browser | 3 --- .../kbn-typed-react-router-config/BUILD.bazel | 2 +- packages/kbn-ui-shared-deps/.babelrc | 3 --- packages/kbn-utility-types/.babelrc | 3 --- packages/kbn-utils/.babelrc | 3 --- src/dev/bazel/jsts_transpiler.bzl | 26 ++++++++++++++----- 88 files changed, 39 insertions(+), 277 deletions(-) delete mode 100644 packages/elastic-datemath/.babelrc delete mode 100644 packages/kbn-ace/.babelrc delete mode 100644 packages/kbn-alerts/.babelrc delete mode 100644 packages/kbn-alerts/.babelrc.browser delete mode 100644 packages/kbn-analytics/.babelrc delete mode 100644 packages/kbn-analytics/.babelrc.browser delete mode 100644 packages/kbn-apm-config-loader/.babelrc delete mode 100644 packages/kbn-apm-utils/.babelrc delete mode 100644 packages/kbn-babel-code-parser/.babelrc delete mode 100644 packages/kbn-cli-dev-mode/.babelrc delete mode 100644 packages/kbn-config-schema/.babelrc delete mode 100644 packages/kbn-config/.babelrc delete mode 100644 packages/kbn-crypto/.babelrc delete mode 100644 packages/kbn-dev-utils/.babelrc delete mode 100644 packages/kbn-docs-utils/.babelrc delete mode 100644 packages/kbn-es-archiver/.babelrc delete mode 100644 packages/kbn-es-query/.babelrc delete mode 100644 packages/kbn-es-query/.babelrc.browser delete mode 100644 packages/kbn-es/.babelrc delete mode 100644 packages/kbn-field-types/.babelrc delete mode 100644 packages/kbn-i18n/.babelrc delete mode 100644 packages/kbn-i18n/.babelrc.browser delete mode 100644 packages/kbn-interpreter/.babelrc delete mode 100644 packages/kbn-io-ts-utils/.babelrc delete mode 100644 packages/kbn-legacy-logging/.babelrc delete mode 100644 packages/kbn-logging/.babelrc delete mode 100644 packages/kbn-mapbox-gl/.babelrc delete mode 100644 packages/kbn-monaco/.babelrc delete mode 100644 packages/kbn-monaco/.babelrc.browser delete mode 100644 packages/kbn-optimizer/.babelrc delete mode 100644 packages/kbn-plugin-generator/.babelrc delete mode 100644 packages/kbn-plugin-helpers/.babelrc delete mode 100644 packages/kbn-rule-data-utils/.babelrc delete mode 100644 packages/kbn-securitysolution-autocomplete/.babelrc delete mode 100644 packages/kbn-securitysolution-autocomplete/.babelrc.browser delete mode 100644 packages/kbn-securitysolution-es-utils/.babelrc delete mode 100644 packages/kbn-securitysolution-hook-utils/.babelrc delete mode 100644 packages/kbn-securitysolution-hook-utils/.babelrc.browser delete mode 100644 packages/kbn-securitysolution-io-ts-alerting-types/.babelrc delete mode 100644 packages/kbn-securitysolution-io-ts-alerting-types/.babelrc.browser delete mode 100644 packages/kbn-securitysolution-io-ts-list-types/.babelrc delete mode 100644 packages/kbn-securitysolution-io-ts-list-types/.babelrc.browser delete mode 100644 packages/kbn-securitysolution-io-ts-types/.babelrc delete mode 100644 packages/kbn-securitysolution-io-ts-types/.babelrc.browser delete mode 100644 packages/kbn-securitysolution-io-ts-utils/.babelrc delete mode 100644 packages/kbn-securitysolution-io-ts-utils/.babelrc.browser delete mode 100644 packages/kbn-securitysolution-list-api/.babelrc delete mode 100644 packages/kbn-securitysolution-list-api/.babelrc.browser delete mode 100644 packages/kbn-securitysolution-list-constants/.babelrc delete mode 100644 packages/kbn-securitysolution-list-constants/.babelrc.browser delete mode 100644 packages/kbn-securitysolution-list-hooks/.babelrc delete mode 100644 packages/kbn-securitysolution-list-hooks/.babelrc.browser delete mode 100644 packages/kbn-securitysolution-list-utils/.babelrc delete mode 100644 packages/kbn-securitysolution-list-utils/.babelrc.browser delete mode 100644 packages/kbn-securitysolution-t-grid/.babelrc delete mode 100644 packages/kbn-securitysolution-t-grid/.babelrc.browser delete mode 100644 packages/kbn-securitysolution-utils/.babelrc delete mode 100644 packages/kbn-server-http-tools/.babelrc delete mode 100644 packages/kbn-server-route-repository/.babelrc delete mode 100644 packages/kbn-std/.babelrc delete mode 100644 packages/kbn-storybook/.babelrc delete mode 100644 packages/kbn-telemetry-tools/.babelrc delete mode 100644 packages/kbn-test/.babelrc delete mode 100644 packages/kbn-tinymath/babel.config.js delete mode 100644 packages/kbn-typed-react-router-config/.babelrc delete mode 100644 packages/kbn-typed-react-router-config/.babelrc.browser delete mode 100644 packages/kbn-ui-shared-deps/.babelrc delete mode 100644 packages/kbn-utility-types/.babelrc delete mode 100644 packages/kbn-utils/.babelrc diff --git a/packages/elastic-datemath/.babelrc b/packages/elastic-datemath/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/elastic-datemath/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-ace/.babelrc b/packages/kbn-ace/.babelrc deleted file mode 100644 index 30ffbd24e1f1..000000000000 --- a/packages/kbn-ace/.babelrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"], - "ignore": [ - "src/ace/modes/x_json/worker/x_json.ace.worker.js" - ] -} diff --git a/packages/kbn-ace/BUILD.bazel b/packages/kbn-ace/BUILD.bazel index 5b9c38b16b53..a13863b765d3 100644 --- a/packages/kbn-ace/BUILD.bazel +++ b/packages/kbn-ace/BUILD.bazel @@ -45,6 +45,8 @@ jsts_transpiler( srcs = SRCS, additional_args = [ "--copy-files", + "--ignore", + "**/*/src/ace/modes/x_json/worker/x_json.ace.worker.js", "--quiet" ], build_pkg_name = package_name(), diff --git a/packages/kbn-alerts/.babelrc b/packages/kbn-alerts/.babelrc deleted file mode 100644 index 40a198521b90..000000000000 --- a/packages/kbn-alerts/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-alerts/.babelrc.browser b/packages/kbn-alerts/.babelrc.browser deleted file mode 100644 index 71bbfbcd6eb2..000000000000 --- a/packages/kbn-alerts/.babelrc.browser +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/webpack_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-alerts/BUILD.bazel b/packages/kbn-alerts/BUILD.bazel index a571380202cd..91c575346fff 100644 --- a/packages/kbn-alerts/BUILD.bazel +++ b/packages/kbn-alerts/BUILD.bazel @@ -57,7 +57,7 @@ jsts_transpiler( name = "target_web", srcs = SRCS, build_pkg_name = package_name(), - config_file = ".babelrc.browser" + web = True, ) ts_config( diff --git a/packages/kbn-analytics/.babelrc b/packages/kbn-analytics/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-analytics/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-analytics/.babelrc.browser b/packages/kbn-analytics/.babelrc.browser deleted file mode 100644 index dc6a77bbe0bc..000000000000 --- a/packages/kbn-analytics/.babelrc.browser +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/webpack_preset"] -} diff --git a/packages/kbn-analytics/BUILD.bazel b/packages/kbn-analytics/BUILD.bazel index ca8cdcbffbb5..cc65746e890c 100644 --- a/packages/kbn-analytics/BUILD.bazel +++ b/packages/kbn-analytics/BUILD.bazel @@ -42,7 +42,7 @@ jsts_transpiler( name = "target_web", srcs = SRCS, build_pkg_name = package_name(), - config_file = ".babelrc.browser" + web = True, ) ts_config( diff --git a/packages/kbn-apm-config-loader/.babelrc b/packages/kbn-apm-config-loader/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-apm-config-loader/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-apm-utils/.babelrc b/packages/kbn-apm-utils/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-apm-utils/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-babel-code-parser/.babelrc b/packages/kbn-babel-code-parser/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-babel-code-parser/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-cli-dev-mode/.babelrc b/packages/kbn-cli-dev-mode/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-cli-dev-mode/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-config-schema/.babelrc b/packages/kbn-config-schema/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-config-schema/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-config/.babelrc b/packages/kbn-config/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-config/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-crypto/.babelrc b/packages/kbn-crypto/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-crypto/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-dev-utils/.babelrc b/packages/kbn-dev-utils/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-dev-utils/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-docs-utils/.babelrc b/packages/kbn-docs-utils/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-docs-utils/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-es-archiver/.babelrc b/packages/kbn-es-archiver/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-es-archiver/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-es-query/.babelrc b/packages/kbn-es-query/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-es-query/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-es-query/.babelrc.browser b/packages/kbn-es-query/.babelrc.browser deleted file mode 100644 index dc6a77bbe0bc..000000000000 --- a/packages/kbn-es-query/.babelrc.browser +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/webpack_preset"] -} diff --git a/packages/kbn-es-query/BUILD.bazel b/packages/kbn-es-query/BUILD.bazel index d4a531d308f6..b3d861d937c8 100644 --- a/packages/kbn-es-query/BUILD.bazel +++ b/packages/kbn-es-query/BUILD.bazel @@ -76,7 +76,7 @@ jsts_transpiler( name = "target_web", srcs = SRCS, build_pkg_name = package_name(), - config_file = ".babelrc.browser" + web = True, ) ts_config( diff --git a/packages/kbn-es/.babelrc b/packages/kbn-es/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-es/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-field-types/.babelrc b/packages/kbn-field-types/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-field-types/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-i18n/.babelrc b/packages/kbn-i18n/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-i18n/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-i18n/.babelrc.browser b/packages/kbn-i18n/.babelrc.browser deleted file mode 100644 index dc6a77bbe0bc..000000000000 --- a/packages/kbn-i18n/.babelrc.browser +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/webpack_preset"] -} diff --git a/packages/kbn-i18n/BUILD.bazel b/packages/kbn-i18n/BUILD.bazel index 62d5fb1d75a4..49d5603b2c51 100644 --- a/packages/kbn-i18n/BUILD.bazel +++ b/packages/kbn-i18n/BUILD.bazel @@ -65,7 +65,7 @@ jsts_transpiler( name = "target_web", srcs = SRCS, build_pkg_name = package_name(), - config_file = ".babelrc.browser" + web = True, ) ts_config( diff --git a/packages/kbn-interpreter/.babelrc b/packages/kbn-interpreter/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-interpreter/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-io-ts-utils/.babelrc b/packages/kbn-io-ts-utils/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-io-ts-utils/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-legacy-logging/.babelrc b/packages/kbn-legacy-logging/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-legacy-logging/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-logging/.babelrc b/packages/kbn-logging/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-logging/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-mapbox-gl/.babelrc b/packages/kbn-mapbox-gl/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-mapbox-gl/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-monaco/.babelrc b/packages/kbn-monaco/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-monaco/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-monaco/.babelrc.browser b/packages/kbn-monaco/.babelrc.browser deleted file mode 100644 index dc6a77bbe0bc..000000000000 --- a/packages/kbn-monaco/.babelrc.browser +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/webpack_preset"] -} diff --git a/packages/kbn-monaco/BUILD.bazel b/packages/kbn-monaco/BUILD.bazel index 3656210cb6b1..d2d9bf3f9a00 100644 --- a/packages/kbn-monaco/BUILD.bazel +++ b/packages/kbn-monaco/BUILD.bazel @@ -56,7 +56,7 @@ jsts_transpiler( name = "target_web", srcs = SRCS, build_pkg_name = package_name(), - config_file = ".babelrc.browser" + web = True, ) webpack( diff --git a/packages/kbn-optimizer/.babelrc b/packages/kbn-optimizer/.babelrc deleted file mode 100644 index 1685d1644d94..000000000000 --- a/packages/kbn-optimizer/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"], - "ignore": ["**/*.test.js"] -} diff --git a/packages/kbn-plugin-generator/.babelrc b/packages/kbn-plugin-generator/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-plugin-generator/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-plugin-helpers/.babelrc b/packages/kbn-plugin-helpers/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-plugin-helpers/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-rule-data-utils/.babelrc b/packages/kbn-rule-data-utils/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-rule-data-utils/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-securitysolution-autocomplete/.babelrc b/packages/kbn-securitysolution-autocomplete/.babelrc deleted file mode 100644 index 40a198521b90..000000000000 --- a/packages/kbn-securitysolution-autocomplete/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-securitysolution-autocomplete/.babelrc.browser b/packages/kbn-securitysolution-autocomplete/.babelrc.browser deleted file mode 100644 index 71bbfbcd6eb2..000000000000 --- a/packages/kbn-securitysolution-autocomplete/.babelrc.browser +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/webpack_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-securitysolution-autocomplete/BUILD.bazel b/packages/kbn-securitysolution-autocomplete/BUILD.bazel index 53cd7b4f8d3e..ac90a0479ce2 100644 --- a/packages/kbn-securitysolution-autocomplete/BUILD.bazel +++ b/packages/kbn-securitysolution-autocomplete/BUILD.bazel @@ -72,7 +72,7 @@ jsts_transpiler( name = "target_web", srcs = SRCS, build_pkg_name = package_name(), - config_file = ".babelrc.browser" + web = True, ) ts_config( diff --git a/packages/kbn-securitysolution-es-utils/.babelrc b/packages/kbn-securitysolution-es-utils/.babelrc deleted file mode 100644 index 40a198521b90..000000000000 --- a/packages/kbn-securitysolution-es-utils/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-securitysolution-hook-utils/.babelrc b/packages/kbn-securitysolution-hook-utils/.babelrc deleted file mode 100644 index b17a19d6faf3..000000000000 --- a/packages/kbn-securitysolution-hook-utils/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-securitysolution-hook-utils/.babelrc.browser b/packages/kbn-securitysolution-hook-utils/.babelrc.browser deleted file mode 100644 index 71bbfbcd6eb2..000000000000 --- a/packages/kbn-securitysolution-hook-utils/.babelrc.browser +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/webpack_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-securitysolution-hook-utils/BUILD.bazel b/packages/kbn-securitysolution-hook-utils/BUILD.bazel index de007b34eeb2..bc7fd3bce141 100644 --- a/packages/kbn-securitysolution-hook-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-hook-utils/BUILD.bazel @@ -54,7 +54,7 @@ jsts_transpiler( name = "target_web", srcs = SRCS, build_pkg_name = package_name(), - config_file = ".babelrc.browser" + web = True, ) ts_config( diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/.babelrc b/packages/kbn-securitysolution-io-ts-alerting-types/.babelrc deleted file mode 100644 index b17a19d6faf3..000000000000 --- a/packages/kbn-securitysolution-io-ts-alerting-types/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/.babelrc.browser b/packages/kbn-securitysolution-io-ts-alerting-types/.babelrc.browser deleted file mode 100644 index 71bbfbcd6eb2..000000000000 --- a/packages/kbn-securitysolution-io-ts-alerting-types/.babelrc.browser +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/webpack_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel index 940c6d589da1..cdee3a2f9254 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel +++ b/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel @@ -55,7 +55,7 @@ jsts_transpiler( name = "target_web", srcs = SRCS, build_pkg_name = package_name(), - config_file = ".babelrc.browser" + web = True, ) ts_config( diff --git a/packages/kbn-securitysolution-io-ts-list-types/.babelrc b/packages/kbn-securitysolution-io-ts-list-types/.babelrc deleted file mode 100644 index 40a198521b90..000000000000 --- a/packages/kbn-securitysolution-io-ts-list-types/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-securitysolution-io-ts-list-types/.babelrc.browser b/packages/kbn-securitysolution-io-ts-list-types/.babelrc.browser deleted file mode 100644 index 71bbfbcd6eb2..000000000000 --- a/packages/kbn-securitysolution-io-ts-list-types/.babelrc.browser +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/webpack_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel index 07ed552cdc40..ff4f2e80cbd3 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel +++ b/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel @@ -53,7 +53,7 @@ jsts_transpiler( name = "target_web", srcs = SRCS, build_pkg_name = package_name(), - config_file = ".babelrc.browser" + web = True, ) ts_config( diff --git a/packages/kbn-securitysolution-io-ts-types/.babelrc b/packages/kbn-securitysolution-io-ts-types/.babelrc deleted file mode 100644 index 40a198521b90..000000000000 --- a/packages/kbn-securitysolution-io-ts-types/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-securitysolution-io-ts-types/.babelrc.browser b/packages/kbn-securitysolution-io-ts-types/.babelrc.browser deleted file mode 100644 index 71bbfbcd6eb2..000000000000 --- a/packages/kbn-securitysolution-io-ts-types/.babelrc.browser +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/webpack_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-securitysolution-io-ts-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-types/BUILD.bazel index adabf9708a59..fe2247ac0b61 100644 --- a/packages/kbn-securitysolution-io-ts-types/BUILD.bazel +++ b/packages/kbn-securitysolution-io-ts-types/BUILD.bazel @@ -53,7 +53,7 @@ jsts_transpiler( name = "target_web", srcs = SRCS, build_pkg_name = package_name(), - config_file = ".babelrc.browser" + web = True, ) ts_config( diff --git a/packages/kbn-securitysolution-io-ts-utils/.babelrc b/packages/kbn-securitysolution-io-ts-utils/.babelrc deleted file mode 100644 index 40a198521b90..000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-securitysolution-io-ts-utils/.babelrc.browser b/packages/kbn-securitysolution-io-ts-utils/.babelrc.browser deleted file mode 100644 index 71bbfbcd6eb2..000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/.babelrc.browser +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/webpack_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-securitysolution-io-ts-utils/BUILD.bazel b/packages/kbn-securitysolution-io-ts-utils/BUILD.bazel index 346bd19451ab..24819bdd16a3 100644 --- a/packages/kbn-securitysolution-io-ts-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-io-ts-utils/BUILD.bazel @@ -57,7 +57,7 @@ jsts_transpiler( name = "target_web", srcs = SRCS, build_pkg_name = package_name(), - config_file = ".babelrc.browser" + web = True, ) ts_config( diff --git a/packages/kbn-securitysolution-list-api/.babelrc b/packages/kbn-securitysolution-list-api/.babelrc deleted file mode 100644 index 40a198521b90..000000000000 --- a/packages/kbn-securitysolution-list-api/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-securitysolution-list-api/.babelrc.browser b/packages/kbn-securitysolution-list-api/.babelrc.browser deleted file mode 100644 index 71bbfbcd6eb2..000000000000 --- a/packages/kbn-securitysolution-list-api/.babelrc.browser +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/webpack_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-securitysolution-list-api/BUILD.bazel b/packages/kbn-securitysolution-list-api/BUILD.bazel index 6858a9389119..52a134456cdd 100644 --- a/packages/kbn-securitysolution-list-api/BUILD.bazel +++ b/packages/kbn-securitysolution-list-api/BUILD.bazel @@ -56,7 +56,7 @@ jsts_transpiler( name = "target_web", srcs = SRCS, build_pkg_name = package_name(), - config_file = ".babelrc.browser" + web = True, ) ts_config( diff --git a/packages/kbn-securitysolution-list-constants/.babelrc b/packages/kbn-securitysolution-list-constants/.babelrc deleted file mode 100644 index 40a198521b90..000000000000 --- a/packages/kbn-securitysolution-list-constants/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-securitysolution-list-constants/.babelrc.browser b/packages/kbn-securitysolution-list-constants/.babelrc.browser deleted file mode 100644 index 71bbfbcd6eb2..000000000000 --- a/packages/kbn-securitysolution-list-constants/.babelrc.browser +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/webpack_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-securitysolution-list-constants/BUILD.bazel b/packages/kbn-securitysolution-list-constants/BUILD.bazel index 9b3de9520f6a..db4dd94091ab 100644 --- a/packages/kbn-securitysolution-list-constants/BUILD.bazel +++ b/packages/kbn-securitysolution-list-constants/BUILD.bazel @@ -45,7 +45,7 @@ jsts_transpiler( name = "target_web", srcs = SRCS, build_pkg_name = package_name(), - config_file = ".babelrc.browser" + web = True, ) ts_config( diff --git a/packages/kbn-securitysolution-list-hooks/.babelrc b/packages/kbn-securitysolution-list-hooks/.babelrc deleted file mode 100644 index 40a198521b90..000000000000 --- a/packages/kbn-securitysolution-list-hooks/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-securitysolution-list-hooks/.babelrc.browser b/packages/kbn-securitysolution-list-hooks/.babelrc.browser deleted file mode 100644 index 71bbfbcd6eb2..000000000000 --- a/packages/kbn-securitysolution-list-hooks/.babelrc.browser +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/webpack_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-securitysolution-list-hooks/BUILD.bazel b/packages/kbn-securitysolution-list-hooks/BUILD.bazel index ba8c579bb97d..2a9666bd1429 100644 --- a/packages/kbn-securitysolution-list-hooks/BUILD.bazel +++ b/packages/kbn-securitysolution-list-hooks/BUILD.bazel @@ -62,7 +62,7 @@ jsts_transpiler( name = "target_web", srcs = SRCS, build_pkg_name = package_name(), - config_file = ".babelrc.browser" + web = True, ) ts_config( diff --git a/packages/kbn-securitysolution-list-utils/.babelrc b/packages/kbn-securitysolution-list-utils/.babelrc deleted file mode 100644 index 40a198521b90..000000000000 --- a/packages/kbn-securitysolution-list-utils/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-securitysolution-list-utils/.babelrc.browser b/packages/kbn-securitysolution-list-utils/.babelrc.browser deleted file mode 100644 index 71bbfbcd6eb2..000000000000 --- a/packages/kbn-securitysolution-list-utils/.babelrc.browser +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/webpack_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-securitysolution-list-utils/BUILD.bazel b/packages/kbn-securitysolution-list-utils/BUILD.bazel index 4701723286ef..eb33eb1a03b6 100644 --- a/packages/kbn-securitysolution-list-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-list-utils/BUILD.bazel @@ -58,7 +58,7 @@ jsts_transpiler( name = "target_web", srcs = SRCS, build_pkg_name = package_name(), - config_file = ".babelrc.browser" + web = True, ) ts_config( diff --git a/packages/kbn-securitysolution-t-grid/.babelrc b/packages/kbn-securitysolution-t-grid/.babelrc deleted file mode 100644 index 40a198521b90..000000000000 --- a/packages/kbn-securitysolution-t-grid/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-securitysolution-t-grid/.babelrc.browser b/packages/kbn-securitysolution-t-grid/.babelrc.browser deleted file mode 100644 index 71bbfbcd6eb2..000000000000 --- a/packages/kbn-securitysolution-t-grid/.babelrc.browser +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/webpack_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-securitysolution-t-grid/BUILD.bazel b/packages/kbn-securitysolution-t-grid/BUILD.bazel index f9a6a5d7934a..dd4e73af9572 100644 --- a/packages/kbn-securitysolution-t-grid/BUILD.bazel +++ b/packages/kbn-securitysolution-t-grid/BUILD.bazel @@ -54,7 +54,7 @@ jsts_transpiler( name = "target_web", srcs = SRCS, build_pkg_name = package_name(), - config_file = ".babelrc.browser" + web = True, ) ts_config( diff --git a/packages/kbn-securitysolution-utils/.babelrc b/packages/kbn-securitysolution-utils/.babelrc deleted file mode 100644 index 40a198521b90..000000000000 --- a/packages/kbn-securitysolution-utils/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"], - "ignore": ["**/*.test.ts", "**/*.test.tsx"] -} diff --git a/packages/kbn-server-http-tools/.babelrc b/packages/kbn-server-http-tools/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-server-http-tools/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-server-route-repository/.babelrc b/packages/kbn-server-route-repository/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-server-route-repository/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-std/.babelrc b/packages/kbn-std/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-std/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-storybook/.babelrc b/packages/kbn-storybook/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-storybook/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-telemetry-tools/.babelrc b/packages/kbn-telemetry-tools/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-telemetry-tools/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-test/.babelrc b/packages/kbn-test/.babelrc deleted file mode 100644 index 1685d1644d94..000000000000 --- a/packages/kbn-test/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"], - "ignore": ["**/*.test.js"] -} diff --git a/packages/kbn-tinymath/babel.config.js b/packages/kbn-tinymath/babel.config.js deleted file mode 100644 index b4a118df51af..000000000000 --- a/packages/kbn-tinymath/babel.config.js +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -module.exports = { - env: { - web: { - presets: ['@kbn/babel-preset/webpack_preset'], - }, - node: { - presets: ['@kbn/babel-preset/node_preset'], - }, - }, - ignore: ['**/*.test.ts', '**/*.test.tsx'], -}; diff --git a/packages/kbn-typed-react-router-config/.babelrc b/packages/kbn-typed-react-router-config/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-typed-react-router-config/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-typed-react-router-config/.babelrc.browser b/packages/kbn-typed-react-router-config/.babelrc.browser deleted file mode 100644 index dc6a77bbe0bc..000000000000 --- a/packages/kbn-typed-react-router-config/.babelrc.browser +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/webpack_preset"] -} diff --git a/packages/kbn-typed-react-router-config/BUILD.bazel b/packages/kbn-typed-react-router-config/BUILD.bazel index be346f8321fa..7fccc53bd744 100644 --- a/packages/kbn-typed-react-router-config/BUILD.bazel +++ b/packages/kbn-typed-react-router-config/BUILD.bazel @@ -56,7 +56,7 @@ jsts_transpiler( name = "target_web", srcs = SRCS, build_pkg_name = package_name(), - config_file = ".babelrc.browser" + web = True, ) ts_config( diff --git a/packages/kbn-ui-shared-deps/.babelrc b/packages/kbn-ui-shared-deps/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-ui-shared-deps/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-utility-types/.babelrc b/packages/kbn-utility-types/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-utility-types/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-utils/.babelrc b/packages/kbn-utils/.babelrc deleted file mode 100644 index 7da72d177912..000000000000 --- a/packages/kbn-utils/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/src/dev/bazel/jsts_transpiler.bzl b/src/dev/bazel/jsts_transpiler.bzl index 03033bbfa83f..5116c73adb3c 100644 --- a/src/dev/bazel/jsts_transpiler.bzl +++ b/src/dev/bazel/jsts_transpiler.bzl @@ -2,28 +2,42 @@ load("@npm//@babel/cli:index.bzl", _babel = "babel") -def jsts_transpiler(name, srcs, build_pkg_name, root_input_dir = "src", config_file = ".babelrc", additional_args = ["--quiet"], **kwargs): +def jsts_transpiler(name, srcs, build_pkg_name, web = False, root_input_dir = "src", additional_args = ["--quiet"], **kwargs): """A macro around the autogenerated babel rule. Args: name: target name srcs: list of sources + build_pkg_name: package name into the build folder + web: setup the correct presets to consume the outputs in the browser, defaults to "False" and optimizes for node root_input_dir: defines the root input dir to transpile files from, defaults to "src" - config_file: transpiler config file, it defaults to a package local .babelrc additional_args: Any additional extra arguments, defaults to --quiet **kwargs: the rest """ + + inline_presets = [ + "--presets", + ] + + if web: + inline_presets += [ + "@kbn/babel-preset/webpack_preset", + ] + else: + inline_presets += [ + "@kbn/babel-preset/node_preset", + ] + args = [ "./%s/%s" % (build_pkg_name, root_input_dir), - "--config-file", - "./%s/%s" % (build_pkg_name, config_file), "--out-dir", "$(@D)", + "--no-babelrc", "--extensions", ".ts,.tsx,.js", - ] + additional_args + ] + inline_presets + additional_args - data = [config_file] + srcs + [ + data = srcs + [ "//packages/kbn-babel-preset", ] From 9132b43128bbf8ad943577dd6043a320459d92f4 Mon Sep 17 00:00:00 2001 From: ymao1 Date: Fri, 3 Sep 2021 18:48:45 -0400 Subject: [PATCH 39/47] [Alerting][Docs] Add de-duplication example to ES query docs + update email docs wording (#111026) * Change to allowlist * Adding example to es query rule * Apply suggestions from code review Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- .../connectors/action-types/email.asciidoc | 2 +- .../alerting/rule-types/es-query.asciidoc | 30 ++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/docs/management/connectors/action-types/email.asciidoc b/docs/management/connectors/action-types/email.asciidoc index 98d7b2591a57..131ff5ea5e9f 100644 --- a/docs/management/connectors/action-types/email.asciidoc +++ b/docs/management/connectors/action-types/email.asciidoc @@ -107,7 +107,7 @@ For other email servers, you can check the list of well-known services that Node [[elasticcloud]] ==== Sending email from Elastic Cloud -IMPORTANT: These instructions require you to link:{cloud}/ec-watcher.html#ec-watcher-whitelist[whitelist] the email addresses that notifications get sent first. +IMPORTANT: These instructions require you to link:{cloud}/ec-watcher.html#ec-watcher-whitelist[allowlist] the email addresses that notifications get sent. Use the following connector settings to send email from Elastic Cloud: diff --git a/docs/user/alerting/rule-types/es-query.asciidoc b/docs/user/alerting/rule-types/es-query.asciidoc index 5615c79a6c9c..65d39ba170c3 100644 --- a/docs/user/alerting/rule-types/es-query.asciidoc +++ b/docs/user/alerting/rule-types/es-query.asciidoc @@ -60,4 +60,32 @@ image::user/alerting/images/rule-types-es-query-valid.png[Test {es} query return * An error message is shown if the query is invalid. + [role="screenshot"] -image::user/alerting/images/rule-types-es-query-invalid.png[Test {es} query shows error when invalid] \ No newline at end of file +image::user/alerting/images/rule-types-es-query-invalid.png[Test {es} query shows error when invalid] + +[float] +==== Match de-duplication + +The {es} query rule type performs de-duplication of document matches across rule executions. If you configure the rule with a schedule interval smaller than the time window, and a document matches a query in multiple rule executions, it will be alerted on only once. + +Suppose you have a rule configured to run every minute. The rule uses a time window of 1 hour and checks if there are more than 99 matches for the query. The {es} query rule type will do the following: + +[cols="3*<"] +|=== + +| `Execution 1 (0:00)` +| Rule finds 113 matches in the last hour: `113 > 99` +| Rule is active and user will be alerted. + +| `Execution 2 (0:01)` +| Rule finds 127 matches in the last hour. 105 of the matches are duplicates that were alerted on in Execution 1, so you actually have 22 matches: `22 !> 99` +| No alert. + +| `Execution 3 (0:02)` +| Rule finds 159 matches in the last hour. 88 of the matches are duplicates that were alerted on in Execution 1, so you actually have 71 matches: `71 !> 99` +| No alert. + +| `Execution 4 (0:03)` +| Rule finds 190 matches in the last hour. 71 of them are duplicates that were alerted on in Exeuction 1, so you actually have 119 matches: `119 > 99` +| Rule is active and user will be alerted. + +|=== \ No newline at end of file From 61e533f253bbd3c1f123ffd660e0fd31c760a8e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Sun, 5 Sep 2021 07:14:06 +0200 Subject: [PATCH 40/47] [APM] Clean up readme (#110973) * [APM] Clean up readme * Update linting.md * Update testing.md * Update testing.md * Update testing.md * Update plugin-list.asciidoc Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/developer/plugin-list.asciidoc | 2 +- x-pack/plugins/apm/dev_docs/feature_flags.md | 14 +++ x-pack/plugins/apm/dev_docs/linting.md | 22 +++++ x-pack/plugins/apm/dev_docs/local_setup.md | 50 +++++++++++ x-pack/plugins/apm/dev_docs/testing.md | 66 ++++++++++++++ x-pack/plugins/apm/readme.md | 93 ++------------------ x-pack/plugins/apm/scripts/test/README.md | 64 +------------- 7 files changed, 162 insertions(+), 149 deletions(-) create mode 100644 x-pack/plugins/apm/dev_docs/feature_flags.md create mode 100644 x-pack/plugins/apm/dev_docs/linting.md create mode 100644 x-pack/plugins/apm/dev_docs/local_setup.md create mode 100644 x-pack/plugins/apm/dev_docs/testing.md diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index d2d543ff59d5..dfb62f23445e 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -350,7 +350,7 @@ The plugin exposes the static DefaultEditorController class to consume. |{kib-repo}blob/{branch}/x-pack/plugins/apm/readme.md[apm] -|To access an elasticsearch instance that has live data you have two options: +|Local setup documentation |{kib-repo}blob/{branch}/x-pack/plugins/banners/README.md[banners] diff --git a/x-pack/plugins/apm/dev_docs/feature_flags.md b/x-pack/plugins/apm/dev_docs/feature_flags.md new file mode 100644 index 000000000000..9f722dd5eac5 --- /dev/null +++ b/x-pack/plugins/apm/dev_docs/feature_flags.md @@ -0,0 +1,14 @@ +## Feature flags + +To set up a flagged feature, add the name of the feature key (`apm:myFeature`) to [commmon/ui_settings_keys.ts](./common/ui_settings_keys.ts) and the feature parameters to [server/ui_settings.ts](./server/ui_settings.ts). + +Test for the feature like: + +```js +import { myFeatureEnabled } from '../ui_settings_keys'; +if (core.uiSettings.get(myFeatureEnabled)) { + doStuff(); +} +``` + +Settings can be managed in Kibana under Stack Management > Advanced Settings > Observability. \ No newline at end of file diff --git a/x-pack/plugins/apm/dev_docs/linting.md b/x-pack/plugins/apm/dev_docs/linting.md new file mode 100644 index 000000000000..a4fd3094f121 --- /dev/null +++ b/x-pack/plugins/apm/dev_docs/linting.md @@ -0,0 +1,22 @@ +## Linting + +_Note: Run the following commands from the root of Kibana._ + +### Typescript + +``` +node scripts/type_check.js --project x-pack/plugins/apm/tsconfig.json +``` + +### Prettier + +``` +yarn prettier "./x-pack/plugins/apm/**/*.{tsx,ts,js}" --write +``` + +### ESLint + +``` +node scripts/eslint.js x-pack/legacy/plugins/apm +``` +diff --git a/x-pack/plugins/apm/dev_docs/feature_flags.md b/x-pack/plugins/apm/dev_docs/feature_flags.md diff --git a/x-pack/plugins/apm/dev_docs/local_setup.md b/x-pack/plugins/apm/dev_docs/local_setup.md new file mode 100644 index 000000000000..d977f4444514 --- /dev/null +++ b/x-pack/plugins/apm/dev_docs/local_setup.md @@ -0,0 +1,50 @@ +## Local environment setup + +### Kibana + +``` +git clone git@github.com:elastic/kibana.git +cd kibana/ +yarn kbn bootstrap +yarn start --no-base-path +``` + +### APM Server, Elasticsearch and data + +To access an elasticsearch instance that has live data you have two options: + +#### A. Connect to Elasticsearch on Cloud (internal devs only) + +Find the credentials for the cluster [here](https://github.com/elastic/apm-dev/blob/master/docs/credentials/apm-ui-clusters.md#apmelstcco) + +#### B. Start Elastic Stack and APM data generators + +``` +git clone git@github.com:elastic/apm-integration-testing.git +cd apm-integration-testing/ +./scripts/compose.py start master --all --no-kibana +``` + +_Docker Compose is required_ + +### Setup default APM users + +APM behaves differently depending on which the role and permissions a logged in user has. To create the users run: + +```sh +node x-pack/plugins/apm/scripts/create-apm-users-and-roles.js --username admin --password changeme --kibana-url http://localhost:5601 --role-suffix +``` + +This will create: + +**apm_read_user**: Read only user + +**apm_power_user**: Read+write user. + +## Debugging Elasticsearch queries + +All APM api endpoints accept `_inspect=true` as a query param that will result in the underlying ES query being outputted in the Kibana backend process. + +Example: +`/api/apm/services/my_service?_inspect=true` +diff --git a/x-pack/plugins/apm/dev_docs/linting.md b/x-pack/plugins/apm/dev_docs/linting.md diff --git a/x-pack/plugins/apm/dev_docs/testing.md b/x-pack/plugins/apm/dev_docs/testing.md new file mode 100644 index 000000000000..93f32111048c --- /dev/null +++ b/x-pack/plugins/apm/dev_docs/testing.md @@ -0,0 +1,66 @@ +# Testing + +## Unit Tests (Jest) + +``` +node scripts/test/jest [--watch] [--updateSnapshot] +``` + +#### Coverage + +HTML coverage report can be found in target/coverage/jest after tests have run. + +``` +open target/coverage/jest/index.html +``` + +--- + +## API Tests + +API tests are separated in two suites: + +- a basic license test suite [default] +- a trial license test suite (the equivalent of gold+) + +``` +node scripts/test/api [--trial] [--help] +``` + +The API tests are located in `x-pack/test/apm_api_integration/`. + +**API Test tips** + +- For debugging access Elasticsearch on http://localhost:9220 (`elastic` / `changeme`) +- To update snapshots append `--updateSnapshots` to the functional_test_runner command + +--- + +## E2E Tests (Cypress) + +``` +node scripts/test/e2e [--trial] [--help] +``` + +The E2E tests are located [here](../../ftr_e2e) + +--- + +## Functional tests (Security and Correlations tests) +TODO: We could try moving this tests to the new e2e tests located at `x-pack/plugins/apm/ftr_e2e`. + +**Start server** + +``` +node scripts/functional_tests_server --config x-pack/test/functional/config.js +``` + +**Run tests** + +``` +node scripts/functional_test_runner --config x-pack/test/functional/config.js --grep='APM specs' +``` + +APM tests are located in `x-pack/test/functional/apps/apm`. +For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) +diff --git a/x-pack/plugins/apm/scripts/test/README.md b/x-pack/plugins/apm/scripts/test/README.md diff --git a/x-pack/plugins/apm/readme.md b/x-pack/plugins/apm/readme.md index a331bb4e9f11..fe7e77d28986 100644 --- a/x-pack/plugins/apm/readme.md +++ b/x-pack/plugins/apm/readme.md @@ -2,105 +2,28 @@ ## Local environment setup -### Kibana - -``` -git clone git@github.com:elastic/kibana.git -cd kibana/ -yarn kbn bootstrap -yarn start --no-base-path -``` - -### APM Server, Elasticsearch and data - -To access an elasticsearch instance that has live data you have two options: - -#### A. Connect to Elasticsearch on Cloud (internal devs only) - -Find the credentials for the cluster [here](https://github.com/elastic/apm-dev/blob/master/docs/credentials/apm-ui-clusters.md#apmelstcco) - -#### B. Start Elastic Stack and APM data generators - -``` -git clone git@github.com:elastic/apm-integration-testing.git -cd apm-integration-testing/ -./scripts/compose.py start master --all --no-kibana -``` - -_Docker Compose is required_ +[Local setup documentation](./dev_docs/local_setup.md) ## Testing -Go to [tests documentation](./scripts/test/README.md) +[Testing documentation](./dev_docs/testing.md) ## Linting -_Note: Run the following commands from `kibana/`._ - -### Typescript - -``` -node scripts/type_check.js --project x-pack/plugins/apm/tsconfig.json -``` - -### Prettier - -``` -yarn prettier "./x-pack/plugins/apm/**/*.{tsx,ts,js}" --write -``` - -### ESLint - -``` -node scripts/eslint.js x-pack/legacy/plugins/apm -``` - -## Setup default APM users - -APM behaves differently depending on which the role and permissions a logged in user has. To create the users run: - -```sh -node x-pack/plugins/apm/scripts/create-apm-users-and-roles.js --username admin --password changeme --kibana-url http://localhost:5601 --role-suffix -``` - -This will create: - -**apm_read_user**: Read only user - -**apm_power_user**: Read+write user. - -## Debugging Elasticsearch queries - -All APM api endpoints accept `_inspect=true` as a query param that will result in the underlying ES query being outputted in the Kibana backend process. - -Example: -`/api/apm/services/my_service?_inspect=true` +[Linting documentation](./dev_docs/linting.md) ## Storybook -Start the [Storybook](https://storybook.js.org/) development environment with -`yarn storybook apm`. All files with a .stories.tsx extension will be loaded. -You can access the development environment at http://localhost:9001. - -## Experimental features settings - -To set up a flagged feature, add the name of the feature key (`apm:myFeature`) to [commmon/ui_settings_keys.ts](./common/ui_settings_keys.ts) and the feature parameters to [server/ui_settings.ts](./server/ui_settings.ts). - -Test for the feature like: - -```js -import { myFeatureEnabled } from '../ui_settings_keys'; -if (core.uiSettings.get(myFeatureEnabled)) { - doStuff(); -} +**Start** +``` +yarn storybook apm ``` -Settings can be managed in Kibana under Stack Management > Advanced Settings > Observability. +All files with a .stories.tsx extension will be loaded. You can access the development environment at http://localhost:9001. ## Further resources - -- [Cypress integration tests](./e2e/README.md) - [VSCode setup instructions](./dev_docs/vscode_setup.md) - [Github PR commands](./dev_docs/github_commands.md) - [Routing and Linking](./dev_docs/routing_and_linking.md) - [Telemetry](./dev_docs/telemetry.md) +- [Features flags](./dev_docs/feature_flags.md) diff --git a/x-pack/plugins/apm/scripts/test/README.md b/x-pack/plugins/apm/scripts/test/README.md index b241b2efdfd9..2b5e4212ea08 100644 --- a/x-pack/plugins/apm/scripts/test/README.md +++ b/x-pack/plugins/apm/scripts/test/README.md @@ -1,63 +1 @@ -## Unit Tests (Jest) - -``` -node scripts/tests/jest [--watch] [--updateSnapshot] -``` - -#### Coverage - -HTML coverage report can be found in target/coverage/jest after tests have run. - -``` -open target/coverage/jest/index.html -``` - ---- - -## API Tests - -API tests are separated in two suites: - -- a basic license test suite [default] -- a trial license test suite (the equivalent of gold+) - -``` -node scripts/tests/api [--trial] [--help] -``` - -The API tests are located in `x-pack/test/apm_api_integration/`. - -**API Test tips** - -- For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) -- To update snapshots append `--updateSnapshots` to the functional_test_runner command - ---- - -## E2E Tests (Cypress) - -``` -node scripts/tests/e2e [--trial] [--help] -``` - -The E2E tests are located [here](../../ftr_e2e) - ---- - -## Functional tests (Security and Correlations tests) -TODO: We could try moving this tests to the new e2e tests located at `x-pack/plugins/apm/ftr_e2e`. - -**Start server** - -``` -node scripts/functional_tests_server --config x-pack/test/functional/config.js -``` - -**Run tests** - -``` -node scripts/functional_test_runner --config x-pack/test/functional/config.js --grep='APM specs' -``` - -APM tests are located in `x-pack/test/functional/apps/apm`. -For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) +Go to [testing documentation](../../dev_docs/testing.md) \ No newline at end of file From f955947d4e66ad0be65649c31ae852c80bbefafd Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 6 Sep 2021 01:10:52 +0100 Subject: [PATCH 41/47] skip flaky suite (#110970) --- .../saved_objects/migrationsv2/test_helpers/retry.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/server/saved_objects/migrationsv2/test_helpers/retry.test.ts b/src/core/server/saved_objects/migrationsv2/test_helpers/retry.test.ts index 246f61c71ae4..ff5bf3d01c64 100644 --- a/src/core/server/saved_objects/migrationsv2/test_helpers/retry.test.ts +++ b/src/core/server/saved_objects/migrationsv2/test_helpers/retry.test.ts @@ -8,7 +8,8 @@ import { retryAsync } from './retry_async'; -describe('retry', () => { +// FLAKY: https://github.com/elastic/kibana/issues/110970 +describe.skip('retry', () => { it('retries throwing functions until they succeed', async () => { let i = 0; await expect( From 907a34076f83a0995dcec8bbaf4854c0085c0184 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 6 Sep 2021 01:17:07 +0100 Subject: [PATCH 42/47] skip failing es promotion suites (#111240) --- .../apis/uptime/rest/telemetry_collectors_fleet.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors_fleet.ts b/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors_fleet.ts index 768b65453fab..49fcdb8eba4f 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors_fleet.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors_fleet.ts @@ -14,7 +14,8 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const client = getService('es'); - describe('telemetry collectors fleet', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/111240 + describe.skip('telemetry collectors fleet', () => { before('generating data', async () => { await getService('esArchiver').load( 'x-pack/test/functional/es_archives/uptime/blank_data_stream' From 4416a31aa7bc6e619ee732b72534aac153688f75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Mon, 6 Sep 2021 09:10:04 +0200 Subject: [PATCH 43/47] [Osquery] Fix support for disabled security (#110547) --- package.json | 2 +- .../action_results/use_action_privileges.tsx | 12 --- .../osquery/public/common/page_paths.ts | 2 - .../plugins/osquery/public/components/app.tsx | 9 ++ .../osquery/public/components/empty_state.tsx | 86 +++++++++++++++++++ .../components/manage_integration_link.tsx | 16 ++-- .../public/live_queries/form/index.tsx | 16 ++-- .../public/packs/common/add_pack_query.tsx | 4 +- .../osquery/public/packs/common/pack_form.tsx | 4 +- .../public/routes/saved_queries/edit/form.tsx | 5 +- .../public/routes/saved_queries/new/form.tsx | 5 +- .../scheduled_query_groups/edit/index.tsx | 5 +- .../osquery/public/saved_queries/constants.ts | 1 + .../saved_queries/saved_query_flyout.tsx | 3 +- .../public/saved_queries/use_saved_query.ts | 3 +- .../saved_queries/use_update_saved_query.ts | 3 +- .../scheduled_query_groups/form/index.tsx | 6 +- .../queries/ecs_mapping_editor_field.tsx | 3 +- .../queries/query_flyout.tsx | 7 +- ...duled_query_group_queries_status_table.tsx | 70 ++++++++++++--- .../use_scheduled_query_group_query_errors.ts | 9 +- ...cheduled_query_group_query_last_results.ts | 84 +++++++++++------- .../privileges_check_route.ts | 32 +++---- yarn.lock | 8 +- 24 files changed, 280 insertions(+), 115 deletions(-) create mode 100644 x-pack/plugins/osquery/public/components/empty_state.tsx diff --git a/package.json b/package.json index e603190c7269..5aabfc66e463 100644 --- a/package.json +++ b/package.json @@ -348,7 +348,7 @@ "react-moment-proptypes": "^1.7.0", "react-monaco-editor": "^0.41.2", "react-popper-tooltip": "^2.10.1", - "react-query": "^3.21.0", + "react-query": "^3.21.1", "react-redux": "^7.2.0", "react-resizable": "^1.7.5", "react-resize-detector": "^4.2.0", diff --git a/x-pack/plugins/osquery/public/action_results/use_action_privileges.tsx b/x-pack/plugins/osquery/public/action_results/use_action_privileges.tsx index 2c80c874e89f..6d0477b22ede 100644 --- a/x-pack/plugins/osquery/public/action_results/use_action_privileges.tsx +++ b/x-pack/plugins/osquery/public/action_results/use_action_privileges.tsx @@ -6,28 +6,16 @@ */ import { useQuery } from 'react-query'; - -import { i18n } from '@kbn/i18n'; import { useKibana } from '../common/lib/kibana'; -import { useErrorToast } from '../common/hooks/use_error_toast'; export const useActionResultsPrivileges = () => { const { http } = useKibana().services; - const setErrorToast = useErrorToast(); return useQuery( ['actionResultsPrivileges'], () => http.get('/internal/osquery/privileges_check'), { keepPreviousData: true, - select: (response) => response?.has_all_requested ?? false, - onSuccess: () => setErrorToast(), - onError: (error: Error) => - setErrorToast(error, { - title: i18n.translate('xpack.osquery.action_results_privileges.fetchError', { - defaultMessage: 'Error while fetching action results privileges', - }), - }), } ); }; diff --git a/x-pack/plugins/osquery/public/common/page_paths.ts b/x-pack/plugins/osquery/public/common/page_paths.ts index 0e0d8310ae8b..8df1006da181 100644 --- a/x-pack/plugins/osquery/public/common/page_paths.ts +++ b/x-pack/plugins/osquery/public/common/page_paths.ts @@ -27,8 +27,6 @@ export interface DynamicPagePathValues { [key: string]: string; } -export const BASE_PATH = '/app/fleet'; - // If routing paths are changed here, please also check to see if // `pagePathGetters()`, below, needs any modifications export const PAGE_ROUTING_PATHS = { diff --git a/x-pack/plugins/osquery/public/components/app.tsx b/x-pack/plugins/osquery/public/components/app.tsx index 44407139ab49..33fb6ac6a2ad 100644 --- a/x-pack/plugins/osquery/public/components/app.tsx +++ b/x-pack/plugins/osquery/public/components/app.tsx @@ -5,6 +5,8 @@ * 2.0. */ +/* eslint-disable react-hooks/rules-of-hooks */ + import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiTabs, EuiTab } from '@elastic/eui'; @@ -14,10 +16,17 @@ import { Container, Nav, Wrapper } from './layouts'; import { OsqueryAppRoutes } from '../routes'; import { useRouterNavigate } from '../common/lib/kibana'; import { ManageIntegrationLink } from './manage_integration_link'; +import { useOsqueryIntegrationStatus } from '../common/hooks'; +import { OsqueryAppEmptyState } from './empty_state'; const OsqueryAppComponent = () => { const location = useLocation(); const section = useMemo(() => location.pathname.split('/')[1] ?? 'overview', [location.pathname]); + const { data: osqueryIntegration, isFetched } = useOsqueryIntegrationStatus(); + + if (isFetched && osqueryIntegration.install_status !== 'installed') { + return ; + } return ( diff --git a/x-pack/plugins/osquery/public/components/empty_state.tsx b/x-pack/plugins/osquery/public/components/empty_state.tsx new file mode 100644 index 000000000000..1ee0d496c0dd --- /dev/null +++ b/x-pack/plugins/osquery/public/components/empty_state.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiButton } from '@elastic/eui'; + +import { KibanaPageTemplate } from '../../../../../src/plugins/kibana_react/public'; +import { INTEGRATIONS_PLUGIN_ID } from '../../../fleet/common'; +import { pagePathGetters } from '../../../fleet/public'; +import { isModifiedEvent, isLeftClickEvent, useKibana } from '../common/lib/kibana'; +import { OsqueryIcon } from './osquery_icon'; +import { useBreadcrumbs } from '../common/hooks/use_breadcrumbs'; +import { OSQUERY_INTEGRATION_NAME } from '../../common'; + +const OsqueryAppEmptyStateComponent = () => { + useBreadcrumbs('base'); + + const { + application: { getUrlForApp, navigateToApp }, + } = useKibana().services; + + const integrationHref = useMemo(() => { + return getUrlForApp(INTEGRATIONS_PLUGIN_ID, { + path: pagePathGetters.integration_details_overview({ + pkgkey: OSQUERY_INTEGRATION_NAME, + })[1], + }); + }, [getUrlForApp]); + + const integrationClick = useCallback( + (event) => { + if (!isModifiedEvent(event) && isLeftClickEvent(event)) { + event.preventDefault(); + return navigateToApp(INTEGRATIONS_PLUGIN_ID, { + path: pagePathGetters.integration_details_overview({ + pkgkey: OSQUERY_INTEGRATION_NAME, + })[1], + }); + } + }, + [navigateToApp] + ); + + const pageHeader = useMemo( + () => ({ + iconType: OsqueryIcon, + pageTitle: ( + + ), + description: ( + + ), + rightSideItems: [ + // eslint-disable-next-line @elastic/eui/href-or-on-click + + + , + ], + }), + [integrationClick, integrationHref] + ); + + return ; +}; + +export const OsqueryAppEmptyState = React.memo(OsqueryAppEmptyStateComponent); diff --git a/x-pack/plugins/osquery/public/components/manage_integration_link.tsx b/x-pack/plugins/osquery/public/components/manage_integration_link.tsx index 44b923860e1a..32779ded46c5 100644 --- a/x-pack/plugins/osquery/public/components/manage_integration_link.tsx +++ b/x-pack/plugins/osquery/public/components/manage_integration_link.tsx @@ -24,11 +24,9 @@ const ManageIntegrationLinkComponent = () => { const integrationHref = useMemo(() => { if (osqueryIntegration) { return getUrlForApp(INTEGRATIONS_PLUGIN_ID, { - path: - '#' + - pagePathGetters.integration_details_policies({ - pkgkey: `${osqueryIntegration.name}-${osqueryIntegration.version}`, - })[1], + path: pagePathGetters.integration_details_policies({ + pkgkey: `${osqueryIntegration.name}-${osqueryIntegration.version}`, + })[1], }); } }, [getUrlForApp, osqueryIntegration]); @@ -39,11 +37,9 @@ const ManageIntegrationLinkComponent = () => { event.preventDefault(); if (osqueryIntegration) { return navigateToApp(INTEGRATIONS_PLUGIN_ID, { - path: - '#' + - pagePathGetters.integration_details_policies({ - pkgkey: `${osqueryIntegration.name}-${osqueryIntegration.version}`, - })[1], + path: pagePathGetters.integration_details_policies({ + pkgkey: `${osqueryIntegration.name}-${osqueryIntegration.version}`, + })[1], }); } } diff --git a/x-pack/plugins/osquery/public/live_queries/form/index.tsx b/x-pack/plugins/osquery/public/live_queries/form/index.tsx index 987be904c87e..69b02dee8b9f 100644 --- a/x-pack/plugins/osquery/public/live_queries/form/index.tsx +++ b/x-pack/plugins/osquery/public/live_queries/form/index.tsx @@ -114,7 +114,7 @@ const LiveQueryFormComponent: React.FC = ({ ), }); - const { setFieldValue, submit } = form; + const { setFieldValue, submit, isSubmitting } = form; const actionId = useMemo(() => data?.actions[0].action_id, [data?.actions]); const agentIds = useMemo(() => data?.actions[0].agents, [data?.actions]); @@ -185,7 +185,10 @@ const LiveQueryFormComponent: React.FC = ({ )} - + = ({ ), [ - agentSelected, - permissions.writeSavedQueries, - handleShowSaveQueryFlout, queryComponentProps, + singleAgentMode, + permissions.writeSavedQueries, + agentSelected, queryValueProvided, resultsStatus, - singleAgentMode, + handleShowSaveQueryFlout, + isSubmitting, submit, ] ); diff --git a/x-pack/plugins/osquery/public/packs/common/add_pack_query.tsx b/x-pack/plugins/osquery/public/packs/common/add_pack_query.tsx index 2d58e2dfe952..d1115898b4e4 100644 --- a/x-pack/plugins/osquery/public/packs/common/add_pack_query.tsx +++ b/x-pack/plugins/osquery/public/packs/common/add_pack_query.tsx @@ -51,7 +51,7 @@ const AddPackQueryFormComponent = ({ handleSubmit }) => { }, }, }); - const { submit } = form; + const { submit, isSubmitting } = form; const createSavedQueryMutation = useMutation( (payload) => http.post(`/internal/osquery/saved_query`, { body: JSON.stringify(payload) }), @@ -108,7 +108,7 @@ const AddPackQueryFormComponent = ({ handleSubmit }) => { - + {'Add query'} diff --git a/x-pack/plugins/osquery/public/packs/common/pack_form.tsx b/x-pack/plugins/osquery/public/packs/common/pack_form.tsx index 86d4d8dff6ba..ab0984e80894 100644 --- a/x-pack/plugins/osquery/public/packs/common/pack_form.tsx +++ b/x-pack/plugins/osquery/public/packs/common/pack_form.tsx @@ -40,7 +40,7 @@ const PackFormComponent = ({ data, handleSubmit }) => { }, }, }); - const { submit } = form; + const { submit, isSubmitting } = form; return (

@@ -50,7 +50,7 @@ const PackFormComponent = ({ data, handleSubmit }) => { - + {'Save pack'} diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/edit/form.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/edit/form.tsx index a7596575b90c..617d83821d08 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/edit/form.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/edit/form.tsx @@ -38,6 +38,7 @@ const EditSavedQueryFormComponent: React.FC = ({ defaultValue, handleSubmit, }); + const { submit, isSubmitting } = form; return (
@@ -58,12 +59,12 @@ const EditSavedQueryFormComponent: React.FC = ({ = ({ defaultValue, handleSubmit, }); + const { submit, isSubmitting } = form; return ( @@ -54,12 +55,12 @@ const NewSavedQueryFormComponent: React.FC = ({ { const { data } = useScheduledQueryGroup({ scheduledQueryGroupId }); - useBreadcrumbs('scheduled_query_group_edit', { scheduledQueryGroupName: data?.name ?? '' }); + useBreadcrumbs('scheduled_query_group_edit', { + scheduledQueryGroupId: data?.id ?? '', + scheduledQueryGroupName: data?.name ?? '', + }); const LeftColumn = useMemo( () => ( diff --git a/x-pack/plugins/osquery/public/saved_queries/constants.ts b/x-pack/plugins/osquery/public/saved_queries/constants.ts index 69ca805e3e8f..8edcfd00d178 100644 --- a/x-pack/plugins/osquery/public/saved_queries/constants.ts +++ b/x-pack/plugins/osquery/public/saved_queries/constants.ts @@ -6,3 +6,4 @@ */ export const SAVED_QUERIES_ID = 'savedQueryList'; +export const SAVED_QUERY_ID = 'savedQuery'; diff --git a/x-pack/plugins/osquery/public/saved_queries/saved_query_flyout.tsx b/x-pack/plugins/osquery/public/saved_queries/saved_query_flyout.tsx index 6d14943a6bc8..8c35a359a9ba 100644 --- a/x-pack/plugins/osquery/public/saved_queries/saved_query_flyout.tsx +++ b/x-pack/plugins/osquery/public/saved_queries/saved_query_flyout.tsx @@ -42,6 +42,7 @@ const SavedQueryFlyoutComponent: React.FC = ({ defaultValue defaultValue, handleSubmit, }); + const { submit, isSubmitting } = form; return ( @@ -72,7 +73,7 @@ const SavedQueryFlyoutComponent: React.FC = ({ defaultValue - + { queryClient.invalidateQueries(SAVED_QUERIES_ID); + queryClient.invalidateQueries([SAVED_QUERY_ID, { savedQueryId }]); navigateToApp(PLUGIN_ID, { path: pagePathGetters.saved_queries() }); toasts.addSuccess( i18n.translate('xpack.osquery.editSavedQuery.successToastMessageText', { diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx index 685960ecd202..3598a9fd2e44 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx @@ -88,7 +88,7 @@ const ScheduledQueryGroupFormComponent: React.FC = `scheduled_query_groups/${editMode ? defaultValue?.id : ''}` ); - const { isLoading, mutateAsync } = useMutation( + const { mutateAsync } = useMutation( (payload: Record) => editMode && defaultValue?.id ? http.put(packagePolicyRouteService.getUpdatePath(defaultValue.id), { @@ -248,7 +248,7 @@ const ScheduledQueryGroupFormComponent: React.FC = ), }); - const { setFieldValue, submit } = form; + const { setFieldValue, submit, isSubmitting } = form; const policyIdEuiFieldProps = useMemo( () => ({ isDisabled: !!defaultValue, options: agentPolicyOptions }), @@ -368,7 +368,7 @@ const ScheduledQueryGroupFormComponent: React.FC = ( ) )(args); - if (fieldRequiredError && (!!(!editForm && args.formData.value?.field.length) || editForm)) { + // @ts-expect-error update types + if (fieldRequiredError && ((!editForm && args.formData['value.field'].length) || editForm)) { return fieldRequiredError; } diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx index cae9711694f2..d38c1b2118f2 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { isEmpty } from 'lodash'; import { EuiCallOut, EuiFlyout, @@ -66,7 +67,7 @@ const QueryFlyoutComponent: React.FC = ({ if (isValid && ecsFieldValue) { onSave({ ...payload, - ecs_mapping: ecsFieldValue, + ...(isEmpty(ecsFieldValue) ? {} : { ecs_mapping: ecsFieldValue }), }); onClose(); } @@ -81,7 +82,7 @@ const QueryFlyoutComponent: React.FC = ({ [integrationPackageVersion] ); - const { submit, setFieldValue, reset } = form; + const { submit, setFieldValue, reset, isSubmitting } = form; const [{ query }] = useFormData({ form, @@ -245,7 +246,7 @@ const QueryFlyoutComponent: React.FC = ({ - + = ({ toggleErrors, expanded, }) => { + const data = useKibana().services.data; + const [logsIndexPattern, setLogsIndexPattern] = useState(undefined); + const { data: lastResultsData, isFetched } = useScheduledQueryGroupQueryLastResults({ actionId, agentIds, interval, + logsIndexPattern, }); const { data: errorsData, isFetched: errorsFetched } = useScheduledQueryGroupQueryErrors({ actionId, agentIds, interval, + logsIndexPattern, }); const handleErrorsToggle = useCallback(() => toggleErrors({ queryId, interval }), [ @@ -409,20 +414,41 @@ const ScheduledQueryLastResults: React.FC = ({ toggleErrors, ]); + useEffect(() => { + const fetchLogsIndexPattern = async () => { + const indexPattern = await data.indexPatterns.find('logs-*'); + + setLogsIndexPattern(indexPattern[0]); + }; + fetchLogsIndexPattern(); + }, [data.indexPatterns]); + if (!isFetched || !errorsFetched) { return ; } - if (!lastResultsData) { + if (!lastResultsData && !errorsData?.total) { return <>{'-'}; } return ( - {lastResultsData.first_event_ingested_time?.value ? ( - - <>{moment(lastResultsData.first_event_ingested_time?.value).fromNow()} + {lastResultsData?.['@timestamp'] ? ( + + {' '} + + + } + > + ) : ( '-' @@ -432,10 +458,17 @@ const ScheduledQueryLastResults: React.FC = ({ - {lastResultsData?.doc_count ?? 0} + {lastResultsData?.docCount ?? 0} - {'Documents'} + + + @@ -443,10 +476,17 @@ const ScheduledQueryLastResults: React.FC = ({ - {lastResultsData?.unique_agents?.value ?? 0} + {lastResultsData?.uniqueAgentsCount ?? 0} - {'Agents'} + + + @@ -458,7 +498,15 @@ const ScheduledQueryLastResults: React.FC = ({ - {'Errors'} + + {' '} + + { const data = useKibana().services.data; @@ -28,9 +30,8 @@ export const useScheduledQueryGroupQueryErrors = ({ return useQuery( ['scheduledQueryErrors', { actionId, interval }], async () => { - const indexPattern = await data.indexPatterns.find('logs-*'); const searchSource = await data.search.searchSource.create({ - index: indexPattern[0], + index: logsIndexPattern, fields: ['*'], sort: [ { @@ -80,7 +81,7 @@ export const useScheduledQueryGroupQueryErrors = ({ }, { keepPreviousData: true, - enabled: !!(!skip && actionId && interval && agentIds?.length), + enabled: !!(!skip && actionId && interval && agentIds?.length && logsIndexPattern), select: (response) => response.rawResponse.hits ?? [], refetchOnReconnect: false, refetchOnWindowFocus: false, diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group_query_last_results.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group_query_last_results.ts index f972640e2598..7cfd6be461e0 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group_query_last_results.ts +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group_query_last_results.ts @@ -6,13 +6,14 @@ */ import { useQuery } from 'react-query'; - +import { IndexPattern } from '../../../../../src/plugins/data/common'; import { useKibana } from '../common/lib/kibana'; interface UseScheduledQueryGroupQueryLastResultsProps { actionId: string; agentIds?: string[]; interval: number; + logsIndexPattern?: IndexPattern; skip?: boolean; } @@ -20,6 +21,7 @@ export const useScheduledQueryGroupQueryLastResults = ({ actionId, agentIds, interval, + logsIndexPattern, skip = false, }: UseScheduledQueryGroupQueryLastResultsProps) => { const data = useKibana().services.data; @@ -27,23 +29,9 @@ export const useScheduledQueryGroupQueryLastResults = ({ return useQuery( ['scheduledQueryLastResults', { actionId }], async () => { - const indexPattern = await data.indexPatterns.find('logs-*'); - const searchSource = await data.search.searchSource.create({ - index: indexPattern[0], - size: 0, - aggs: { - runs: { - terms: { - field: 'response_id', - order: { first_event_ingested_time: 'desc' }, - size: 1, - }, - aggs: { - first_event_ingested_time: { min: { field: '@timestamp' } }, - unique_agents: { cardinality: { field: 'agent.id' } }, - }, - }, - }, + const lastResultsSearchSource = await data.search.searchSource.create({ + index: logsIndexPattern, + size: 1, query: { // @ts-expect-error update types bool: { @@ -59,26 +47,62 @@ export const useScheduledQueryGroupQueryLastResults = ({ action_id: actionId, }, }, - { - range: { - '@timestamp': { - gte: `now-${interval * 2}s`, - lte: 'now', - }, - }, - }, ], }, }, }); - return searchSource.fetch$().toPromise(); + const lastResultsResponse = await lastResultsSearchSource.fetch$().toPromise(); + + const responseId = lastResultsResponse.rawResponse?.hits?.hits[0]?._source?.response_id; + + if (responseId) { + const aggsSearchSource = await data.search.searchSource.create({ + index: logsIndexPattern, + size: 0, + aggs: { + unique_agents: { cardinality: { field: 'agent.id' } }, + }, + query: { + // @ts-expect-error update types + bool: { + should: agentIds?.map((agentId) => ({ + match_phrase: { + 'agent.id': agentId, + }, + })), + minimum_should_match: 1, + filter: [ + { + match_phrase: { + action_id: actionId, + }, + }, + { + match_phrase: { + response_id: responseId, + }, + }, + ], + }, + }, + }); + + const aggsResponse = await aggsSearchSource.fetch$().toPromise(); + + return { + '@timestamp': lastResultsResponse.rawResponse?.hits?.hits[0]?.fields?.['@timestamp'], + // @ts-expect-error update types + uniqueAgentsCount: aggsResponse.rawResponse.aggregations?.unique_agents?.value, + docCount: aggsResponse.rawResponse?.hits?.total, + }; + } + + return null; }, { keepPreviousData: true, - enabled: !!(!skip && actionId && interval && agentIds?.length), - // @ts-expect-error update types - select: (response) => response.rawResponse.aggregations?.runs?.buckets[0] ?? [], + enabled: !!(!skip && actionId && interval && agentIds?.length && logsIndexPattern), refetchOnReconnect: false, refetchOnWindowFocus: false, } diff --git a/x-pack/plugins/osquery/server/routes/privileges_check/privileges_check_route.ts b/x-pack/plugins/osquery/server/routes/privileges_check/privileges_check_route.ts index 80c335c1c46d..d9683d23deb1 100644 --- a/x-pack/plugins/osquery/server/routes/privileges_check/privileges_check_route.ts +++ b/x-pack/plugins/osquery/server/routes/privileges_check/privileges_check_route.ts @@ -9,7 +9,6 @@ import { OSQUERY_INTEGRATION_NAME, PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; -// eslint-disable-next-line @typescript-eslint/no-unused-vars export const privilegesCheckRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { router.get( { @@ -20,23 +19,26 @@ export const privilegesCheckRoute = (router: IRouter, osqueryContext: OsqueryApp }, }, async (context, request, response) => { - const esClient = context.core.elasticsearch.client.asCurrentUser; - - const privileges = ( - await esClient.security.hasPrivileges({ - body: { - index: [ - { - names: [`logs-${OSQUERY_INTEGRATION_NAME}.result*`], - privileges: ['read'], - }, - ], + if (osqueryContext.security.authz.mode.useRbacForRequest(request)) { + const checkPrivileges = osqueryContext.security.authz.checkPrivilegesDynamicallyWithRequest( + request + ); + const { hasAllRequested } = await checkPrivileges({ + elasticsearch: { + cluster: [], + index: { + [`logs-${OSQUERY_INTEGRATION_NAME}.result*`]: ['read'], + }, }, - }) - ).body; + }); + + return response.ok({ + body: `${hasAllRequested}`, + }); + } return response.ok({ - body: privileges, + body: 'true', }); } ); diff --git a/yarn.lock b/yarn.lock index 4d49a2f06e1e..f0a1ff1278f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23379,10 +23379,10 @@ react-popper@^2.2.4: react-fast-compare "^3.0.1" warning "^4.0.2" -react-query@^3.21.0: - version "3.21.0" - resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.21.0.tgz#2e099a7906c38eeeb750e8b9b12121a21fa8d9ef" - integrity sha512-5rY5J8OD9f4EdkytjSsdCO+pqbJWKwSIMETfh/UyxqyjLURHE0IhlB+IPNPrzzu/dzK0rRxi5p0IkcCdSfizDQ== +react-query@^3.21.1: + version "3.21.1" + resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.21.1.tgz#8fe4df90bf6c6a93e0552ea9baff211d1b28f6e0" + integrity sha512-aKFLfNJc/m21JBXJk7sR9tDUYPjotWA4EHAKvbZ++GgxaY+eI0tqBxXmGBuJo0Pisis1W4pZWlZgoRv9yE8yjA== dependencies: "@babel/runtime" "^7.5.5" broadcast-channel "^3.4.1" From eef094bafb4ce265126495754f8cb1cf760bb614 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Mon, 6 Sep 2021 11:13:38 +0300 Subject: [PATCH 44/47] [Canvas] `TagCloud` (#106858) * Added `tagCloud` to canvas. * Added `icon` to the `tagCloud` element. * Added column name support at `tag_cloud`. * Added condition to `vis_dimension` not to pass invalid index. Added check of accessor index, if such column exists at vis_dimension. Removed checks of column existance from TagCloudChart. Added test for accessing data by column name in addition to a column number. Updated tag_cloud element in Canvas. Fixed types. Removed almost all `any` and `as` types. * Added test suites for `vis_dimension` function. * Added tests for DatatableColumn accessors at tag_cloud_fn and to_ast. * Refactored metrics, tagcloud and tests. Added valid functional tests to metrics and tag_cloud. Fixed types of metrics_vis. Added handling of empty data at tag_cloud renderer. * Added storybook ( still doesn't work ). * Fixed some mistakes. * Added working storybook with mocks. * Added clear storybook for tag_cloud_vis_renderer. * Updated the location of vis_dimension test after movement of the function. * Fixed unused type. * Fixed tests and added handling of the column name at `visualizations/**/*/prepare_log_table.ts` * Reduced the complexity of checking the accessor at `tag_cloud_chart.tsx` * Added comments at unclear places of code. * Added the logic for disabling elements for renderers from disabled plugins. * removed garbage from `kibana.yml`. * Fixed element_strings.test error. * Made changes, based on nits. * Fixed mistake. * Removed `disabled` flag for `expression_*` plugins. * recovered lost comments at the unclear places. * removed dead code. * fixed test errors. * Fixed test error, I hope. * fixed more tests. * fixed code, based on nits. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/dev/storybook/aliases.ts | 1 + .../expression_tagcloud/.storybook/main.js | 23 +++ .../tagcloud_function.test.ts.snap | 102 +++++++++++- .../tagcloud_function.test.ts | 63 ++++++- .../expression_functions/tagcloud_function.ts | 6 +- .../common/types/expression_functions.ts | 17 +- .../public/__mocks__/format_service.ts | 13 ++ .../public/__mocks__/palettes.ts | 52 ++++++ .../__stories__/tagcloud_renderer.stories.tsx | 125 ++++++++++++++ .../public/components/tag_cloud.scss | 2 + .../components/tagcloud_component.test.tsx | 155 +++++++++++------- .../public/components/tagcloud_component.tsx | 44 +++-- .../tagcloud_renderer.tsx | 44 +++-- .../public/{services.ts => format_service.ts} | 0 .../expression_tagcloud/public/plugin.ts | 2 +- .../components/metric_vis_component.tsx | 19 ++- .../vis_types/metric/public/metric_vis_fn.ts | 21 +-- src/plugins/vis_types/metric/public/types.ts | 6 +- .../public/__snapshots__/to_ast.test.ts.snap | 104 +++++++++++- .../vis_types/tagcloud/public/to_ast.test.ts | 57 ++++++- .../vis_types/tagcloud/public/types.ts | 14 +- .../vis_dimension.test.ts | 70 ++++++++ .../expression_functions/vis_dimension.ts | 14 +- .../common/prepare_log_table.test.ts | 5 +- .../common/prepare_log_table.ts | 23 ++- .../screenshots/baseline/metric_all_data.png | Bin 22339 -> 29776 bytes .../baseline/metric_empty_data.png | Bin 0 -> 5163 bytes .../baseline/metric_invalid_data.png | Bin 3763 -> 1993 bytes .../baseline/tagcloud_empty_data.png | Bin 0 -> 4467 bytes .../metric_empty_data.json} | 2 +- .../baseline/metric_invalid_data.json | 2 +- .../tagcloud_empty_data.json} | 2 +- .../baseline/tagcloud_invalid_data.json | 2 +- .../session/tagcloud_empty_data.json | 1 + .../session/tagcloud_invalid_data.json | 2 +- .../test_suites/run_pipeline/metric.ts | 16 +- .../test_suites/run_pipeline/tag_cloud.ts | 34 ++-- .../canvas_plugin_src/elements/index.ts | 3 +- .../elements/tag_cloud/index.ts | 21 +++ .../canvas/i18n/elements/element_strings.ts | 8 + x-pack/plugins/canvas/public/plugin.tsx | 1 + .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 43 files changed, 891 insertions(+), 187 deletions(-) create mode 100644 src/plugins/chart_expressions/expression_tagcloud/.storybook/main.js create mode 100644 src/plugins/chart_expressions/expression_tagcloud/public/__mocks__/format_service.ts create mode 100644 src/plugins/chart_expressions/expression_tagcloud/public/__mocks__/palettes.ts create mode 100644 src/plugins/chart_expressions/expression_tagcloud/public/__stories__/tagcloud_renderer.stories.tsx rename src/plugins/chart_expressions/expression_tagcloud/public/{services.ts => format_service.ts} (100%) create mode 100644 src/plugins/visualizations/common/expression_functions/vis_dimension.test.ts create mode 100644 test/interpreter_functional/screenshots/baseline/metric_empty_data.png create mode 100644 test/interpreter_functional/screenshots/baseline/tagcloud_empty_data.png rename test/interpreter_functional/snapshots/{session/metric_single_metric_data.json => baseline/metric_empty_data.json} (89%) rename test/interpreter_functional/snapshots/{session/partial_test_1.json => baseline/tagcloud_empty_data.json} (65%) create mode 100644 test/interpreter_functional/snapshots/session/tagcloud_empty_data.json create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/elements/tag_cloud/index.ts diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 965a716098f3..9395c5fdf883 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -23,6 +23,7 @@ export const storybookAliases = { expression_repeat_image: 'src/plugins/expression_repeat_image/.storybook', expression_reveal_image: 'src/plugins/expression_reveal_image/.storybook', expression_shape: 'src/plugins/expression_shape/.storybook', + expression_tagcloud: 'src/plugins/chart_expressions/expression_tagcloud/.storybook', infra: 'x-pack/plugins/infra/.storybook', security_solution: 'x-pack/plugins/security_solution/.storybook', ui_actions_enhanced: 'x-pack/plugins/ui_actions_enhanced/.storybook', diff --git a/src/plugins/chart_expressions/expression_tagcloud/.storybook/main.js b/src/plugins/chart_expressions/expression_tagcloud/.storybook/main.js new file mode 100644 index 000000000000..cb483d539428 --- /dev/null +++ b/src/plugins/chart_expressions/expression_tagcloud/.storybook/main.js @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { defaultConfig } from '@kbn/storybook'; +import webpackMerge from 'webpack-merge'; +import { resolve } from 'path'; + +const mockConfig = { + resolve: { + alias: { + '../format_service': resolve(__dirname, '../public/__mocks__/format_service.ts'), + }, + }, +}; + +module.exports = { + ...defaultConfig, + webpackFinal: (config) => webpackMerge(config, mockConfig), +}; diff --git a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/__snapshots__/tagcloud_function.test.ts.snap b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/__snapshots__/tagcloud_function.test.ts.snap index 56b24f0ae004..da116bc50f37 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/__snapshots__/tagcloud_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/__snapshots__/tagcloud_function.test.ts.snap @@ -4,23 +4,35 @@ exports[`interpreter/functions#tagcloud logs correct datatable to inspector 1`] Object { "columns": Array [ Object { - "id": "col-0-1", + "id": "Count", "meta": Object { "dimensionName": "Tag size", }, "name": "Count", }, + Object { + "id": "country", + "meta": Object { + "dimensionName": "Tags", + }, + "name": "country", + }, ], "rows": Array [ Object { - "col-0-1": 0, + "Count": 0, + "country": "US", + }, + Object { + "Count": 10, + "country": "UK", }, ], "type": "datatable", } `; -exports[`interpreter/functions#tagcloud returns an object with the correct structure 1`] = ` +exports[`interpreter/functions#tagcloud returns an object with the correct structure for number accessors 1`] = ` Object { "as": "tagcloud", "type": "render", @@ -29,13 +41,22 @@ Object { "visData": Object { "columns": Array [ Object { - "id": "col-0-1", + "id": "Count", "name": "Count", }, + Object { + "id": "country", + "name": "country", + }, ], "rows": Array [ Object { - "col-0-1": 0, + "Count": 0, + "country": "US", + }, + Object { + "Count": 10, + "country": "UK", }, ], "type": "datatable", @@ -43,16 +64,81 @@ Object { "visParams": Object { "bucket": Object { "accessor": 1, + }, + "maxFontSize": 72, + "metric": Object { + "accessor": 0, + }, + "minFontSize": 18, + "orientation": "single", + "palette": Object { + "name": "default", + "type": "palette", + }, + "scale": "linear", + "showLabel": true, + }, + "visType": "tagcloud", + }, +} +`; + +exports[`interpreter/functions#tagcloud returns an object with the correct structure for string accessors 1`] = ` +Object { + "as": "tagcloud", + "type": "render", + "value": Object { + "syncColors": false, + "visData": Object { + "columns": Array [ + Object { + "id": "Count", + "name": "Count", + }, + Object { + "id": "country", + "name": "country", + }, + ], + "rows": Array [ + Object { + "Count": 0, + "country": "US", + }, + Object { + "Count": 10, + "country": "UK", + }, + ], + "type": "datatable", + }, + "visParams": Object { + "bucket": Object { + "accessor": Object { + "id": "country", + "meta": Object { + "type": "string", + }, + "name": "country", + }, "format": Object { - "id": "number", + "params": Object {}, }, + "type": "vis_dimension", }, "maxFontSize": 72, "metric": Object { - "accessor": 0, + "accessor": Object { + "id": "Count", + "meta": Object { + "type": "number", + }, + "name": "Count", + }, "format": Object { - "id": "number", + "params": Object {}, }, + "type": "vis_dimension", }, "minFontSize": 18, "orientation": "single", diff --git a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.test.ts b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.test.ts index 2c6e021b5107..8abdc36704b4 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.test.ts +++ b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.test.ts @@ -9,14 +9,23 @@ import { tagcloudFunction } from './tagcloud_function'; import { functionWrapper } from '../../../../expressions/common/expression_functions/specs/tests/utils'; +import { ExpressionValueVisDimension } from '../../../../visualizations/public'; import { Datatable } from '../../../../expressions/common/expression_types/specs'; describe('interpreter/functions#tagcloud', () => { const fn = functionWrapper(tagcloudFunction()); + const column1 = 'Count'; + const column2 = 'country'; const context = { type: 'datatable', - rows: [{ 'col-0-1': 0 }], - columns: [{ id: 'col-0-1', name: 'Count' }], + columns: [ + { id: column1, name: column1 }, + { id: column2, name: column2 }, + ], + rows: [ + { [column1]: 0, [column2]: 'US' }, + { [column1]: 10, [column2]: 'UK' }, + ], }; const visConfig = { scale: 'linear', @@ -24,12 +33,52 @@ describe('interpreter/functions#tagcloud', () => { minFontSize: 18, maxFontSize: 72, showLabel: true, - metric: { accessor: 0, format: { id: 'number' } }, - bucket: { accessor: 1, format: { id: 'number' } }, }; - it('returns an object with the correct structure', () => { - const actual = fn(context, visConfig, undefined); + const numberAccessors = { + metric: { accessor: 0 }, + bucket: { accessor: 1 }, + }; + + const stringAccessors: { + metric: ExpressionValueVisDimension; + bucket: ExpressionValueVisDimension; + } = { + metric: { + type: 'vis_dimension', + accessor: { + id: column1, + name: column1, + meta: { + type: 'number', + }, + }, + format: { + params: {}, + }, + }, + bucket: { + type: 'vis_dimension', + accessor: { + id: column2, + name: column2, + meta: { + type: 'string', + }, + }, + format: { + params: {}, + }, + }, + }; + + it('returns an object with the correct structure for number accessors', () => { + const actual = fn(context, { ...visConfig, ...numberAccessors }, undefined); + expect(actual).toMatchSnapshot(); + }); + + it('returns an object with the correct structure for string accessors', () => { + const actual = fn(context, { ...visConfig, ...stringAccessors }, undefined); expect(actual).toMatchSnapshot(); }); @@ -44,7 +93,7 @@ describe('interpreter/functions#tagcloud', () => { }, }, }; - await fn(context, visConfig, handlers as any); + await fn(context, { ...visConfig, ...numberAccessors }, handlers as any); expect(loggedTable!).toMatchSnapshot(); }); diff --git a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.ts b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.ts index c3553c4660ce..2ce50e94aeda 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.ts +++ b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { prepareLogTable, Dimension } from '../../../../visualizations/common/prepare_log_table'; -import { TagCloudVisParams } from '../types'; +import { TagCloudRendererParams } from '../types'; import { ExpressionTagcloudFunction } from '../types'; import { EXPRESSION_NAME } from '../constants'; @@ -125,7 +125,7 @@ export const tagcloudFunction: ExpressionTagcloudFunction = () => { }, }, fn(input, args, handlers) { - const visParams = { + const visParams: TagCloudRendererParams = { scale: args.scale, orientation: args.orientation, minFontSize: args.minFontSize, @@ -139,7 +139,7 @@ export const tagcloudFunction: ExpressionTagcloudFunction = () => { type: 'palette', name: args.palette, }, - } as TagCloudVisParams; + }; if (handlers?.inspectorAdapters?.tables) { const argsTable: Dimension[] = [[[args.metric], dimension.tagSize]]; diff --git a/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts index b1aba30380b5..1ee0434e1603 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts @@ -10,19 +10,10 @@ import { Datatable, ExpressionFunctionDefinition, ExpressionValueRender, - SerializedFieldFormat, } from '../../../../expressions'; import { ExpressionValueVisDimension } from '../../../../visualizations/common'; import { EXPRESSION_NAME } from '../constants'; -interface Dimension { - accessor: number; - format: { - id?: string; - params?: SerializedFieldFormat; - }; -} - interface TagCloudCommonParams { scale: 'linear' | 'log' | 'square root'; orientation: 'single' | 'right angled' | 'multiple'; @@ -36,16 +27,16 @@ export interface TagCloudVisConfig extends TagCloudCommonParams { bucket?: ExpressionValueVisDimension; } -export interface TagCloudVisParams extends TagCloudCommonParams { +export interface TagCloudRendererParams extends TagCloudCommonParams { palette: PaletteOutput; - metric: Dimension; - bucket?: Dimension; + metric: ExpressionValueVisDimension; + bucket?: ExpressionValueVisDimension; } export interface TagcloudRendererConfig { visType: typeof EXPRESSION_NAME; visData: Datatable; - visParams: TagCloudVisParams; + visParams: TagCloudRendererParams; syncColors: boolean; } diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/__mocks__/format_service.ts b/src/plugins/chart_expressions/expression_tagcloud/public/__mocks__/format_service.ts new file mode 100644 index 000000000000..77f6d8eb0bf3 --- /dev/null +++ b/src/plugins/chart_expressions/expression_tagcloud/public/__mocks__/format_service.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const getFormatService = () => ({ + deserialize: (target: any) => ({ + convert: (text: string, format: string) => text, + }), +}); diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/__mocks__/palettes.ts b/src/plugins/chart_expressions/expression_tagcloud/public/__mocks__/palettes.ts new file mode 100644 index 000000000000..7ca00b58f562 --- /dev/null +++ b/src/plugins/chart_expressions/expression_tagcloud/public/__mocks__/palettes.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PaletteDefinition, SeriesLayer } from '../../../../charts/public'; +import { random } from 'lodash'; + +export const getPaletteRegistry = () => { + const colors = [ + '#54B399', + '#6092C0', + '#D36086', + '#9170B8', + '#CA8EAE', + '#D6BF57', + '#B9A888', + '#DA8B45', + '#AA6556', + '#E7664C', + ]; + const mockPalette: PaletteDefinition = { + id: 'default', + title: 'My Palette', + getCategoricalColor: (_: SeriesLayer[]) => colors[random(0, colors.length - 1)], + getCategoricalColors: (num: number) => colors, + toExpression: () => ({ + type: 'expression', + chain: [ + { + type: 'function', + function: 'system_palette', + arguments: { + name: ['default'], + }, + }, + ], + }), + }; + + return { + get: (name: string) => mockPalette, + getAll: () => [mockPalette], + }; +}; + +export const palettes = { + getPalettes: async () => getPaletteRegistry(), +}; diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/__stories__/tagcloud_renderer.stories.tsx b/src/plugins/chart_expressions/expression_tagcloud/public/__stories__/tagcloud_renderer.stories.tsx new file mode 100644 index 000000000000..1e0dc2600d1a --- /dev/null +++ b/src/plugins/chart_expressions/expression_tagcloud/public/__stories__/tagcloud_renderer.stories.tsx @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { tagcloudRenderer } from '../expression_renderers'; +import { Render } from '../../../../presentation_util/public/__stories__'; +import { TagcloudRendererConfig } from '../../common/types'; +import { palettes } from '../__mocks__/palettes'; + +const config: TagcloudRendererConfig = { + visType: 'tagcloud', + visData: { + type: 'datatable', + rows: [ + { country: 'US', Count: 14 }, + { country: 'JP', Count: 13 }, + { country: 'UK', Count: 13 }, + { country: 'CN', Count: 8 }, + { country: 'TZ', Count: 14 }, + { country: 'NL', Count: 11 }, + { country: 'AZ', Count: 14 }, + { country: 'BR', Count: 11 }, + { country: 'DE', Count: 16 }, + { country: 'SA', Count: 11 }, + { country: 'RU', Count: 9 }, + { country: 'IN', Count: 9 }, + { country: 'PH', Count: 7 }, + ], + columns: [ + { id: 'country', name: 'country', meta: { type: 'string' } }, + { id: 'Count', name: 'Count', meta: { type: 'number' } }, + ], + }, + visParams: { + scale: 'linear', + orientation: 'single', + minFontSize: 18, + maxFontSize: 72, + showLabel: true, + metric: { + type: 'vis_dimension', + accessor: { id: 'Count', name: 'Count', meta: { type: 'number' } }, + format: { id: 'string', params: {} }, + }, + bucket: { + type: 'vis_dimension', + accessor: { id: 'country', name: 'country', meta: { type: 'string' } }, + format: { id: 'string', params: {} }, + }, + palette: { type: 'palette', name: 'default' }, + }, + syncColors: false, +}; + +const containerSize = { + width: '700px', + height: '700px', +}; + +storiesOf('renderers/tag_cloud_vis', module) + .add('Default', () => { + return ( + tagcloudRenderer({ palettes })} config={config} {...containerSize} /> + ); + }) + .add('With log scale', () => { + return ( + tagcloudRenderer({ palettes })} + config={{ ...config, visParams: { ...config.visParams, scale: 'log' } }} + {...containerSize} + /> + ); + }) + .add('With square root scale', () => { + return ( + tagcloudRenderer({ palettes })} + config={{ ...config, visParams: { ...config.visParams, scale: 'square root' } }} + {...containerSize} + /> + ); + }) + .add('With right angled orientation', () => { + return ( + tagcloudRenderer({ palettes })} + config={{ ...config, visParams: { ...config.visParams, orientation: 'right angled' } }} + {...containerSize} + /> + ); + }) + .add('With multiple orientations', () => { + return ( + tagcloudRenderer({ palettes })} + config={{ ...config, visParams: { ...config.visParams, orientation: 'multiple' } }} + {...containerSize} + /> + ); + }) + .add('With hidden label', () => { + return ( + tagcloudRenderer({ palettes })} + config={{ ...config, visParams: { ...config.visParams, showLabel: false } }} + {...containerSize} + /> + ); + }) + .add('With empty results', () => { + return ( + tagcloudRenderer({ palettes })} + config={{ ...config, visData: { ...config.visData, rows: [] } }} + {...containerSize} + /> + ); + }); diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/components/tag_cloud.scss b/src/plugins/chart_expressions/expression_tagcloud/public/components/tag_cloud.scss index 51b5e9dedd84..8a017150fe19 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/public/components/tag_cloud.scss +++ b/src/plugins/chart_expressions/expression_tagcloud/public/components/tag_cloud.scss @@ -9,6 +9,8 @@ flex: 1 1 0; display: flex; flex-direction: column; + // it is used for rendering at `Canvas`. + height: 100%; } .tgcChart__wrapper text { diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.test.tsx b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.test.tsx index 542a9c1cd9bf..f65630e422cc 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.test.tsx +++ b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.test.tsx @@ -6,15 +6,15 @@ * Side Public License, v 1. */ import React from 'react'; -import { Wordcloud, Settings } from '@elastic/charts'; +import { Wordcloud, Settings, WordcloudSpec } from '@elastic/charts'; import { chartPluginMock } from '../../../../charts/public/mocks'; import type { Datatable } from '../../../../expressions/public'; import { mount } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; import TagCloudChart, { TagCloudChartProps } from './tagcloud_component'; -import { TagCloudVisParams } from '../../common/types'; +import { TagCloudRendererParams } from '../../common/types'; -jest.mock('../services', () => ({ +jest.mock('../format_service', () => ({ getFormatService: jest.fn(() => { return { deserialize: jest.fn(), @@ -23,29 +23,34 @@ jest.mock('../services', () => ({ })); const palettesRegistry = chartPluginMock.createPaletteRegistry(); -const visData = ({ +const geoDestId = 'geo.dest'; +const countId = 'Count'; +const visData: Datatable = { + type: 'datatable', columns: [ { - id: 'col-0', - name: 'geo.dest: Descending', + id: geoDestId, + name: `${geoDestId}: Descending`, + meta: { type: 'string' }, }, { - id: 'col-1', + id: countId, name: 'Count', + meta: { type: 'number' }, }, ], rows: [ - { 'col-0': 'CN', 'col-1': 26 }, - { 'col-0': 'IN', 'col-1': 17 }, - { 'col-0': 'US', 'col-1': 6 }, - { 'col-0': 'DE', 'col-1': 4 }, - { 'col-0': 'BR', 'col-1': 3 }, + { [geoDestId]: 'CN', [countId]: 26 }, + { [geoDestId]: 'IN', [countId]: 17 }, + { [geoDestId]: 'US', [countId]: 6 }, + { [geoDestId]: 'DE', [countId]: 4 }, + { [geoDestId]: 'BR', [countId]: 3 }, ], -} as unknown) as Datatable; +}; -const visParams = { - bucket: { accessor: 0, format: {} }, - metric: { accessor: 1, format: {} }, +const visParams: TagCloudRendererParams = { + bucket: { type: 'vis_dimension', accessor: 0, format: { params: {} } }, + metric: { type: 'vis_dimension', accessor: 1, format: { params: {} } }, scale: 'linear', orientation: 'single', palette: { @@ -55,13 +60,42 @@ const visParams = { minFontSize: 12, maxFontSize: 70, showLabel: true, -} as TagCloudVisParams; +}; + +const formattedData: WordcloudSpec['data'] = [ + { + color: 'black', + text: 'CN', + weight: 1, + }, + { + color: 'black', + text: 'IN', + weight: 0.6086956521739131, + }, + { + color: 'black', + text: 'US', + weight: 0.13043478260869565, + }, + { + color: 'black', + text: 'DE', + weight: 0.043478260869565216, + }, + { + color: 'black', + text: 'BR', + weight: 0, + }, +]; describe('TagCloudChart', function () { - let wrapperProps: TagCloudChartProps; + let wrapperPropsWithIndexes: TagCloudChartProps; + let wrapperPropsWithColumnNames: TagCloudChartProps; beforeAll(() => { - wrapperProps = { + wrapperPropsWithIndexes = { visData, visParams, palettesRegistry, @@ -70,68 +104,77 @@ describe('TagCloudChart', function () { syncColors: false, visType: 'tagcloud', }; + + wrapperPropsWithColumnNames = { + visData, + visParams: { + ...visParams, + bucket: { + type: 'vis_dimension', + accessor: { + id: geoDestId, + name: geoDestId, + meta: { type: 'string' }, + }, + format: { id: 'string', params: {} }, + }, + metric: { + type: 'vis_dimension', + accessor: { + id: countId, + name: countId, + meta: { type: 'number' }, + }, + format: { id: 'number', params: {} }, + }, + }, + palettesRegistry, + fireEvent: jest.fn(), + renderComplete: jest.fn(), + syncColors: false, + visType: 'tagcloud', + }; }); - it('renders the Wordcloud component', async () => { - const component = mount(); + it('renders the Wordcloud component with', async () => { + const component = mount(); expect(component.find(Wordcloud).length).toBe(1); }); it('renders the label correctly', async () => { - const component = mount(); + const component = mount(); const label = findTestSubject(component, 'tagCloudLabel'); expect(label.text()).toEqual('geo.dest: Descending - Count'); }); it('not renders the label if showLabel setting is off', async () => { const newVisParams = { ...visParams, showLabel: false }; - const newProps = { ...wrapperProps, visParams: newVisParams }; + const newProps = { ...wrapperPropsWithIndexes, visParams: newVisParams }; const component = mount(); const label = findTestSubject(component, 'tagCloudLabel'); expect(label.length).toBe(0); }); - it('receives the data on the correct format', () => { - const component = mount(); - expect(component.find(Wordcloud).prop('data')).toStrictEqual([ - { - color: 'black', - text: 'CN', - weight: 1, - }, - { - color: 'black', - text: 'IN', - weight: 0.6086956521739131, - }, - { - color: 'black', - text: 'US', - weight: 0.13043478260869565, - }, - { - color: 'black', - text: 'DE', - weight: 0.043478260869565216, - }, - { - color: 'black', - text: 'BR', - weight: 0, - }, - ]); + it('receives the data in the correct format for bucket and metric accessors of type number', () => { + const component = mount(); + expect(component.find(Wordcloud).prop('data')).toStrictEqual(formattedData); + }); + + it('receives the data in the correct format for bucket and metric accessors of type DatatableColumn', () => { + const component = mount(); + expect(component.find(Wordcloud).prop('data')).toStrictEqual(formattedData); }); it('sets the angles correctly', async () => { - const newVisParams = { ...visParams, orientation: 'right angled' } as TagCloudVisParams; - const newProps = { ...wrapperProps, visParams: newVisParams }; + const newVisParams: TagCloudRendererParams = { ...visParams, orientation: 'right angled' }; + const newProps = { ...wrapperPropsWithIndexes, visParams: newVisParams }; const component = mount(); expect(component.find(Wordcloud).prop('endAngle')).toBe(90); expect(component.find(Wordcloud).prop('angleCount')).toBe(2); }); it('calls filter callback', () => { - const component = mount(); + const component = mount(); component.find(Settings).prop('onElementClick')!([ [ { @@ -145,6 +188,6 @@ describe('TagCloudChart', function () { }, ], ]); - expect(wrapperProps.fireEvent).toHaveBeenCalled(); + expect(wrapperPropsWithIndexes.fireEvent).toHaveBeenCalled(); }); }); diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx index 163a2e8ce38a..b7d38c71f586 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx +++ b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx @@ -12,8 +12,13 @@ import { throttle } from 'lodash'; import { EuiIconTip, EuiResizeObserver } from '@elastic/eui'; import { Chart, Settings, Wordcloud, RenderChangeListener } from '@elastic/charts'; import type { PaletteRegistry } from '../../../../charts/public'; -import type { IInterpreterRenderHandlers } from '../../../../expressions/public'; -import { getFormatService } from '../services'; +import { + Datatable, + DatatableColumn, + IInterpreterRenderHandlers, +} from '../../../../expressions/public'; +import { getFormatService } from '../format_service'; +import { ExpressionValueVisDimension } from '../../../../visualizations/public'; import { TagcloudRendererConfig } from '../../common/types'; import './tag_cloud.scss'; @@ -68,6 +73,17 @@ const ORIENTATIONS = { }, }; +const getColumn = ( + accessor: ExpressionValueVisDimension['accessor'], + columns: Datatable['columns'] +): DatatableColumn => { + if (typeof accessor === 'number') { + return columns[accessor]; + } + + return columns.filter(({ id }) => id === accessor.id)[0]; +}; + export const TagCloudChart = ({ visData, visParams, @@ -81,18 +97,18 @@ export const TagCloudChart = ({ const bucketFormatter = bucket ? getFormatService().deserialize(bucket.format) : null; const tagCloudData = useMemo(() => { - const tagColumn = bucket ? visData.columns[bucket.accessor].id : -1; - const metricColumn = visData.columns[metric.accessor]?.id; + const tagColumn = bucket ? getColumn(bucket.accessor, visData.columns).id : null; + const metricColumn = getColumn(metric.accessor, visData.columns).id; const metrics = visData.rows.map((row) => row[metricColumn]); - const values = bucket ? visData.rows.map((row) => row[tagColumn]) : []; + const values = bucket && tagColumn !== null ? visData.rows.map((row) => row[tagColumn]) : []; const maxValue = Math.max(...metrics); const minValue = Math.min(...metrics); return visData.rows.map((row) => { - const tag = row[tagColumn] === undefined ? 'all' : row[tagColumn]; + const tag = tagColumn === null ? 'all' : row[tagColumn]; return { - text: (bucketFormatter ? bucketFormatter.convert(tag, 'text') : tag) as string, + text: bucketFormatter ? bucketFormatter.convert(tag, 'text') : tag, weight: tag === 'all' || visData.rows.length <= 1 ? 1 @@ -112,7 +128,9 @@ export const TagCloudChart = ({ ]); const label = bucket - ? `${visData.columns[bucket.accessor].name} - ${visData.columns[metric.accessor].name}` + ? `${getColumn(bucket.accessor, visData.columns).name} - ${ + getColumn(metric.accessor, visData.columns).name + }` : ''; const onRenderChange = useCallback( @@ -133,17 +151,17 @@ export const TagCloudChart = ({ ); const handleWordClick = useCallback( - (d) => { + (elements) => { if (!bucket) { return; } - const termsBucket = visData.columns[bucket.accessor]; - const clickedValue = d[0][0].text; + const termsBucketId = getColumn(bucket.accessor, visData.columns).id; + const clickedValue = elements[0][0].text; const rowIndex = visData.rows.findIndex((row) => { const formattedValue = bucketFormatter - ? bucketFormatter.convert(row[termsBucket.id], 'text') - : row[termsBucket.id]; + ? bucketFormatter.convert(row[termsBucketId], 'text') + : row[termsBucketId]; return formattedValue === clickedValue; }); diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/expression_renderers/tagcloud_renderer.tsx b/src/plugins/chart_expressions/expression_tagcloud/public/expression_renderers/tagcloud_renderer.tsx index 58e177dac677..294371b3a570 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/public/expression_renderers/tagcloud_renderer.tsx +++ b/src/plugins/chart_expressions/expression_tagcloud/public/expression_renderers/tagcloud_renderer.tsx @@ -5,15 +5,16 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ + import React, { lazy } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; +import { ClassNames } from '@emotion/react'; import { i18n } from '@kbn/i18n'; -import { ExpressionRenderDefinition } from '../../../../expressions/common'; import { VisualizationContainer } from '../../../../visualizations/public'; -import { withSuspense } from '../../../../presentation_util/public'; -import { TagcloudRendererConfig } from '../../common/types'; +import { ExpressionRenderDefinition } from '../../../../expressions/common/expression_renderers'; import { ExpressioTagcloudRendererDependencies } from '../plugin'; +import { TagcloudRendererConfig } from '../../common/types'; import { EXPRESSION_NAME } from '../../common'; export const strings = { @@ -27,8 +28,11 @@ export const strings = { }), }; -const LazyTagcloudComponent = lazy(() => import('../components/tagcloud_component')); -const TagcloudComponent = withSuspense(LazyTagcloudComponent); +const tagCloudVisClass = { + height: '100%', +}; + +const TagCloudChart = lazy(() => import('../components/tagcloud_component')); export const tagcloudRenderer: ( deps: ExpressioTagcloudRendererDependencies @@ -43,17 +47,29 @@ export const tagcloudRenderer: ( }); const palettesRegistry = await palettes.getPalettes(); + const showNoResult = config.visData.rows.length === 0; + render( - - - + + {({ css, cx }) => ( + + + + )} + , domNode ); diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/services.ts b/src/plugins/chart_expressions/expression_tagcloud/public/format_service.ts similarity index 100% rename from src/plugins/chart_expressions/expression_tagcloud/public/services.ts rename to src/plugins/chart_expressions/expression_tagcloud/public/format_service.ts diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/plugin.ts b/src/plugins/chart_expressions/expression_tagcloud/public/plugin.ts index 7cbc9ac7c670..9ffb910bde21 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/public/plugin.ts +++ b/src/plugins/chart_expressions/expression_tagcloud/public/plugin.ts @@ -12,7 +12,7 @@ import { ChartsPluginSetup } from '../../../charts/public'; import { tagcloudRenderer } from './expression_renderers'; import { tagcloudFunction } from '../common/expression_functions'; import { FieldFormatsStart } from '../../../field_formats/public'; -import { setFormatService } from './services'; +import { setFormatService } from './format_service'; interface SetupDeps { expressions: ExpressionsSetup; diff --git a/src/plugins/vis_types/metric/public/components/metric_vis_component.tsx b/src/plugins/vis_types/metric/public/components/metric_vis_component.tsx index c3735bdc0d79..837ec5ff60dc 100644 --- a/src/plugins/vis_types/metric/public/components/metric_vis_component.tsx +++ b/src/plugins/vis_types/metric/public/components/metric_vis_component.tsx @@ -16,7 +16,7 @@ import { Datatable } from '../../../../expressions/public'; import { getHeatmapColors } from '../../../../charts/public'; import { VisParams, MetricVisMetric } from '../types'; import { getFormatService } from '../services'; -import { SchemaConfig } from '../../../../visualizations/public'; +import { ExpressionValueVisDimension } from '../../../../visualizations/public'; import { Range } from '../../../../expressions/public'; import './metric_vis.scss'; @@ -98,6 +98,16 @@ class MetricVisComponent extends Component { return fieldFormatter.convert(value, format); }; + private getColumn( + accessor: ExpressionValueVisDimension['accessor'], + columns: Datatable['columns'] = [] + ) { + if (typeof accessor === 'number') { + return columns[accessor]; + } + return columns.filter(({ id }) => accessor.id === id)[0]; + } + private processTableGroups(table: Datatable) { const config = this.props.visParams.metric; const dimensions = this.props.visParams.dimensions; @@ -112,13 +122,12 @@ class MetricVisComponent extends Component { let bucketFormatter: IFieldFormat; if (dimensions.bucket) { - bucketColumnId = table.columns[dimensions.bucket.accessor].id; + bucketColumnId = this.getColumn(dimensions.bucket.accessor, table.columns).id; bucketFormatter = getFormatService().deserialize(dimensions.bucket.format); } - dimensions.metrics.forEach((metric: SchemaConfig) => { - const columnIndex = metric.accessor; - const column = table?.columns[columnIndex]; + dimensions.metrics.forEach((metric: ExpressionValueVisDimension) => { + const column = this.getColumn(metric.accessor, table?.columns); const formatter = getFormatService().deserialize(metric.format); table.rows.forEach((row, rowIndex) => { let title = column.name; diff --git a/src/plugins/vis_types/metric/public/metric_vis_fn.ts b/src/plugins/vis_types/metric/public/metric_vis_fn.ts index 9a144defed4e..210552732bc0 100644 --- a/src/plugins/vis_types/metric/public/metric_vis_fn.ts +++ b/src/plugins/vis_types/metric/public/metric_vis_fn.ts @@ -15,9 +15,10 @@ import { Render, Style, } from '../../../expressions/public'; -import { visType, DimensionsVisParam, VisParams } from './types'; +import { visType, VisParams } from './types'; import { prepareLogTable, Dimension } from '../../../visualizations/public'; import { ColorSchemas, vislibColorMaps, ColorMode } from '../../../charts/public'; +import { ExpressionValueVisDimension } from '../../../visualizations/public'; export type Input = Datatable; @@ -32,8 +33,8 @@ interface Arguments { subText: string; colorRange: Range[]; font: Style; - metric: any[]; // these aren't typed yet - bucket: any; // these aren't typed yet + metric: ExpressionValueVisDimension[]; + bucket: ExpressionValueVisDimension; } export interface MetricVisRenderValue { @@ -150,14 +151,6 @@ export const createMetricVisFn = (): MetricVisExpressionFunctionDefinition => ({ }, }, fn(input, args, handlers) { - const dimensions: DimensionsVisParam = { - metrics: args.metric, - }; - - if (args.bucket) { - dimensions.bucket = args.bucket; - } - if (args.percentageMode && (!args.colorRange || args.colorRange.length === 0)) { throw new Error('colorRange must be provided when using percentageMode'); } @@ -184,6 +177,7 @@ export const createMetricVisFn = (): MetricVisExpressionFunctionDefinition => ({ const logTable = prepareLogTable(input, argsTable); handlers.inspectorAdapters.tables.logDatatable('default', logTable); } + return { type: 'render', as: 'metric_vis', @@ -209,7 +203,10 @@ export const createMetricVisFn = (): MetricVisExpressionFunctionDefinition => ({ fontSize, }, }, - dimensions, + dimensions: { + metrics: args.metric, + ...(args.bucket ? { bucket: args.bucket } : {}), + }, }, }, }; diff --git a/src/plugins/vis_types/metric/public/types.ts b/src/plugins/vis_types/metric/public/types.ts index 1baaa25959f3..8e86c0217bba 100644 --- a/src/plugins/vis_types/metric/public/types.ts +++ b/src/plugins/vis_types/metric/public/types.ts @@ -7,14 +7,14 @@ */ import { Range } from '../../../expressions/public'; -import { SchemaConfig } from '../../../visualizations/public'; +import { ExpressionValueVisDimension } from '../../../visualizations/public'; import { ColorMode, Labels, Style, ColorSchemas } from '../../../charts/public'; export const visType = 'metric'; export interface DimensionsVisParam { - metrics: SchemaConfig[]; - bucket?: SchemaConfig; + metrics: ExpressionValueVisDimension[]; + bucket?: ExpressionValueVisDimension; } export interface MetricVisParam { diff --git a/src/plugins/vis_types/tagcloud/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_types/tagcloud/public/__snapshots__/to_ast.test.ts.snap index fed6fb54288f..9e4c3071db8d 100644 --- a/src/plugins/vis_types/tagcloud/public/__snapshots__/to_ast.test.ts.snap +++ b/src/plugins/vis_types/tagcloud/public/__snapshots__/to_ast.test.ts.snap @@ -1,6 +1,108 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`tagcloud vis toExpressionAst function should match snapshot params fulfilled 1`] = ` +exports[`tagcloud vis toExpressionAst function should match snapshot params fulfilled with DatatableColumn vis_dimension.accessor at metric 1`] = ` +Object { + "chain": Array [ + Object { + "arguments": Object { + "aggs": Array [], + "index": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "id": Array [ + "123", + ], + }, + "function": "indexPatternLoad", + "type": "function", + }, + ], + "type": "expression", + }, + ], + "metricsAtAllLevels": Array [ + false, + ], + "partialRows": Array [ + false, + ], + }, + "function": "esaggs", + "type": "function", + }, + Object { + "arguments": Object { + "bucket": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "accessor": Array [ + 0, + ], + "format": Array [ + "terms", + ], + "formatParams": Array [ + "{\\"id\\":\\"string\\",\\"otherBucketLabel\\":\\"Other\\",\\"missingBucketLabel\\":\\"Missing\\"}", + ], + }, + "function": "visdimension", + "type": "function", + }, + ], + "type": "expression", + }, + ], + "maxFontSize": Array [ + 15, + ], + "metric": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "accessor": Array [ + 1, + ], + "format": Array [ + "number", + ], + }, + "function": "visdimension", + "type": "function", + }, + ], + "type": "expression", + }, + ], + "minFontSize": Array [ + 5, + ], + "orientation": Array [ + "single", + ], + "palette": Array [ + "default", + ], + "scale": Array [ + "linear", + ], + "showLabel": Array [ + true, + ], + }, + "function": "tagcloud", + "type": "function", + }, + ], + "type": "expression", +} +`; + +exports[`tagcloud vis toExpressionAst function should match snapshot params fulfilled with number vis_dimension.accessor at metric 1`] = ` Object { "chain": Array [ Object { diff --git a/src/plugins/vis_types/tagcloud/public/to_ast.test.ts b/src/plugins/vis_types/tagcloud/public/to_ast.test.ts index c70448ab113c..6de1d4fb3e75 100644 --- a/src/plugins/vis_types/tagcloud/public/to_ast.test.ts +++ b/src/plugins/vis_types/tagcloud/public/to_ast.test.ts @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -import { Vis } from 'src/plugins/visualizations/public'; +import { Vis, VisToExpressionAstParams } from '../../../visualizations/public'; import { toExpressionAst } from './to_ast'; import { TagCloudVisParams } from './types'; -const mockSchemas = { +const mockedSchemas = { metric: [{ accessor: 1, format: { id: 'number' }, params: {}, label: 'Count', aggType: 'count' }], segment: [ { @@ -31,14 +31,14 @@ const mockSchemas = { }; jest.mock('../../../visualizations/public', () => ({ - getVisSchemas: () => mockSchemas, + getVisSchemas: () => mockedSchemas, })); describe('tagcloud vis toExpressionAst function', () => { let vis: Vis; beforeEach(() => { - vis = { + vis = ({ isHierarchical: () => false, type: {}, params: { @@ -51,15 +51,15 @@ describe('tagcloud vis toExpressionAst function', () => { aggs: [], }, }, - } as any; + } as unknown) as Vis; }); it('should match snapshot without params', () => { - const actual = toExpressionAst(vis, {} as any); + const actual = toExpressionAst(vis, {} as VisToExpressionAstParams); expect(actual).toMatchSnapshot(); }); - it('should match snapshot params fulfilled', () => { + it('should match snapshot params fulfilled with number vis_dimension.accessor at metric', () => { vis.params = { scale: 'linear', orientation: 'single', @@ -70,9 +70,48 @@ describe('tagcloud vis toExpressionAst function', () => { type: 'palette', name: 'default', }, - metric: { accessor: 0, format: { id: 'number' } }, + metric: { + type: 'vis_dimension', + accessor: 0, + format: { + id: 'number', + params: { + id: 'number', + }, + }, + }, + }; + const actual = toExpressionAst(vis, {} as VisToExpressionAstParams); + expect(actual).toMatchSnapshot(); + }); + + it('should match snapshot params fulfilled with DatatableColumn vis_dimension.accessor at metric', () => { + vis.params = { + scale: 'linear', + orientation: 'single', + minFontSize: 5, + maxFontSize: 15, + showLabel: true, + palette: { + type: 'palette', + name: 'default', + }, + metric: { + type: 'vis_dimension', + accessor: { + id: 'count', + name: 'count', + meta: { type: 'number' }, + }, + format: { + id: 'number', + params: { + id: 'number', + }, + }, + }, }; - const actual = toExpressionAst(vis, {} as any); + const actual = toExpressionAst(vis, {} as VisToExpressionAstParams); expect(actual).toMatchSnapshot(); }); }); diff --git a/src/plugins/vis_types/tagcloud/public/types.ts b/src/plugins/vis_types/tagcloud/public/types.ts index 28a7c6506eb3..996555ae99f8 100644 --- a/src/plugins/vis_types/tagcloud/public/types.ts +++ b/src/plugins/vis_types/tagcloud/public/types.ts @@ -6,15 +6,7 @@ * Side Public License, v 1. */ import type { ChartsPluginSetup, PaletteOutput } from '../../../charts/public'; -import type { SerializedFieldFormat } from '../../../expressions/public'; - -interface Dimension { - accessor: number; - format: { - id?: string; - params?: SerializedFieldFormat; - }; -} +import { ExpressionValueVisDimension } from '../../../visualizations/public'; interface TagCloudCommonParams { scale: 'linear' | 'log' | 'square root'; @@ -26,8 +18,8 @@ interface TagCloudCommonParams { export interface TagCloudVisParams extends TagCloudCommonParams { palette: PaletteOutput; - metric: Dimension; - bucket?: Dimension; + metric: ExpressionValueVisDimension; + bucket?: ExpressionValueVisDimension; } export interface TagCloudTypeProps { diff --git a/src/plugins/visualizations/common/expression_functions/vis_dimension.test.ts b/src/plugins/visualizations/common/expression_functions/vis_dimension.test.ts new file mode 100644 index 000000000000..249c796afeac --- /dev/null +++ b/src/plugins/visualizations/common/expression_functions/vis_dimension.test.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Arguments, visDimension } from './vis_dimension'; +import { functionWrapper } from '../../../expressions/common/expression_functions/specs/tests/utils'; +import { Datatable } from '../../../expressions/common'; +import moment from 'moment'; + +describe('interpreter/functions#vis_dimension', () => { + const fn = functionWrapper(visDimension()); + const column1 = 'username'; + const column2 = '@timestamp'; + + const input: Datatable = { + type: 'datatable', + columns: [ + { id: column1, name: column1, meta: { type: 'string' } }, + { id: column2, name: column2, meta: { type: 'date' } }, + ], + rows: [ + { [column1]: 'user1', [column2]: moment().toISOString() }, + { [column1]: 'user2', [column2]: moment().toISOString() }, + ], + }; + + it('should return vis_dimension accessor in number format when type of the passed accessor is number', () => { + const accessor = 0; + const args: Arguments = { accessor }; + + const result = fn(input, args); + expect(result).toHaveProperty('type', 'vis_dimension'); + expect(result).toHaveProperty('accessor', accessor); + expect(result).toHaveProperty('format'); + expect(result.format).toBeDefined(); + expect(typeof result.format === 'object').toBeTruthy(); + }); + + it('should return vis_dimension accessor in DatatableColumn format when type of the passed accessor is string', () => { + const accessor = column2; + const args: Arguments = { accessor }; + const searchingObject = input.columns.filter(({ id }) => id === accessor)[0]; + + const result = fn(input, args); + expect(result).toHaveProperty('type', 'vis_dimension'); + expect(result).toHaveProperty('accessor'); + expect(result.accessor).toMatchObject(searchingObject); + expect(result).toHaveProperty('format'); + expect(result.format).toBeDefined(); + expect(typeof result.format === 'object').toBeTruthy(); + }); + + it('should throw error when the passed number accessor is out of columns array boundary', () => { + const accessor = input.columns.length; + const args: Arguments = { accessor }; + + expect(() => fn(input, args)).toThrowError('Column name or index provided is invalid'); + }); + + it("should throw error when the passed column doesn't exist in columns", () => { + const accessor = column1 + '_modified'; + const args: Arguments = { accessor }; + + expect(() => fn(input, args)).toThrowError('Column name or index provided is invalid'); + }); +}); diff --git a/src/plugins/visualizations/common/expression_functions/vis_dimension.ts b/src/plugins/visualizations/common/expression_functions/vis_dimension.ts index 6886fa94f878..60d3fc78ac55 100644 --- a/src/plugins/visualizations/common/expression_functions/vis_dimension.ts +++ b/src/plugins/visualizations/common/expression_functions/vis_dimension.ts @@ -14,7 +14,7 @@ import { DatatableColumn, } from '../../../expressions/common'; -interface Arguments { +export interface Arguments { accessor: string | number; format?: string; formatParams?: string; @@ -31,6 +31,12 @@ export type ExpressionValueVisDimension = ExpressionValueBoxed< } >; +const getAccessorByIndex = (accessor: number, columns: Datatable['columns']) => + columns.length > accessor ? accessor : undefined; + +const getAccessorById = (accessor: DatatableColumn['id'], columns: Datatable['columns']) => + columns.find((c) => c.id === accessor); + export const visDimension = (): ExpressionFunctionDefinition< 'visdimension', Datatable, @@ -69,13 +75,13 @@ export const visDimension = (): ExpressionFunctionDefinition< fn: (input, args) => { const accessor = typeof args.accessor === 'number' - ? args.accessor - : input.columns.find((c) => c.id === args.accessor); + ? getAccessorByIndex(args.accessor, input.columns) + : getAccessorById(args.accessor, input.columns); if (accessor === undefined) { throw new Error( i18n.translate('visualizations.function.visDimension.error.accessor', { - defaultMessage: 'Column name provided is invalid', + defaultMessage: 'Column name or index provided is invalid', }) ); } diff --git a/src/plugins/visualizations/common/prepare_log_table.test.ts b/src/plugins/visualizations/common/prepare_log_table.test.ts index dc02adbd458e..7176ba46c40e 100644 --- a/src/plugins/visualizations/common/prepare_log_table.test.ts +++ b/src/plugins/visualizations/common/prepare_log_table.test.ts @@ -19,13 +19,14 @@ describe('prepareLogTable', () => { meta: {}, }, { + id: 'd3', meta: {}, }, ], }; const logTable = prepareLogTable(datatable as any, [ [[{ accessor: 0 } as any], 'dimension1'], - [[{ accessor: 2 } as any], 'dimension3'], + [[{ accessor: { id: 'd3' } } as any], 'dimension3'], [[{ accessor: 1 } as any], 'dimension2'], ]); expect(logTable).toMatchInlineSnapshot( @@ -42,6 +43,7 @@ describe('prepareLogTable', () => { }, }, { + id: 'd3', meta: { dimensionName: 'dimension3', }, @@ -62,6 +64,7 @@ describe('prepareLogTable', () => { }, }, Object { + "id": "d3", "meta": Object { "dimensionName": "dimension3", }, diff --git a/src/plugins/visualizations/common/prepare_log_table.ts b/src/plugins/visualizations/common/prepare_log_table.ts index 0018a18ce7f1..b3f74c8611af 100644 --- a/src/plugins/visualizations/common/prepare_log_table.ts +++ b/src/plugins/visualizations/common/prepare_log_table.ts @@ -8,16 +8,31 @@ import { ExpressionValueVisDimension } from './expression_functions/vis_dimension'; import { ExpressionValueXYDimension } from './expression_functions/xy_dimension'; -import { Datatable } from '../../expressions/common/expression_types/specs'; +import { Datatable, DatatableColumn } from '../../expressions/common/expression_types/specs'; export type Dimension = [ Array | undefined, string ]; -const getDimensionName = (columnIndex: number, dimensions: Dimension[]) => { +const isColumnEqualToAccessor = ( + column: DatatableColumn, + columnIndex: number, + accessor: ExpressionValueVisDimension['accessor'] | ExpressionValueXYDimension['accessor'] +) => { + if (typeof accessor === 'number') { + return accessor === columnIndex; + } + return accessor.id === column.id; +}; + +const getDimensionName = ( + column: DatatableColumn, + columnIndex: number, + dimensions: Dimension[] +) => { for (const dimension of dimensions) { - if (dimension[0]?.find((d) => d.accessor === columnIndex)) { + if (dimension[0]?.find((d) => isColumnEqualToAccessor(column, columnIndex, d.accessor))) { return dimension[1]; } } @@ -31,7 +46,7 @@ export const prepareLogTable = (datatable: Datatable, dimensions: Dimension[]) = ...column, meta: { ...column.meta, - dimensionName: getDimensionName(columnIndex, dimensions), + dimensionName: getDimensionName(column, columnIndex, dimensions), }, }; }), diff --git a/test/interpreter_functional/screenshots/baseline/metric_all_data.png b/test/interpreter_functional/screenshots/baseline/metric_all_data.png index 54ee1f4da6684ab4f12fd87bc84d3439aa0dd763..66357a371a5be96013f5a3cc4f33400f78f4392d 100644 GIT binary patch literal 29776 zcmeFZWmJ}H)HUk106`=Kqy-d|?hXYcq$C6d0qKe+J;vS}Ztmy0*R`&>=A3KY_`i}Bzk7%1&b4dT?n+9CDqOpE zv-#S!Kj=|!!*}S(&A(r}W$(u4z+H{_}Ii~l}E|BGVF;4{$$dPNNlS$TOD z9-hjDC7zD%ZYmnCXpeJYlCKnld1}!yRejCPgXw0IhT1x7IQaNJ^>k04Qmi{*_Ds0! zCCKjWC2%Lo@{`@unw|CX^~J>|6AN$kFEJjv?%UKbnv$00l{?SFZN1-N&D+ssJ!iwa z>%i~j?TbrBCM)XjK!%8>|J!SH5=<1zy(>Ba+ko6$(}W%uxG5~fk~gtJJ>4rwCBA)! zxv{A!OH(L`^Mfij9-+S(eLIzm|b5HW~9^ zti<^F#p!Os-3c?UO2?Ch#l^7b<>u9&G&H^b>kH(fVq#dhiqRt@58%-h%FWnWSZvWS zF~vmFc>fBF#i;P2mAspVpQ2HyUuemre|Po393jW+Up7bTw4Ol#79y4Y;eNxHeMt*_FW=Jwr< z-p7-Sjo;~eIMMMLwU>Ly3v=AB>M-gz^_#DB_4Qd6zl!e?bjNe({r+k6Mj0!zV}7^L zWH>bUY}l#pWQi{zARzmRc$Q~RBHyoiP0iE8O_|NHuZX~j8kgS%(?6=Ve;4btnMrdC zlb)yAGu=Me?t+iYP|l0<^HYlavga-$+AMc*YI288@FIw*?mR9fMcd`8(Ln3FzJhyA zK9kRsbRP|AZE{T)>8_qVq9Jc4h~xb5b7$OqcI~TY5q;oV$x;0Fo9_p%pTqCXq07W` z>LWJGEe?M*yovJm%O2Knk5|F3{m8W>#OGV*%9u&}=*r43TEj7zA-;SbJuD1+!eYL3 zf@P1h(Rg^twD8QAoRH&`tcGPHir8_b;SB3)zbUD9g2$NuQg`|2(96r~72)3AzT@H| z7h*4`v_jp?MQdkkwl)y(Qb?3T-4 zh^BqN)qBb%DtaE&Ha{E86}|Vvb6~Xo%HXAx)NW*BBc&T+R$Mv%JYi(yjcS>6$~O|e zs8*4Cy)&DSx5MJ&eGBg6;}>Z)>DQeNAF~@rJku5y`8HOhW5j>3gE6JCM^;(shW7%DwisT_K2EYG%ZEJO{jGNC1W-Z((LQoVrr#~&!2IYgsyOk4TN5(8uQjE zsH%1*2{^wk(#Bg(v?t3`D{uZzfBt$kTQMk^*?%rYyQZ)1;^bhX@9wS;3g)Bz&GM_p zl@5^D53wCBE=VugV`Sm37zADz25f!DC+Q~&GM(8;t#ypMm zQcgkrSJud=sJ#OB4>N7GLVZOdD3p|q+4OpPdNoI<S(M*Xs~_CLSIh zEB)zIufOKz1|f3*B}1A*k#HBYffy9D|5UIl&&Vh&PTyuFxo4uI^ zp=N$pqm^eUoaR$LLWFEik2s76f2e$R=V@+sCf6PM)_bzwE0j%tc0~GqvwYg}L@ye+ zpR8VIcel>bytU)3cl1+6+6-1w?oU6z4Qcl0zQrZtdMPTJ#(MMbTxo7g&Ru}80n{nH zW@Tl4d(>bxRxXq*7L2n~`u9<94Ukh&d~LTqC1SaY=47+|5*{!z9Ui_zF{5a1U`Q!) zvikVdI~NbK$!eVY_r2P$zFQ3~yC7!oxO~0Bb2a>(ix#Q1tG2zjkl+{*6{UdJuP`uL z>U_`j?11`F8vf_MLSbVQ}UcVtW#1}Ze4gfFjA<6{`$ixN>}VryN&}t8Li0Mw}j!D(mG8( z3`(VXK60bu0V7G`Jtn&$zIabw(#iW(0Xpys)V

3ULeN4iikg8v%78<9ly5o8zR!};PGrP%D9cj5KVZ+df%6ll?+{5W?A^!c*kQ)X zZ{9R}p@myiPbifGj&2dTlfQV;;;_C;lca?VR=G@k+1{@vw%zo$WCtJQ;wCvijCBIS zh%OXxnocy9Pv^HK=UvXxX}qiOTZ@VfYxzLcy4Oa|_)Sq(mT^B>)9A!@Q(qqY6BZ zW@sZGFW+7&9@3m+f(p^T#4x_=y*X~bKf1=WOMt9<`%#;I=gC5s7A$AGm55>w58iYbXQkhrKriK&g)oKeGrt=C`Z zEdYU^A0oipPlBTpN+$B*2p4lO683vmM7S?>O)~q?VLK zjLPwQ*Tfxuk+{n&MZrZe(mqP+=cD9j{ukEyJFO=c1A7P1!O_jPW znK`4?_Cl8nKR)UTUhSD|AV1TW+|2`71=tPWkZX>Vn%qNilNA$fCNO?TsSqrvT6evg zh&{Jgz&D_T0CqiwZ%R6eF+cjsGzNVu!qPsV2jW z?|8nD`}?CM*lT>Svf4wyW|O-iSRk)P4u2M-JFK=CWWRj;`0>wJK5{e?_w(6CFRH=k zO1d{A8K0o?!GVnBa{psW!ZhMXFxNY&eSH}u9(XhgyIl!H$|>j0t@KS7u8$fJ{5mF& zR;;#rk}>6AjW$)W+PeOLb>St^V`?01?4Bzg*Hd;v!q{`ffSZQf3CH(h>%#d%uBe?k z^xlV*b9qFNX5sycnQw~b*ft)p%qh2q5_>nz&Ik?V5Qx`0S-$Jv)CPENop9_8kBSS) zE}wFXRx_&#nVES{OZ#}y9=WG>cS%N)pN3ya+Srg@ew#e`uMEVSsPv!%YnPYtgDx3h@6w&TEFqd zR!pE^naOc71zk=6@Xea)@Wf`7Cr)K%;|+tp1`0|_LD>Ka8qV_{w(9NqslP*tbzP5rBWTFAk<8#(Z^b~@En2K;<;E2S$x!k4Qh~w5GkrUtvUs>Tj zXOa&K`D5wpCj+v8Tj`?kGM6|LcBw0VcMj#yFg;aPY8fFRwaploSZLmWGR zIedDDTWHc!QH=ADD?0}3_0yt&CUys_1A+F@HL_j!lIKhuCchV=;tQ7ZS_)=RZQr-S zMk^{Si@hF!6WJwnc^{2l*5D6n?fJPKm6MZOE{8*0BYh=CI)ZO6$)b;|BEEcicp1GF zZL>HZI`tJcW#i5F1%VJ!six*B@?)(wQYv#DSWxqD;QTglK<2G{zF3>9n;YCt2SDa( z^e8A5WR5ON?kT#S9c7p&cd=kAS99(!bvJ|@1WD7b4Q8_ns+SV+iD8hpuE;lKX8|8f za&8T62^q>6wm5gdq?S#joga9eQ>4?pqZLHPkq}U|xU^nraQifP;r0A9HX7 zL%nF6&O6;e-!L4`XV_12^Z4}&1+TAC5k*6To171dHfVxlr7|D|`U0FNX#9?{G}Lq2 z>vIKyS95e50DKO%ebv1-CY(t4r6)<79gsV<6GL z^2T4pyzFX!Cfclq-1J9mbm+JtuTT`pkS9}t$Em=CecJ*`Je8P9!P#d?ItDJFqk?({^-dQRK@4dpHJ9tJ4mOK zNs0f*Gf|=AB1sM>HBq(G(MCx2K?w$}@#44MBIJhOL_yK%{O%FMp~GuuI`#c7E}e>N z2q~L^^kZ)B2Z~X6&AEgFMLPRYA0%cFriVm!n`55-eA_jN&CNHz^)G9>c#|Ga@+Bb< zP9rRKKP3R^L$lrUrhlvsEGk7o1>N1FE6|zqKp(T*91{6dteM0{j$6c1gh+Ko`79~P zV$-Z5li`KSqxHEROGqx4JAoW2_d_bIk9EPc5~xHWy>Usy})>FU421{rQTS#?8Ez{xIEFYwXEONlHw*lU(jy>(2G*?3{$ z;a$J;_?U-M4Y*XxBpUO*yiO0crwJ6*%C>&Y?>=0vgCe~2(-j~0F41p;i4neR)Ur2^A0|P%PHxm;Qw3m2d(2Te`V*sD(UF zb<~xu?~*X|+E;S6)Yngi>i;Ymw2_|V-Jk~JV5(y6 z8=a%&EG4L0mLfo4M#EuzO*Kfgqm2_Z+|;(9G{^mn_DTvNq2vI_SJdp+=rNyfqjbdsq3c zsOY!nhK5gWpd8U~aYY9h$3}RSzk|hT>*!?rEofqDeKV5=4ZrqhxoWBLbM4fdaL`$b zEC-aUY_{5`B`+7TGS*rL)2%Zx9dcgh;6LJg`H+Ew6qlImLpsEwz0#!XH)-zMVtiar z6ivzUKt9DG5X;(akvC4~@;<4SR?Gzmx}5L6lr|kJ@!p*mo0W1rIpG)Bz9kK;CAlb7 zoN_q?NFQG0ufjhr+ZV0=s-PcLG(Iw65RD|?CFa!8?oCR50tOB}eNa6vXd5`nU2U<; z&7%8DGXL>}5_ziphYQl*6hNXN!-@?qNdV$xSy%dS!xF8|$|!V;UP?X0KbV+{ZMDuB z!6$T;5R{!Hz}TR|xY?Jem)!-=Jg-kZX@Aek%&KyN{|KQN1CSQ8_d_Tfv!kA*QOU)H zB$(*&9QtIn{rr5WiZ)V3+`8pGn2qaQ@H3H*yu2&ezs*2M7?#prN!7cWmNGY|o$ZH9 z^YRHj{X)%t4;7U0$jxX^-@;VqV>5*CaK3t>&iIk~!TLC{`~G~WGs{qoqar~5>l@f% z*LK&wo`+gNAtWq5TFz9O-QfHG?59>+Zx7bu zbq$u^&-Ja|10PD#mV3vVHG{FX1f|Z(GCE|WNN3p$e3L*X(~$~q^Lq7A|F0q_1AvSR z6QF0Gy=ZA|-Ald`81SavjcWSBZm`Cm)-=eArD#{@j|Z2Hh!8NRO`h zSGwR3KgQBz!qoH&2uk}f{u}A5G>0TsuMU@UT|T0riKa<%xvkh8$N3y^k^>7H4U3%N z>b~M6Q)NtjcvO-<@C~J9S<;=zkVM}1;%2g#MJ#>iOh&7*NSwFb8vH|ZbtL4ZYL|)s6kO$PP#ou>m(lcc(w=eF=19|bM5V?&ke|~=Dz{YW| zVUqK7s%W};`j>rB!zCten&z=tVYX{-ZJnXhDmB3+Bzz8jCiNcr9iDIzUrZfwQ&WsO z9H{evfuMSHDC@sq@suYBy2g!$cDEplPFmG{d^`{>+TScwMP0Jr4zz;QyN?SAiD+z$ z)@XtQ(md!(jSXu^Q2VEvd3G%>oe*e%n?hGe2b%~Lf3C$lu;TD{e2mN0O{j@Q>%YD> z#TZX;V{01sb7L#sI`KvXBIVP$M>LPa4i+`ZS4`?@A+Q)Uvp)JsTywCT&52o?JY%J$ z6so_b2wve%X=^ZXk@@r=(qJc9QUf1~dHK@Vr+~w3!q4>#^Lj^R-T?7BA6gyA?C9#E;^RXxKU^CM&+!G{w^;?!%L;S6#F4I7 z;0h;0Y?3D-t6hRNWkDA$1LWv#DdO~y`Sq(e?XeLF^bG)F{B`)6^<)`$?x2TRx6R85 zdS=J6Wnz<%NZZmrecGU;t$iIX5F%5AOmfhH&qmTv=?VAgn?&qcp$YMO`{+9auu2Sa z79Xx@2min-o0OE7a*2&0r-ySp>8FSD%!}YaIPe3#Q6xl4M+jBqEHF~m(;n=bw{B5T z&d~wmTh5}Yzihp6PtYT~y&cu8{!pKG4HwLvXVQ)^=-42)<>$~s*N2<+*_8`@N{ z;I>UG9plrIr(Qn1Sy^*)xG{I~NCOIn@<{`-Jv^@Wq*WkpW(b@3&Y6^&_-cRq^5q)O z86G|!%kk z=xAS|vG0z2+YDzhF!CN(sajjAuZ}5 z=--fhj!Noim!Aj0$ug9phoeYVL7+$@C96#fc3>So4vt4Q#5kd&<8ye-9kguk0&g#` z_unAKR(9ic`zXo<8lL{ZUUx&gm|8Rw_#JT+!2m|6=Bbtzrlu`C!p5FeQhNTJ!g@(C z(7%esHWoCZ)s{8y{;smF?oA-Jk2pD_!fE8JR(7zF{>AU&-nt*38gBRn1kz0lMUUtDq3zcSCjmkZZzf;4nc_!taKndsL)6p{z51%(vI6w=a7( zt%{8Xl#%w_6*PrdDv4t!BKYO%}ygf5tnRp$jXYLbmc@k zVk}!RJ?756d-KvMsi`cBR?)Ch*+^|#5DPrcwWp_Qy860W+WoG$eu054$-s{MpO%jY z7gS0~6*bLTypjjf{~pZDn3drIu3E#QQc?m^i`d)8<3E^jednO% z3|2cr4hrM|RP(613!?C?LfutX z*sMF5-dpMnFX^YD-4CQVk;aWW}zx z3Cy?cTrDgtSRvXANO(@^eD9Gj?#xn=2%HkT+cw=D-f1s+suZDj3+bRSR_CHniA}vg zmi`bD!A>TWdwm(UbeGXtSx=-Z)y-%{phfamtW6Kk(V2>j_VJTkw5fLw=U}Ih%)UpP znA2=F`B=6gpDkw89 z1LU#6>PU_-tj>4SdUF!xPtNNv9RD7 zQ(--5p~`OVBNUL2>;2Tg?T5Ji$g`#LerKNei_#gxFw=q^!O!Pf1 z8x?wJn`Z$u@4`P@VJOPPvp?@4o6cA3^|`CY%%a==jIl0@?UEi$8u6?fimR)H2nFy* zGUC`jbjBiE+E$RMajdiz&yL3j02AO3ovS8j zQFgmqAFHPt4t=|IB?X2w6hUUoh4uoZd!fl=gLslLR%X@_TXWD!90R`Dd_#7qc3asd zAGmF;GP-&O`_mv<o3H zx~~d&5R~?x?Q``!Zv4J~KdP6{A&c!^s1SGf(dPK~xq)8hB5=55lLdqI-a6#e)Fi+W z()lK{u8hcWKd8gta~mm+#%}VZpw#w-Cnkp>6s?U4_wRc9b{kGcv&G05b`Gxhlf{gc zxzf~qw;jinPIPx8rcD`67ho>^a3<@bw_XORadpn=hX@4V)iQ4DA%{K^qVe};HS;2> z?W=k>eo6zr%;IZ`EVk=b)B;uZO%daZOV9S6yUezSX#$&URf^|i*i~t0Xn1GwoKJdj zX$jU74eLn;_-@R^H!CXl@k^3%)oy7 zAHTipu;ZO7?%t&2a^aL9`R*M(&|KzHy)I4{t*$cCk-nO@ialvHH6;IU_kr6(33~~) z5XCg9~|^q7wmVY_4F;Yp5{`g$1+0=diHuFFE;5gZZSyS+8@br#gTBY#anS?B_QE{eU?G zw+jw$HDp_t3cVV?{)BasaZCC&8ajHaVkSCEscwfe;Kv3nVBhZeHWDXiQWz7n{0a-1 zd2Oq(6s4kSeB0(5@}aqjUw^@fRI7|@{b<5q zd}oT00o|UD9>1L9QiqF|+II$Ki+lMm@< znny-Q?+?`709h*yZa*VTn+kzLqw6y%c*h_Kmk&OC%FP5DIY#vDaK#NR1KMxkcSnyL z9*)_jrDy1@jpg_vCpT!ni62V=)+9C=Iqk--Z0V`DnAc z^02HAzWrAVSr-%??OEV?DW4 zi;~@urM|$?eLD<7tH+d=8LnshmR`KfrTX2fzg-URW|!F_!S{PuEk&4F|UdIwa@Ja5`Lj4iHb9qSy6E zy0+ew2fD9K`SWeo4(~f{HVnb(`R74?l&}A`9-h2vW&{b>VqVkiO^_gGYXmg{T=}*T z6fzgD&q9}9QsD5ycmz@q(>~26{GeLiC<@xf@K>Rr9skY@RRqWoyhW?ZSbjZTGhpU$ zmJJ80@F*{V1Ll+by2qz4Bp9vfF4NFlyif2s!3B#CCd;m2Ynn{uKFt7S*Mzk4kWPSp zkJEKN9}SLA0c+4I-9Yv`mECvosDHa23vAb4VcWfPJ;b8@_I4m+#Im=!;~X6O3>ca_ zmWg`_eAwndlaU_WATuXFTYYBH3Rg=11`J!c7Kggm=V8la>93CM(mQ9k@G~|K>1uWA7|q8 zaIQdX(vlC^OoW^c$Q!rYu}2~&%#9RncrK3g<*3L(1y;>G*4IN>D|`VY3Dre32A|IQAh+Y{w*Ir2q8IJ7*nBO-L(+0W-ehxqe)0r^X)wC=AJ6dGanrx=lE zkEFgK2n2V2wLYpH(q)CgH(6+uW*|k^WNt@E*t@l*|ggZrqsbe%h;>)E>=_i$H!UlmQYpPiA(I>X&un!f&E zzqE#sb>xo9X|=&zPtlpLz=UJl=DWdk+$*E)sf(J@1$9o-ks$TsAaW!=;|+m74oLb* zz}c-aBwcFSV733aa@NOWrN5F24ehRk;mD_7V_$)2e2$B1*sdp^pU2CQkdkR59)4$y zcevh0MX9aw>ZBezyLSZ0o97mY6YsEt*F)aINHA7!ToN;h)3UeR! z53orcsc}>aATd35e)ksH%kS+O#8YyoH4F$y?U3Vx21U#Xk1J28Cs}y?jVt>*k z_6Z+wtalT6T-5&zDYmHr>gcTuan0w!IA>VQ$e&OZka-WD19G>_VOS$b@>uSN=A_kn zB6#;{DM+W`o`3`SoX|fbLvwclB9LNfERCg9*fU zkkX7h{q<-A6%$jx2bYb|v5mVXEH>-~D26(Alh!7%2Q#2y_6&UKBG)s<^PxgDvqyCr zB+w!)gf}me;{{B3p_P5v2xicl=kYvxv4Kn*_Sz3kAo|j-J8q&q`1V0Hk2#=deEIKW{i*#&P)tdNvaRX6#c)+%Bj&=f9)|+_8_U zlZ86+_do(=WD0l<(uutOYn;x%ZEj<%1Tq~0oy}IHk)PD>jqCaI4_9oSadxwWcH)A1 zQCnhO8|DFWw`*bF1`Ev1p)LB-cK!Dz1i(^`UH-j?hpm`4a{Y zrq=pu>6jQ}eCxrMd%ya5*s#9>8(i7<@cK*S60kOdi3)k8Xs|sNVOBQ_qkO_fV7@gt zrRt}H4Xw&OQm^*WH4nQFjwk8MS&Yz{?D$s*zpg%tqmZt0zdocl^W*Yo^40w)y;(ZI zt(ucDVSuVEOek3n`-k45qF(cK;7~Q38!W$?gvh@+J<4JKfUNwQ@PDWyYef!=LiFNKn7 zw-m-imp%2&ts8D_W?>F%uzB?=yGaaW$bg(mpLu)b1LJ5TC62^GSLf)LTQDYpY~?|( zUJs|%=};HQJhU-{^^9Sk(KQp=7Hgdp@Q|~>Dg{ZV<8|TCtIU%AFkYrV8@OLlqar7| zvFE^Fv%91qjhsA!>9Mb%&YCTTX6Sfa7M>Ux5h`VXd7tEPF5enLC<$Ys2H|+gZeq}e z!5jK_qSqXnrWQ?yD%Mr!>_V3np`C--1Ku?!tzF1-*STQ9jigzO(ecSgq+h-V*E?g# z39@+Z_vFav`3DEj#^s<4Fn^_hSx}&65uLvQ5;Ytyjq>ZajGWj+Gg;tVNRVHHgzWoJ0;=R!<;4gKOss zc1H$rKDc05Ji}nYd|z2y9xb#)w@!8p0oTE0b1A;SU`{{>$hAmwGB*k4R-ywJG}8_G zQn(Pnlo1M{Ns|>$?e9;IR)gVl@Suwp%Wp&};)AITl?VE(Ty?)oO$f+hIlZBAlzj&V zMKB0m9$z8Enedr3It_A$XMS4GnE;9}FU99Ccm8n!V;0gNkHct@_MFeO9h*pg)#uka zb-F_a$w*7Wyu>iJ8ESX~hX@Mv5=CsYVd%QaygHUbMZM$OYt8R##l6T04=&Xb>A3*6 ztC_g3o)F9DFc>(P^%0ov_+mzi_S)}s+JmZG6aiB36vPyf|eB z)P?r}I!5U^%NVIs)_*_mVPuHony)lSZd|k~)|%I^Q!h_~W2_?{7$u2OY5dR=CJdm% zJ(T`pU%%#ZEpP)SV3T3oyrAy%wmCrGLESkkFu+ia=)*6(UPJlawYb|$Q~vhh=Uo(a zS8lk|7wgW*(UWSY~$NF1MZ*cXqo55nfua@-g*eH74`0}CA;CPWQ}O`m&tB zQZ1h&tFYXbFAd?%`91dS_5DBc)ch@J$o(} zuyJEA|(C?joNbNuG8w$9L=M)l#z21r8Tnp{uD8mk{wK zBqlzUPvLeq&@Yb7%~jFW*V{ckbk{O5@l@9FEiPtz{PMo|=QR7S-d-9is&IO^=qG&u z!7#EDWG!isKtdu`g+5KMZf&MnMr!zIE%? zyGo}-TpGFkbG_T?wY9HkBWs=>8WGXB2hFwy$FBseT3Gzrm-107=Q+{A2qhB`+n#yG z;>=JV5urP`5Sr<@GlPwZ8FIgPnDXKR7^O8QXOgpI$Ar{NY8c!zFrYD2qP^Uj!)5#w zA0Aj-jGGa0miw-hj66Bcke%rA>G$s%mb}#X#KgoTBnNMx8ELV&W-FV(-@PAA7;$tY zoUOE%@$)CWc5e%=`y~xMKKhnf{*(K6rVbFhxNiKmQSn@N4L3`aTkQ7ZZ9RS*9$}jDOTjNSmFkYSM}0NI3mxV|<~BCGo-PW~spPe_Wl|Fb z`?v=fFlktv9=y%hs1B*E6$rt^l(@pM++CJre^416A>Da_VpvK9`MPT#opc#gTx1!+ zq>1k>+>p^d?r{DnvNMvF5^|D}6H+*ozYpWif6u6TX$0! zh7W|FeIZ9*?rxpZ=$d|g&_Bd}Wb$mW6Hii_{kuW$AHpKzvtNey<*@!si+%Js7NNUi zfc#{8dld!60mGrS8%sS&J}=XS5tqKsi^UL*Ncg0;Hr&&+U z#B~p%*6POM@jEYm-(IiA)z@-9^A~#>nw_29#;E-#tHsV=`5)*21g2VU14Q&;lJNg5 zS+(}}Tj`l{MR6SXj%OmlnOA1htilim}tw-5m){z1^XZf)keo=2MqJ&xr~CQ+L(~y)Kpg#ideIYQ;O~_@ZpK zoesRV7%irpY=-R;OQHcx;VYR_lXm@)$OI3R+@3 zDxbSc$zNQnl}k+fFkqSP0qxnvS*qzc>%SS>Wb^cQKjTR!W51BFz)eUX3A?<6n1stG z-xHCLk}|mPVIt(xy3=BF#Ey2boIKOcrJ4E)3BIQ)rk@_IlFMWc6xnvE8V~2Uz^2H5 z`9VuDdp(iQN1@W{#Z7Z_Ca1%d&SZod6Iw{FTB3lPX=?LokofmOdawqTQCT=0E<{Qey^ zZSp(eR>Yn~TZC1zwdL~LB2$W?oYxcpYo5L#pA4B5vb7y=-y?clVYBromNfLbg*j<$}>4`^!v*BVPnBgtmg?|Ua!;AhetU5+n~mS?^UDh-TS87#EU0ib-MKvIPZnoG0N9r2dWyA2g=r(0x}uou zEzTF7YUt_7r;2fixg9-CmvYu&eh@2YO(qpBI=i*yvjcg?&BMd;q4TB}1R@sp7Jl;e z)e~-~4Q_Q_->(C!2$hXN9bYx z>EYVkmtm@#D7PN+S6p4I_c?5bX4pyb@=Xr3XL)5WP|+}Thx2RGGBOnM-@S#sWI=?x zZ3tP!q>9%s_cELGTz~0Uq4Tn`x3XeC_f6TSwV`iV#KgS~BH|M453-et_&rhXesL=IC^SBF(w@gDEG?pV^5oBtH%?O&vPU5AATu(}NXhNoU9Y7pJhyoDFd1XT^JRJ? z?cbUDzQ#4W%GL!?6_>+AN&Mg6D3fCV@CXvq5r6Z>S$Mp>VXbp$J$Jg|R`{wQWM5yN z3C-H?-G+bns!dKITeE<%$>(k(&_PXFTG|u?QFrKr{5%#)m^n&xK6f~0J~=rtJ+VS* zlS`U=t&GoMtfaGGkgQNu!sUEKHq)kP@TS%ZP588!0E77Q)@-84D$W$P*6@~VbArF# zVdrCh*SOX`own6}9*jiUwt9t{iV#S1il4F7pOPl475ZDy{tIXC1u#?7iJD+9uVgF_ z&+8|;&&YxPd5oIs&o6ywZc+cCVE9#UUmsqMMK2x0Pu;~-%K?3unno&KF~*E02%cWq}WgiqlFcxIpaqw9EmM02@3D48jjWw!b=0fkYEm{?7=H~IhB@d*yc=fV3?>}{*%nZ zlFqnawvC9}hXK)z1&Potb;HSTJmxy=Q(9{Wl?PW9T=C_mZR6vCC-Q6+ZUaAj?#5R- zGsSAR1YqNjB~8^mYixi7Y zIWlJ$iM2OS?xpsPVB`Nz`gQc7uqxJYCWq9y!sX=7>u*Xal3#?(&{utS!;I}g~OXcNMXN!e= zA;syGEq|Kb;$U0{CMmV`Ds0i~#qN$&eW1B<8&hX+1#NMNAwk+HEo@kfsz7u$>) zlL?&0y16>f&u)n|dP`xDUfxp8i-MPOcMu(lP6r!p;WXK(ZE^|Pr$J9#T#R?+x5=QS zMq18OL7%uWAu+hmA^vMq{WIWezJ>RAoEbyDCI?eBVua3AfQF_=dwcAJH2cG}Orccs`i7A#(+B*8LIs-3Y>0s{ ze&^lmTHRwZqAwUSqU2lvBzyU3!CH5ZP5n4-bgqGGCprZSSV*+4uh;Sinl_(*wdK zF^0LRTHVV#FxT`FrZfv_Q~a);f1Q!R5A+88^pl4N_4)HZ0CFGCHa=!$WqVs}hbs)O zC{~H;-NW@^z1}I$37MBmeW^62lV!L-?K>ms0-3c(-h-A#u)%wl|6j8Roq$0H3cZ)x zNEF!Bh1Sbt%6XH1pHA5mMSMe8UB&Jnx|j@%Oow)Bni(7=c)w|BNj6y7VFKBCur?Z* zDL1fk)xrGvvovb9+yG3%KN+tddjWm>zI7=qLZ0w=*sFsBgM&@32gH$g!B<*7Tz9$c z&ifhE+r!+q)&5yoErWa6lH;6tNQ!d%9-)7W_Us1;y<%o}hQsch3$Meq#+s8pBv4#) z7efBo2rH{Q$-yLnub@)T&GkT44#i5gAWPll6t5HT8JeYP&Be&XDOfHv{MoA)M8L-Nhva_4rB3|okvAXxV zH8?aaD+@KSr7^1yceKQa82qu;-hw~k>nD0(IjFEr@~Jdd2P?;5SYA(#<9JhPObW3S zVKy*;jL<&~s#MBTjrF;^hD%!ex%LcGtMvUWt8)H3@g|=`RAZbUDUZ7#`E~SqdRh%@ zg42I_yk~ykpTLVAoJjhWylipg9!)HtJBzXq$yJyoZpO^qoHTGxc)b#G#667!` zv>M{YXH@kW`a(ffVZF>qejE(S?WL$#&m)RiN?`>B)1z487GTXegviKcI* z#Mu4){i{M2+#Ma%PKO&WcjsCNx!mpmkj;JgIXe0vxIC7_SeNd!qj8(^M_W z&Ym8%tI*mD;LUF!ms)J^Z35+>6ncdnO0~W4{U4{ zrj|}TOm2adgYgVnR{s^n&$((L~Yh|X8jFaR_c(iCeb113g8C_5*`qmgA5P>F4;UDv3Kx(z~?m> zJ6`EhD*cs;4-_feC~UUJVyP+eVBEt^kbL(k7q0o&;pq3xI}}eM@5J3a-QVw(1HbeQ zyz!9O>w_t3_`3v++{bqQ=csjw@yQ=uD!GfE_dH_BIdH$4IEYsrZnXKxsKxR*(<=8# zEyz;I%k!HZuO5)dMX=Y(a9K2rIp=Uv=tqbGU9P6|%^;MskKt9~y7jz--z_ ztid#vTFTp4V)o z=q8tnN``b|OOA@Z7b#!8{>udiUi&*BV~m9BLw$D`8=ims86$|+&^;9SRMB8}Ei!hl z#Wq*H9Qn=?i}k6lmqu+B_Wl02de2t3;F0nxVqUMZRPb-0r0AciRAXuIJBQ?#2wa@G zHhZHJ5V9L`D}#B{HlA}|xmaJcKTli}G-ctb({S{8HRHLB|BFDCos9Ml9i%okSL__{ zlp$6IIf@mEj+vvVzWLPdN;m|)&58+HzoRK zs%dOLYTlU548nh4Fjqbi{$d2Bi=fq$#m*=sr2^g{YMB@Ut_~>sXM+=~B}S9iDY7JW zoL*o&6ui9t@nf)Kd934Z3mI`PL}**Z_i$hKg6_O^kj!}<7O(S>|Ksp$MP>rCbh z(GZc9sZur>Vb!Ab^$Qolx&JIN{6g!0oa_^f));XBxKk-*EBOaT7d%8VAowB}Yu{mN0{@(@T_C0OxknY;w4I_3FTQ8&0%_DA~oS z&rZEBEtfZ=MspCmGCBJ4ayJdAefLWq%!Ik zO6T5@V|v%fUM=j(H%}SQow1S*$x8F7)JIO13TY5mu7L|B)#tt z#wj(XhLmd$``*7ur%v(R-Oizrth}(5*8W{XuzE*!_gl)THiJTIY1NcF*lv^`-IAw) z0?bDym$D3sBXdk-=G3ssZ`hwBl5yP>RpvzrjrG*i2X>MmY3$yn9LSt+1CVFXmEIn$ z&PUnRbmH?1E7>39>PufsHr-zJ0!purUBAL+t_x5RA*lb>aYC}`(Pr}_yT`lJSo8!V z0wbGZoyOju&Qf)A*T!j)<#}_$LNtZ}_If{*GgI=*v*yc^ixRfS!}(S}r?(Rfk|>9w z4x1^#6Ga9{6~9p%~OQ zb?4#U$B82;J!wYm^(4;9+XhP=3yQ9bV>V!FjE^L?S7p4?(hxV=KD}cHyMKg(2?xb5 z%qx|=^rELUs^^44@amQAjj1mU{62T{z1QfBvn|jUc+6$tbFE4jK+C=-r4PYddvl4tyCpGw}!`wo4i0+?le5B1j z_mxAw1AG2|p53i8zXmlx33{HiUAR3ffA|nF_D8t zIPJP##NSBz_VUWtJIB2hzAtqs7R9*ILgoQ-INx4!-I-vq_o7{C|Lfeo6MPn+phx7t z)w3K#c-b$0nU$hz5AE$GYcDU46vM&3l4B%#jumy^IjdqUwo0GhmR4zcZO_c*Kei># z>p#}Ztn*%ZWahpi!yzjx+nZ`zAb4OWl^#->8B_nXpRUEgAuJLQ+-h7<*59!`}qaM{PSqwM?}sZ^)g?VOTv&Af_3v#6-faB z%2*eSQe<2(|SOXO*FH?_XQEJR%%i`+X@t z>S<3!OX~T&lR<>(|#s zV$?~6SN3fE%OO}fxiSMhi8!&$h+GHZ*>ZT|97}i|<=(|2rQ)kr2lQWU)5CJfGI+iV za@7(ckCdn67urlj78sX$F#&LWIaUCvgA>pLvBK}eD-AV-D@Wvgfl&uEQ6l>LkA*>g zs6tsHFg#xQNVoMsgeoONSFqLljrO*}E2nV~)ISsmL5j$05|?MYsz`!B#{(f4f@|5S z>{r9UAB)*LRX6FOh#;w^?1)hH)g>+v{3Yb8jbDB(Ulz7k*2S5RFH(c8@MEfm+LICW zYSqm48NO`g6IV13FKyaHBGR%;FIiuR=I0@NxP|Mazs2?)8Eajc>@5iU?CgZiax0Lz zA>XN5wQog`XCnC#X1Xv9U~$Z zh%>7SKwZe_h#$XJ!p!ii__OIcTSN3tl)D!!aU(w{!T~y?xtBpyG@pTZ>FeJ=J<*Ie z!gX|=zx@Pu&B_OI+GZsUb4@t`6SL&xjC*|YV0BC@j&>CyPoWX8oAZ#*X}fir_f zw&0NqYzj51N!sSR1_qRSXmAp1XJ@x4`=P5lA8J;PNku^2L$ZCMQIGe`z36D?ox(YT zH9=AA(sO%}mGB9@JVc)8Xc}Lu(|%>Q5{P==X=GAbJ$0E~wuFP8-i4xfNCD23J+P}o zDM_H}Sv5RNom7!u`kSd?fd1N^p=Z5zyV~yS>u}!IgVD&JOsZtL5CZUEc`pi{E52`P z;I4SQPBFQ4h7ZG~XT3}BhOLByj$j0Fe6q`zBz`UZ$q4Ev??I>i+`>)ria|hETu21e zh76Bij#xKM4ifH@Jemo?oKjM^XqfBIq`o{l(cw@l$Rl+R+ABnT2~n~9i<5o+NMHsA zcA#9T?yF!#d8ojJH5L{VcatL4e&U^>IQ(s-}GxfC^;w3Iw?tk{5RCMmzkR zewLu_y)_*jy8u;vbm2^8?uK+TAgby z?O-4Hz=X<4>|=gytW+DXIgfIAWqwz>!N*WQ@Fy3$vNBv(B_ z{O;jzd+Xjel9BWqw5Oj5lDTes+Q7hHCw(Y;C)K;tfpzSw$2!E3i#^Xc5fREXyDJm+7gK(t~Oi|4Zr8yB4{$k1hg+~_jZn{4U# zb8Pa*?BO3Z!v{M_bTn_i57ab#!g%*_2@8j%X(fDI8|)?23_Rpk`mp_cBkZdoY+Rpm zN7W22o})Z@!u25h3_=ilg>RSL2qIFd<{quI&?J(L<)G`CQ!M-$`nmN`*4{TXB`dC! zl)0XcG|XqoTb{UyvLzUS!}~1~NLT7S)A!C_N(3c#cA5?5SeC}8=ER05vG|GjjQ$j- z1@r_l-l4fyM1vPd2f(6Ib4Dy8n87R)BtEepj%x z>=u`!`nZDQ8ig>=X&E>wZHOxr=FE=PCrW21S^BvQFdy_@u(Uh@zjGB>0kK&l$bYbA-)+oNUp)g66)O|Y zE32xa(*`L$-nR;)_^aTacxI3!mv5u2V_@(exT`B~R;`5MauQ=x?Yc|{ab2;GikdjluS+cX(h^~ti3BI)lQ{Svppp2ztJ~OL0EaTyU zv;+cz%vI<4-ICWByib~&a{{))f}>ras)R%Q!0`BO9G6VBAytCIs~d>w?B7quLI!ww z_`_v-;$W`=EW5%s{Sg2OOsN*e^Rc$lEh(yQ`=gtz8@r^7N{i>HOSHWywh^i%ePJ{1 zBY7fZod*E`^j`FEe&DE=a{kF~o@CDm&I%3x%xh!ART|s&jN9N;0#)R0E;%piMxV;} zpvp5gnTE-%02EKOruV@FcNP7XQ7Z(i2l~;Tm>8+C1yZ3yAFuGyFjk3-xpQ)!i~fPk^kNse$md+>clYgzWZhx{s8ia|+H1 zhVPy8yu?{}`ZOctowLuh>ArodRymucozNx5Eg&Rxf%-Px?(-rqPBPTYx^Rrue0o1i z07MwP4rmXI>L>oU&=B7D#HgU&__b#Q_AgijxoRnhYD(zU===#|6S1F(1OgP)OB=eJ zN2XV~44PA+ooXSH`t6YJG}YCG%DLUS&Sq#Qz(}VaCms(g7^14>+fmD{q`#7li(e4c zl$_^`zqB)p6Ucw)UfIgHV%pY=5}z{lS(429Onb_m!vxx_o$-={-uOhRfkbQXrm)3}lUa$cL-SsQt%y9*8hZ1s-pJ~AE|3cz5Yl#}RP&2Qz4-xkBLiYRSq zEnKed?%FS&(t3GqH^{%jy@hUHEfV-8+Q^RrWh__)N}uP3={9d;SB7-tVCi7C4atEq z)eZ3ZJ;G)**m}h$r23t?eQ$A?!(0VmgZ)eEQ~77xz5Q7`4jVr?7v_bLb&U#&I_&%{ zO^uy8a;`Qsk$m=(1z+&moiE2E7{J1n(9>4Ty1DxNkq3_7c9@jjeEV{bGvh9m+^1%1 zoW`XI{&f$lz=avu_wGGl_;Hi>nsSy={_U@3)1#5IlCXL3+pg|-$+|scg>_(ZKz-c?_r-ca&Bh8m}!sX}G z@?GSvceksw%Eu$JZVNo_aul&|nzhQ-O z{rZ(ZH8i;4>_Zm!pQd+F!Z7zmk?S}O{E}$j%u&9)Z3se2mRaF?6pa+5+)_~TCM4}D zFQ|UQV^FRWKD)mCLZPOoDppSv-H>NQ2?MmbUrI*iLy^;oIFmRCIf5cK%}y|JLf@|` zR0-pRf6*Tm97I$`Hny;D9rnthtXhe$H=xb`pqUd4znz7WVm2+@fG`-iU58C;Ia+b4 zL^DoGDbdTvp!^2+kLr>KQL*pLd`n{w9x@WQ()>gw1l}4o{l&?7C#h1YHmVQ9s@?yv zkX#pMY)Tf!8s>B_yHOV*0Gi^qeTYtqTwZ-clV$4zW;xfHd)$3qEnlw3LN8<62yZuD zSL_OPXO4;1P?c6xw3M3+dcGuGO$nqP1gqM%M>240c$zufKp|`a0EYC$Jfr4?xKkS( zGY``v50aP8D;y&BwZ@dcY=p;JCoGAmk~=#mL~NbetqQXW z(J%y&yhoB&>o0dYSbUyoC27QN7IFD0o@G>Ms|gn@{(EFXewop>Bda-QZ!9uZ#`=gu z)em^*i2#OZi&LM0^-Cn=hNZp_Nc*Dmo9y~t2jO&t+_azTiR{;s8|2;cwo(|r{^Vnc z`^Oe|4PIUQgEeGx<`okeqJZ8w{n0eb_%-S1QGJTnO?O-gJN4H`T|kC!lLL*ES+fsV zR$f6re;7I8<}z1WGZ!qX-`Y46wV@4EJy1HW9w!%nscn?YHR`9u&#ZK!Y~>V?j$u2x zeTZe}`OB9ZgasmKtegBV03Wd{c+qgea?t1Lyb(qWoW16j4){k2fELQv$PN9MYC-}xujdDoU~aRN~o*xBE_6kuXN zhty`s(~Hkq_({O^k%vrf0fZamokx(>v~2G2B~4A`WEnDt#K-SNw9hijN27OZQApq< ziOYI6bpvjMM>;TYQ9n{Ba3Dy8Y0upFC@I_M=B+R-4WNSx6(rAgc4yg_e(r|9Jjt-_ zJo22*oUi@4Ie$B~UtW6ghNm@N`2Mnsc&N?WsnM@F7EU_4xLD4owl_xasqw!*$N)&2 zccD%W4xD}sz^OG2!fz?v-H0$}fZ@R}a&eWk!QDpb5=#@c95I%;HhCNB05hK!YmhDy zFURZGQ_i>wZ#3)sB4luc4RywcTrFWyG0_0il0YPA2uU$VR%su3GgLXWBEf@CToa)l z#|5XS%n%d7YTr!s5LeROFL zAAG0938Tj zpmzFICC`aZ)>VtAAyVzlT;=#x8)t8gxrdd64p@+p@#{E@e^u!EyGO!(J;%b*Mn#B* zFH&efToi}zz9Y-H3KrExySKY3s4zutem@CLFF(;KL;y;{9z)Nc5~bssA9K8w01 z2VUNzJODwPx4_ouO;c zK`F$uN;(+$5;c$H-@b>aRarosTqy3`z#gYZ*>&jA$5I#iETfkf>z8O*x=`7W4K5Om zjD!0hcjkLF5r1c-PUYq?#fu-_peNEEkQt)VVL@$e-4a(H9OFFx;rSMFdxkD>hY&&a z8iO8HejLsu@*=XM8Y*yTuUzFQQg(22SzK&%?fWU@+UGZJWnLxwwR-qy9n zNZver2gw4iJ*oocGyIz;yW{ZnOJlrP2-2yKrN7oupdIo&vC*ge-HcZFnpL|Km|*x| z2ik&Yg1!1|Ua*1E^jLdl`Xd;VwXXQYQO`Pzn$w4<^R0KJiH}4;!<|ZN+hKV35H_>! z9~*m6ECK2ry>(Yss#hm)D@7HN9(mtk0YQ+GNH$*A`^~$a(Ca%%cpu%^YZ*z_6R#UT^;y^R(d>#R`%rZsy#U? zxX2q+L)=Hu)C{FQsIgE?(V0DrVuDJQig<8Xleh)~UknJ&K+D{m)n*wJ5j%VPr4*20 z(-`Li;{$KIhbp`@G;h#_hSHUVz!XPA+d6)Yx(|wJppY-Y&%)m0Qjz@1CX|T=3sYhY z*Z~J44-HKZ3^!~;&bTP?PZJa@=t~m7&IUqjzM5%+YdZh+1EOQGMY0aO&Ur=!*4zqI z$2&gDF6g%r0CU^PfPpd%%)79t>A=9y+i5VFKaGBi8LLH}Og}rL_OI8+MZOP53Ku6T1FdM64%Xp=E38QXR*x!F#)WV2^&Pt= z(>nrz6`%Wbrv3BpC>kJ7SfC<@nWVa|l@TueLXp^}XJ;i5ti8e@RDiY+Z#@5h2j4CH zpR<&E;ez#?%$h_FOqt-Pw0~>@7f|r2u^|3v1SG>=K5j84wg?*dcDu`5XLrPF`We|b z2;=Ws+{Yc62fQY*XHZLtjrns@y1BU-zKvD?r#wu|f)A@<=I8Rc>si~pH#Xfgn+7+t zoV)hp22CFx+?6H1Tz)Ok$F!va{@;MLoruO*VO}9Pjld``gIyplnI)eUbR-rVI3-b8 zUj{V75-SVA_?zJ-fU8HuK+9vD^hGgg8JU^OkV}D|%`DUO@NfR|wVyw!j35E4+fewA zLg=1ZdY=U3IAGl4mSa{?_javwU(bY|$t|aKt1EpK&;3%b9;=c*l$czyyPSq;Gt-f( zflWsk9_UFwORTaSc~XX2y&B^7_-YPK_2KE&0KN(cU_` z#h&A2%_^x-X@Tk^7Yd9<4use7;Gas@m!3wMbFs0~q1jDMag;5I-VxK@iz11Wz0Ea2 zEOTuw6+`1~X7l4K9@6%bl9Irj_tzRaMyxx!dQGk3Y*Z@d-_6_PD3N{io5o#qJ3FJCc!eN~{e zw9t9Jc41~t%HwB9oI(YYRE~$s^wZktecC$94czTQ&)-(ULQf%t!05q*CgyfZ=FM&HOmtfx~-nkRF}=kNRn D<^8a} literal 22339 zcmeIa2UJv9w=Rly)7mJ|DyV>n34tQDARr*1ARr)_qR5C6i=;)8Y>PmHAQB1$$vKzg z3@VbUkX#^Hg(4@(nLD@L|GD>`|J?V^z3;p+-W%iZ!RS%!+Iz3HX8h(irv#{}$kWm= z(@;@S(JI`%qd`UW^G7PGLp(=+fh#<0_Jveb7#f8;x3!)entOlDqm6p(;9x_tFDzeA zIHQtR+R0xlIa1WrfwT3ZU6bf@zu08yh}fBz(np$N+i}E8&s$4Q7JYS&`TAz%Uccq3 zs#mOD8*2yN8}4&`GESbQ)`KO6gvoC1mAao{HTdtzQJKF!JUw#lAD4dn*QLK?^5kEV z_?J!mFol14$A5Q)@24SZR1N2I*Ik|MGY+RvDC|;F##@n`7ugbav_4~rwyONPP z_mkP$t$qQNcJ6Dc^0L`JO((8v>Rc^csGVE75^AMEfsEYgA0{R~=_{%zbdn=-6qS|9 zdmE#KoyCpNgaiV7eU*yycr`VZY)8PsmO_H+p7hT8fxP+6?WxPmpXA8iRCQAH?pcBUkuHd!wC+0WyRZEP;w zBGuz*Zfl3}3B9>{dMl@Sv~jq=SoB^1mic-jA>MU@i}9+s_fX!@Qdras`541cJ3K^+ z>&ZLCS-+%Vq*t_V{Yn3=PD=|HPJ4|b+p6Pft=mL&#yFo!{LMVql{41zU+jtC^O+e?M_wZsWYCn{9k?~qI0!#BKhJ|mg%>SQ4Rf3 zMwR%A)ZDC9blMgl8+!=@>&F7ubNvo89ToltR$EayCA%at^VYgP zb_I8gQs&HBHM_g@q=LA$@*TVSfE%5yrJkeB!|)}@*h``3`zfiG;k2~0tnW&BW@UpE z)DHbQt@|~NShc$RtDd`m40ZC?U76VoNcfpS+seucyQ^W{Kkr;Y-2dqaqYj~fxrdOW zz~-H}`zk&@zt>ir``R^yeDkuwHNv_fWoZq=t(89{^)nT9i@-XhO5dL9Eu|j&FD#X= zE9uK!%Zxc|QU?wi{K8h5)m`a^XLxnWwOlDphUzim-UT0wJY|iHjEL5w2XB>F#cGms zFy5L*cNG*mhqhjzYutqJ-p$7EjIBPW`WmFU=+IMqD^^Zhy-O@}+*!A!_&Q9E3AyY) z5V9S(!@f{wypU=Vej0}#F_HLrLlCZ4gxeX4Pse|2pwKu>@^(GFZ9ly}KS(?_+16ye zu-kwgC=O5@@~&GZRxa81tdPAFl}oq@c?Ns<v{}t zTsvB!bm))wxRRitd(=Pw!hP{;@^bx(6Z6csh6H?+A1zy&iG7HM*V*01LFToQox}UB z5vu7e!zHty4IL)=wY@emJNbI09{8aKxc;l`;M!~!Dto|bF27>s47b#7jwgm!zgE#T z$*^f5X7f7-QMEvRi~;P5ij&iiD)Bd(qsI|XqLIFJ|E&Q#FRyaRtgx1s`<8K%7_*Ck zYm)3N-L}^xA0@+f&?OVzad@8-Zp!J+4HrLl^ms4W22oWr!#hcRv4-$0GBPq9ZQTpI z@ICe`D)8`OAE$kf>gDd9X=BSC_eB!^8ME&c*c4;L(|7j|-MaldsP-N#OrwXao}{vO z6~!rGJfHFL#2^BwvWk`NjXuCuA$@uLZ>MVFh zy3<8+|5;#6qTEFk=V0k#$1b(fI1Q_eJJGHzClO?NjB5GKMJmoa+ttE43&}1Cw~4{9 zK4bCeJaJA53B&x~U5Q$(VkdK+6?3=|YG2pfdMb10PU?^@xU6Z}Ao`VgyJ6aQAx|e+nNRYXBJ(LkuYM ztL1h%0Sxc@#m4eUDdX=qAgiy>>Ucs>MxF!8h3O<&V|XjUKlx&6^Z zt1cs>{MgG`@TYOQUuc=qhc;JNm~b2~B6-eGjdR!^x^=NLXM>6oLB^6N6TDKf=$xti zs%pxk9p@OEP$|ovsSUg)?VDn?dlAa!29*(+aaGRTB9$)lxg7DC+20x#d!q#>uUm{< z0w1OHfc#dmgau-j9Dk{n-A(wGUYi{eE|Cwokpk(9Chcpt?6-pAgQW%9R0T!zu6}yme+IB?Oo4 zH7v#~?D+P)$J^X}%f5UG=7YtMh}hWV*w>N|U;aWjb(yM}e&p~|=`XWuEcjDG@m`7! zwVMVmxIV)Xw*m#3n$M|8`G!^La5xPWQ*(17iKNtq-NMOJooq zNyLCOB}}${naZ=tt*s~}G1ujY*tL}c zyH|=nxH(}GMkLlSGrf6vd2i~Sz(QtfJMNhC>aM5#Ej%PHY(DzN5MAQ<{oJRmZw{jM zO-=HkYsyFYaLMqZ^@Y6<+_w_Rnqnu1^6HwJHOvT`h@vr{aboP*<;J6wkGsA~rjcYU z8mz5dawlkC@ts2n!znXOm3wbQ^_TAD4LXJum3pz{?>bN2&27zAOR9d5Y=bi1zqp6j z&WqumX__Yn7}Xr%lq$CCR%jd8wk`i6nytN+!E^e>f8h`ATSur)G&)Jz6_Tx1?XJu= zcb=XQrYB?&f2zif{uL8%A+t{M9ML<-UW$H0qc=nc49#DpUMK4 zuM*Q^bs%nk(UsisNRM5!$%|QDSwnlxtPI?bHUpI5OgGXj3+?ZPm^MPHGV6^u6puKiE@c;`3laa~lb5Tjj(Iv2a&0l9o`K7Q5a`UbO+Q|Z z21J@h`!&Z@oA7p@$UQwhtyh2xjump)W_Vwum2aTFF!Lp;UVs~M=;h_*F25zTKn_O7 zo8mj8s?}Yyp@(N-9Vo*yo#Ef-`Fpc@1|{0&_o8?0DRRS=CHOPH)O{~)MIuQovp{)W z>p;2di&*@trJ_~@3m&gm5;ZB%>B*57iN-r_1%6>VJS!AgbhqAU*^H_={RB?|b#ud-?KiRCC*Hc4YM155 zMX(Uo=Gluzn8h?Wx1IOP_1l(LR?urRe zUmLZFEP9!+sld5C!n~dzTLMl^SWdS6?n5KmpeqX2vb_=$3Ismwx>SzNplW z?##{bsHmvGXaC`CdwBN+uKW&_dCGBqetyR1+30<)zhd&GRIi#_71HK%eg;lFQ`_u9>LupYoiXldOqR_uZ@)>45TfR$C6!=H{wqs{E)E+7r!)fn0 z^Bgs;Abkke8*`GlwoCrViaFmSDI4R|MFLMC~4F?k~~=| zh{GLUS>1fTSz+x%8-P`_{^th%*9$f_T7h)u(G*5?aqLyLmIFvXiul!yY#t2gVAG zukC}(zVTAMI~W@%u@)Gc=E#{PT)DE#Qv@NIrfWuMc6C9#ejG(upSy~gIkg_RK7SAr z3#EZ?vc+~#KTf+SDkk&E_bRkbv3;Lheh*S&v%rrK)Ir+RuI zpK?aOCCFuzHN6h^<;z6?d!OE5$0m4jz4THGy;Zol8FEew9^3Ooamj{>iAk@}CM{OT zZcZF^Z=sT;pWjpd_^tV;c*1U2XlN252Hmm7+RF^e;5ulP601JhUw=Jg)`;Ms7rm_G z9W~Y#wC6hHx26%$r5Ma|#JYEG3l(%#H9C9IWg)D{uU7Of)vE-kj5q7}t{6n{Ia^!?MuW5_8H)#?;NtV<<}s+cL&0`yUsR zro>#ggJRXkrZXZaD_KRwP__~C*h#fLVP?kIq1P@wxmKs58QHvVQ|IljBAr8 zcn4dK1B9FDsLg|L;d+c}$-Bh1PuARVwoAdA10^O0*#IM-zdlqXL_tlqHRSvr7*dzG zOkq1oQ)gJU-mDAtVoyDN6y~fFS*92l- z9GU8WLLh%j*jItVq&3qZE7@Q^-aU|ne%Z*XY2tWAScO88y29XPf1H)eB2TYCje36R z>tz817vYZl`l}Ra^nguZHaO`rB^S+on&wfaK}DEe@j@b06%=Y9P5}GQ_m{}{ePy>e zC+Ts=Tq{$}XyL-euNGT72QvAq9&vueW9_}!0P!-QQhRoh>M2rwAh9Xv{95UegW)>E-lw2o9Hf_1+W-!Tvte$G?YCvIYlBm>5)>UhwS5~mn7Zyx7R3iVd0vl zauqmwBCEmPyhwB?B zed!|DpsL)164?4yFAsNwiwk2WcbDXh(*{gM&5*j$=#jY56x?N8@ju=|vD;o>$TE_s zY5^cl^@R_d;bE#a3K2VE{AHo zhMmpu>7=hRM@WzyysrWaVLs$CT7#%<|Eh~1VE#|>Rcds+F(TxSBm&8;T>?}y&P%m) z>tT~}2RM~!?@)iY<6@1rtH6tfOM(Id0{AoVKQq6E2PbGU03q?}mTI&ychr^96crac zR{R8aS)^uUShp170V?j-Q18Y;P_x_$b496t^V-AnoAY-(lU2f>;sx#jkRlHMkH=g} zAwt*=Rm;+aOYN5#Wg5#g@g9Z ztee}BY9`klH>+@)UQK0{lkwlcwM~yw-Mrh>5g&+Tp|x3-^eFu@HLr3LSXR5M9b;~w zs*E-&kB>nK?(Qlc^=+pY2Mh#W$OYVH#hWg6C1TKQAA!Ug!TGpuzwLkg_PxW$CgWGS zP@drV4_^I%g_8;H+UCJb8%g{3ro-6H8lk47&?!cg8~ZBz_ zV?)4?`J7~5=o@7c+cgxm2G@ON5fD;$K@hx%7;L4t*vIaCFJB%tG6q!`3C)cEba(OF$`0RKO1r14e)j z1sWc07#S+uZcX(XR9SJB8> zF^$?r*N3|n=8fI1lp(~2vg&92;7lcA@K)@d41y$t#5C~s7h=-|KbYO zXwDx$ZKLY8wzg1^7uy1ZgEav)ugd@P^Us7W?;F!Hf5rgqGt(MfB#`1X*cKR2_$$?N z3{XEGfQrJOVhi$C9jd=7mY4J(4B4Hf&>nafUVzwa!yryQjG0Z|6%?Mxv=}} zOe=DF$`45Vbhp^%yFqk;#3cL%L9eBVfCAd*9#i@Y8DZqf~*3K|#k67LUXH z)20^ilbjgWQghnq(ATf8U|xLwUelVX1?h}SP*5{JA~f_OgJHN-^i>O5&lRVNDvdP7 z?8Jr!)~K9&`=&9mLaU$psK?6$%oIsMKqps*|Kod%iOA%OcC+5MW%9`JfaCqM0{A1N zqjuS`o7hhR2)~)Hm7`6|SZr9e(H!*4+AI-o6Dm&reUrL^xIQnOG&N9e^&-}3tdW-2 zs8G7%@#d0x^oifj=1LXK`Y$>HPeRP&#(1H@vM8uo5u30ao1tRaw#vt2ey90$lZI}1 zY-Y9|1@i++f@#ARU0H53PDF zu|=D9{J zd>B`U&*Hce^G`9&xz>I8B*uJ$Ews6iXI8<}XFX|3M;Id(>kib-_r~5I`S}UW0gisA z-Eq3>HZ^5u(K|x-;K|Y|%EvqX5p3W>Df8l&kYF7J ze2iV3sDrk7vGt&ycM~LA^BM_Ukhw}i@{rqD-igCBZ-A=^(>->ESF`=RAzr^;uA1XQ z9j{)sfneQh4htYS#!*rSI0f^Y>z`A$1w*v*uN0l;(@T$+s6>_d1wT>EU)_H7b>LJD z2JyVC3w!H>P9SEeYY z3tz%pPMP+0nQ`M;c=cRf>GDd)&@|_9HwqY!w8FlOGAf4*^s@N+NSD<=Dp zvzIXefBqSvOTQI`=uU=8?IM<@s*|UEDE|x8 zu!3!|qB)QShX`FYczRqPf$r^A`jq zcTAI8vAy4niOtOsOh5%;hf9k*Ndh^}!*UP;L}l$m&hZx2Zn@#9Fa>&f z0%PDT(@y2`KE?$6!3b7-_5ATxr!jJk$ah4Sxzrc&)7wf=CZe>C<2{4*+3kxBgsCcO6PQh#uq}31EpEk*E|`fvymc@_3;;=+0n5w;H5KwT!m!~(+C6b{8~Jgj z+G_O^fe23n)%Tjlim;-JN+?okF;I;Q5n2yUQZXF<8*d@R+~*BBP69PGIZ6=8XPPS$ z_xdzdC6mM}yd=NLhu@!)+CQ5n;$M>z`=kPWtVVE|cQ4x8`-Z}7U4L%u=7R}~ z?5)=B!}}_hWxpf3Lfn;0BwaZnFO7sjactL}PCrj@>`a*MJD{DHdZhMm0ozWx%6A%= zd>lfAK4L?J@@oz`J9ri|3z(d2toxn9>?!P&j=Pk3+tc&r@rD=%Denfe)}foJ&hBgPpJfmEoMsn`Oodh}5VePeJ}<~6ov9w;pR zl=YgEwoj2)P1mq)a|QAd5stF{As?}TF^G8~9517oe12IccTu?Ury!zcL>sq;%M6Q^ zpsSxehVJ_hp#&rR4TKK!>&M-0i(m3qQ>nh@m|B>#e@94S^OkO$n1eRL-hmGfL6gZE zVR8+toRwt*OwKZ2o1>-3LCIXY_-D_yy)e=&ieLd-(n6RpD8-Vl&-zO#9x>K08} z{WypMn`dI|pFqA24V1WtTynmHMYNfxnsM)y?kSF-@SKn5gS*#RLlIvypfoL((NHb> zT%@{HYdu(s-`(|~&}rxCaAICdIu#DR2P*dGa&fRBNfQYQ9rOhFF7VIyGhTGj09sz? zNm4%U1GXevRY$eLCvVh`J45(6AxwN`q$K>utN}u0Q&B&dop{7pJT)vnjsh~ft3$*X za~lc%2(5a{a*ep(;f(M-mK(j!s#(jQ}GilTapvWDe}Q+K3|U3N9&a~ zPaMCLu`#8`hdz?b5iS`ao~2z92uYNis+k{I>MF4I&z5~`UsP9Dk1GOA-AT&R%e?U1 z8A~{QQJI?I>ZyGRIrMq&d(DF0!9x!jY4j4RS07xik+O?e9suzm0c@;MwI|)aj7Yxfbwh>L$5_p15X#;tKK5)1 zi51@`8NHf>Op;JBT(LhyHRK0;HxS=_CDwh{H-k?EN-$>(0m~61Y8#l1&PjgORV0sr z3KD(nG#~kl!)ea4?KN5iNBzuzekd2x0d(?pXfx*-vE&aiueCiY!S>&KTE(Sb8U%-` z#8EK5YpkgN^KrgO_H-Kl>$Z644V+K9mLhMt9Fc`IiYtU>;=W?Zh0)91KhZ}Zy|q3< z2KMuk`hfSBM1uw}lytB40;1G7Kh>inM$rfCR8I$?V(K0s^jfb(+Tt3EO0d~LRx_T{ zGF|{u)f$LxUcEwT00`bq;AHX}YEG7;a*)ZNW+(ueWZnDRx-*P|^Q-d~^#ubjwbet2 z6wZ5mM!OIUczVk8>%xmrTATzUiOOu(qB9hgFh1kX7@@Cv#ikXpUl)y4pvRD7`zEqb zhX;2?#D1g;P$0H<&Q)NR96!uBP!R`3z|T|*jsHIxhrA#Uod2<>77B`$JXF+|xuy%L z-l3te@%|0QDg0rqr|@HAV-{xd5K-Xa41>gmfwI}g-+)+meQw|)koL|5dZv7dPIro0 zmWC`sU3xP>D;#*JFdu;D&^$|8+HxkjFbgionDzZWMIFp0rP#1gspQ)Uy}6zWC4O)l z$CU|Q-(oC5NGVZGuhrm zy7+feE);iPj}>!VNaDHdSC<89MOCGyl4z{RMtSx%?`vvvFdca&c9w8en^%PK~=c>u%61aa!n6k-cqhp8kxb3?N99OkJ(XOn=G? zwM~yIia0{U6k4BMebyMlmIjnm+J~v0X+flEgBU*4t5$=k_~}q#&>sJx zciyZw9_i!_I%`A%<1zkd>QOG_cP+@3LK`o#kWlEk^!19w{-)I0Qay#z0bq|++*vwB zz9#R`o|`o^l#~;$>DSb|aUv`y5biw@3J2QYR3)aA6#iFapZVN36ehF(PA1ZvJa( zk(0C6d*%nNVs;v|$oZ^^uE$OzLqy0H=#J2z*WQRHWSMjvi@7;HiqcOnmfW(b1del6 zdan}qdh>gRn|_mekQv`IGc>*=;Hi*q3hntG)sh5n6(Hq`T}i^oa8#NiUW1$+ zlJizUMWqun+nWP(Ppc%JDDm?Js* z|KPoco^qc({Pciac5pw~V~4lIYFN5|Z9)7l^ftF0i9aJNNY=O`ZDv{^)(WqHqM%hy zzLc7U()Q3n2uhRjs! zQVbm&Gnb_=CV(u>wqO(z`dxTK__9AOeE=UH&u0N;P$FLv>-7hlEC|X57gXNeJY{BT zimHNce5&UT9WsLCg8TB$1RinX_=;-L}6lS4#jY!yBS6A2gqos}$P<0Cpw|u=U?=-EEd=3PmsH7Aw{-jn#1`pj_ z;Qh?XOzO==fgaGxF{dT}>S5&WJpZa5Y=`m|h*2DZ?0{X&gaoQr`Q&9G%f@?o-0K@A zv5;8^3&UdEaD==A^P+RLgLd68@31{Fi^kz$1LwNByBS$)RCS@0aRQMvG#Piq4OP1; z?Ho9$^+O4SLe2mbht=Z90VhSy|3zQSh`geAx5aDUBtw7yDE`ckcM} zt34-xwZvc6guaid0@s!`2P^;s><>vqaUUC4`=ETDQ32+)XMgX=ss%Kc`dTzI@P-h` z5c4UA(DdNpm?K9Nzi}tr5fL4oC6%o{#$^V3;f5JF@KDxH`#Qt!xN}n9!=W&Ysz>;6 z7{;vd@i{_Ix(LgMvG4U{n+cX!j%cwdZ{MrH$6%vm+}tXYQ#QDiU?DK#TqwsY!$-ZU z(m$s(Mf}_9Ktmv7Hqc6Nm&U-TLV!TxuvL2Ap6uR42ih#sI8 zwj8bfiMa6PeLXT@y1cVkhYS`~!Iy#S<9&xP@Kf7HVWUeFuqLT z9;iEbiqx|5LfD&WJg{My5pDVUCF%0^e3^A+I801*!R#xp{P9@2-Ebv4)uW};RJ(+@ zP<_XZVHfKUA5U$k4V&m}OCC^|;pyqFv@4>>+=w?r!DWrEvu2{syU&qHV*gR^ z14BAIw*-$rWJgjGG?$&`2b>JcUnORv+4_Lzj?jM}EYp)@$O5p756RFVcdL$9CPY4{ z9sP@ouamPz@AUhw&E!KEngRExncvRd-6L^96QJ;3f|xGU6Jhmv`ej;DnOTKzmfoKg z!ZKX(|I&DoW|nLSL&)lp5Fsr)pXuVxirYOCw(n1$L*RBf)UZy*mdSIqgM~)yibDkP zU+vB%!>ydi!~nP+GR)AxDxNVfOYMgfk)djtM`ORq z+6-P`V`GCV*}6x(o($$BIy)9H#t3{ZGDeNiph#Wp3>-=B`n*wO{pZK0khpJl2J{+< zun%Xt=GcRoB&6xUd#+nH|BieUUf(w>g0yF+L)Pa4Uzwc6S$0`s#h7e=vU?2nwx9(>iSewjzS3JX5T#zD<;J-C_D0XK;VE%@# z+oHA8B0;4GnT(=?_PW4v8M?=hIUU}Sv!x1k$%Olm5v zN1m}0HS(~Gyn4SY(f!@A`*8|ggg_bouvN5rrvz0Q4jP^5$PY$(5;AZ`XxD1O=mwDw zX0Kq3Lp?{Bl@SqHnoU7~)p>+UsP-Rtsn z|0qF*Vc^R2tdW@0c93*cEeiMvAig+BNM(0#TH`;hurm9Mtc7FB5plpEg#+JNoE5Is zVcxErDa9;l^QW-gNb(R3m?z!*faTi2Bg8zBFzMrWT+KGm_fS&ECPJO4yNOKeeKnLh-|1fF1?cfy1FwIQ>v2c%H8QIC#j&k-S2~O zojfT)@2?uj5RophkeB?AojIvh@U0$q1E2?jaQ344qxjV(`D(`1ppZsp1pprb+0=Xy zhLfH#_aLV#d?PfbiA;}HlyIZ;5G|RSnp#3+LwJvpqgE(?@Ox?@8g(csl($SP&;1B4 zyYs@qtWvi<-8Qr$A2bAVAwlrkL#dv3ef}uqGiVPDOWGRL>Cqv>H!ys>`fyJS`R5?# zCU|v{A%LC}wjW6^SX5PuMBs2svS{}5=0n_3n8|jU70$FB)Xv_KKK1v%6o{x#?DNl3 z=+O);O6)fZ<$||v=}N}lkfjqeZ|iJ6VGl9b zLUa>G7fJkeyUDSeFoaIaG-PZ$;M93wML#l@oPTOBq;3mmb-ABf>t2WBFZbq6tOgg# zAD&_5;^&O~(=4VmE1GM>-YWG9qfytrPs@>helfqe1}Ju4>rOZKj?cFklt}JXFQ)1~ zok~jIZJP{P9J@w;K8V?Qy*R{GBEfHT694spblWUAF4v36(^NMyaC9hZw}iVV-HDsY zP{Tdz?V~pW0^7}$_2#9WYJvTIWB==$`zK#O27Fw%yjI#G4Ykj_lHnsvN$ZV-da`>iNv*D7TLfT!T7H@cgKsqvV-Fru4d<; zi@UdQo=1OCd@-`VQ{xRyZ}Stbx?AlMbsBZ2tt6_l_(X;sQ%dKb$Os7u(HuJ#{(US& zinuuyu4-0n*=eAgA~&Tfl7ZslQcp|cFwNB~&?;?mFrOkFNM4N^OcBo0pSJgwx-#~z z)=)stcu?_xg>F;w6&(EW{;unPd;;sfK_e!w1WaS-Wn;^CiANYXQ7A>^_uSZw2qEXf zT}eWae$}d)wDiOI&)b1PACl)|;`hqeM+X$t)zt;GYhLr$wR5MGKgq!1R!q6Q95R;s z^3~q-=9xMw%%^?$U`uSCx*Rolxqfkls8b?lOn=;62VVdF+>+-7cyQ2I>flXaDFY{0 zR@QMi0x!W1eb8EE8Y}53_xN#voAdV-f#nI_o|k!q=H8ZJ_0q1cQ`sRdBNB<<$K^R;&n2WC+E@+ml*l8TZ;wkEYAPbb z$K$Z_*jzu>)ow8jI(ex6X|-*`jZ7Ck`cED<;qh8`Ag@kx5h=-C9q6|7 z5gOjpOKCVd79ZZ55WzHOX5L7X3ud0d{Vj&E-K`a7L9ak_b5+enBMH;;q4Q24zM3lI zzGuElW}BWyObzyPT5WSTgP}p#pPZbGm{Tj!RZ;gL2MP9BUZOEA}3Csf&A(I z5bJ}yI0i;W_J`ybTgzb3fiGN893rZv-$Yzc-t#n2yeY~>^%$XehSGzAByklI0*J2B zIV)@L-uhEETp8~f805kw8}Gecd(?nNVrptSB8JlHO7x!KqQ-hIhp9WZ?J!e2uYS^W z$UP0KX$7Wh*RB7xEG?PPPuo3-o9j=W{# zY6}g0*>&?>hEJ{W=_566&5tT1X2J%PKn;9@foD$V$n4CTfw0V^B6Bk0W7GZ*RC9*v zGtZ%SvNj{H2{SX;wNK|#B}P1%@j?oN&eaz|FU(A=1EYywFBh(AjU28ib@B%hoIJ_Q z-EoXRH!?3b*PxNbeqJN7X0bXa$@@GHSXNzoPEWq!>eAG&*;!fYwt#AVoEHXC;;Pmy zC-Wr+Dy&0@ByNV*Y>JYpewkNF5OOYIZ}pTtRiDKNBjH zh!>P;{N8m+%w-`qh*jS>lsCn`648_yDgGL|w7A$V+ji)AR(5V~Sb@vNEO=as!(>~_ zXRWt#rR*;zXCK7tzRI`-N>ETxW}MpFIJh)e>oOXyZfKbIiDd;{?BpL*!k>o$>$HKJ zE}!?fC4~Wh$4AD$QrB~nhOpN%u|1|NP(9*V82sjUG#-x@SYPlmWP&itty8C0+S^we zHadwmHGSTjH_Dj;F|zPeLU<0ylNt$*{pVgrifw+C7ZA4P(}GpLJ$Pv5zM1s3@>TKq zVN>go>gXW;x|>%Ak+TjI$m5;>q!PWmyMSXcN=S?3;*yHnW zOcch9A8KW2hSxzrJ6v7p%-Q zLngsvJ3+u)Qdqe(nA%!vF!2u;lJKKj-e?Sl^tre~t+TTe$wBj9{rzU8yWAYZ^ITWV z1!{MfU)u}@3b(B9unf-jO~gznWGj*Ti?Zio2BD+P(7o89@BI8wiM+VWygCHcXSyXG z0XDO}_7R;`82bK+_7=30Q%TUrbPS1;Z-nb|o`uMkrxEo4VsDU+nr|4nNl&)Lg%>=W zis|fS73i%kHiCl{!c9mt{5KiOSxx^6apLNm*$|PICAW`^Md zWZ4%J1J&1z_l_Lzyew$m^3{?HA^{|{j~~+ow5ykLBY6z-o%rp0tOuPrt$U85G2R(x z`trloHE?$ptKDze*BWJRZr)&mv_JDXE9bOC)xEK%;4s^985uK@J1){-p~h_`uZ&#O z#7d*}cqXFIVgjW$5-zGZKcMRr5E2reH@yE+f^qBc78-YsI%~lb_=IYnch!F=rzIcgXGiO zHNYm2{M7U^D%<#A=lZwzM;KUk#GNYF=nNf{9c=p}_y@ZtBdwSAE`z%{Wua8!G}c=U zsEO_Z@>z!BIeg#0KQ6@owV6!5`2oX>6ZLEzD)!8+n4wn{ zR9>#bB(=Yp67TMnwlLGJAC&_c1QN`J!I$4eN>VnJsUbB+rK>OzHb#cij6Anz+k)K9 zJ~lKU(W8IAr!%b*(H{bpl*oy%W7uB8foJyoyF&>J zl|^NI4e#F9qcw0kPSXPv;hMMKWcT(P*9G*7E%^o^%EiR3Wb$Ejbt}Y*>u#6!tG1Ho zI3`dXBc40>m;u>_!7@%>3C*nGF0&iwcuTVG2itJ9sF2hCw(h7Im%6>Y+WpFVji8OG zw$~{tkHLZLm>5F=jhod=5CqMCvM}ORwqDTixrG zDMX-^b-oqfSV^9^>Bpv~meZ$Cr%z2;GTIE~0<6q(FVm_iaVVQP36}?A*x4aFWJ|`< zA&4&Le!7%^LN{CVm9ektr>?KQ?*wR7`HC;jUOoKkuQXRh`?HifGu34vHZiez?qK%s;7OijIF-3Kwlv5}#FeQ_g85_vKOq?mR^+$>hg+vq?=J6|VxV4p1Q4BIde++V3qbDu#i8auFQ8IW?v#^k7^EniqG% zuf7abFQ=FZDJg1sp1Zl31o35d?s?XjWP9`9QAyJf6(v^J$@@trtv!Y!x#Zezh%+DzYpq}hnz9erU6>50Xt z7j1P}9oD+@8#A)uwa~DzT$rJp>15J09jqJ8AgSvVEAI&`^bk6qRtc^lp=Xti&5P$U z6hS4hzyIm9_qfa=K_JHky1@`6Rr|ss-deudHXGui%6!32RsLs_xx__?Ah#Yis;Vn07TQ4G&`dWc zt+!+xWC3WCI2aNXSSU})2^w}E2}Lk2nM|$}lu&M*i5sPFE>w<=;k|sVAnQYYy_uuxEcalg6N}k# z7O^{LG|^7lurwoMhN+{acTG@C470DlKWXZX=-tw=u**=A6f)M7${_jwy@#PTUwz>R z)7YC^_`~d1h4J5sk(Yv5R^CD;xYrRh*Pm_MR&BBRNhHH5OLBi)&}p$BBlkwQk9ha4 zWp3=y&3?p&p;TLtSj^0x$pt$@p;TI?kkTD;F2GL5R2w$$Zw-pfE|4xtM63rzN75j~Nhfww1Yr_3km0h*|IDb}H(GNV!*rfT( zApsg;n@C&d1%ryA^Gq`SA(5o{P}>iiJmg*xhgUX;g6`h4$zED|#5CN!O)Rw=mSd{- zqw8$YLdW4r^XjqViXUDcMnuQynIT*&$L}_lDiq@tx_PN(9{2=JHt^>}Q55 z913B75;gY=kR;@DL@J`QS~n~?CB>`?fTUjWlV}@@HV*{)0H#l;nMTuzWpN3Afy30Y08v!~ZwEX0K{tDP^l?~L+7$`%% ztp01wtY1LjNQ?Gg<_Dhwk#xWWB}R;Z zRqO=+6K$n2|yq3OBLWl9Jc zg2Z+2^8$BEt1FI~0-Zr-hoXVf+7=G#Zn%!^QInICWBU|+5LD?{i!NQ6Y|nv+r5KXX z5!SH67zEZ0?86;KTUXsur2d5+zz;7UpElq)(p^PGEr79ym1AGABQz822Yz}9i*d+q z3mm_96Pd+zU!3L-pC^Bsb>ixB-{?^Sb`#^4ZquEitT8e~ogGrbFG+x~m=x8^*xYPf zWFT|?$=y0<_0Z7D994T-&qu|FDFnldgRmVkc%w$untEcuSWyoTm%5+*!60rFR|Q8r zY|A^4>5c!71WAhI>#>bLj|g(u&4J^8TaX|#a69f z#OOE^jh|lT6Qf5jZGto3s);ZNQG?D{Lt9b@< zqeG@@Hl$wdL=G}qE~gd#<*6y zVux(wbep$g)TJ61)tX{EAGb6MqOj+{70M^W#O>F=dlg^0d>LQoT{1 z?p{m~MC*xG{Y=r7)5LY{$Dv+UU%!6UtK76gC|TJTQ7!8;6KeuUm3MGRW*k9A+mr8U z7|x&PL-HgM=#rIrZ~X;*yB`+V`9U-=D5#_1y5%E$SlCG*E3$5#;xKIiiGYw1Iq!W< zAI*83;s*pG->a`1uIQ-Z_~8gaaa9P=XSyMmLL2}QTaMK^~UUV_Vtl!zjQx<-QpHjfoY0mRJmw|${nvSNq{#ZH^A^Kp4h~w4Ox}a{} zG-&KF7e`1YrfxU8dyKAhx{b=DQTaXdi)5uEw;l?mEg~WlnrbwxK}Pz_9`kkXY(OhCGN%+UToC4D8Kw04LJ=` z^TDQknubO!&4k40k?s8*wjQ#>oBcAR4ga{HATT6E(-w`kaGDsjCxfk)hSkeoU#c|$ zQ`nx)NI>FziS3}C6Bl4BGc-Odj*cv^&~idFjt<}nc4vE5!1G{_9^08*F_ThQsA19W zm61bGQS;o}Z1+Z?1T^ZN6NxC#J*Q~cLAKI=e^MNfYwg}T8@B%&Gnuj!ngT2Y8tM(N z{F%kt?v`+4uKwu`$_?jgtzc&19z14WP8h6L_Sj@*cvXtmJQj;ZCNX3UPq1$!7|=}E zd++cAyQvLtc(%1vuZol=i1H8c2zv`8EL3vY^G$5tJprdU$c1Ld`W5NeI^XU10fjb` zk>J&pB;!vmwYB~sFgzNq=q#qi#?CH3?kEPJGkgAxy~^yH?hJg`oR+7Sw)V_i|Dxj> z`PXa$&}dws&${PqUYoa#J+?-%_%I$)q1GT&AE+$d>6Hj-oGF`O)`$ehCcTi$mbn2L1WW&Xn^#ga&%4-9%s~s4A>f@Ck2m)xWX@_xof)H;=0GwZzZ**c=Jzf;hoSjIH3FR z;X_Zw3UB3WVcUFi!_bp4+U>eXWD}X0oFz8>((muwxdXkfUYe^HlL~;_&8bBk>M9iR zh+Pq24-FqbPT`*W`_D$<8W=H|YbALMLG_)|?+u*L#TsF8H0iEISIC%EJO)|`9TdGn z$BOmZm3$a9=8u}a3V+y0r_rZ=v5(Ngh%rctmsseiQ!MOGx{oAFT4sZ^`H+hl z4*yF|Xk#ue73x!LY#Bfakumq~1wDB1z)=*8ZA~d4z$)?e>tATDvNvSi2O^d63`@>^ z#BcxaRzE{^_QYYTU7pYrp@A=6FfKV%qYYA#zmx^-0RbfIu1vI^-j}!|cLxP|^|NgF}7lpy%&LjfhYM4P4A?B`}Tl(_Z&Q=yA+y56={I;H68;Z&puXi=#`6`_LRV#cbdh%iS~KoF>0QVQWtLbyfi85s}( zfq)^9R1v7QxKd3I+Q*Iv0%hY$@8mx97oD-0(T;PX5i?R|8^WzC-XKiUcphc3Mb51$^~0zPl%6vIFMe8vMFe)xPpJpASUf5O9$=l%o_ zmmmK>Ewh54-38ut9ad=6DwmC58@~trGV|mU2qGt+I<<;w9UJ=XH_p;Bn=!J#rmCaE z1%h1FpKXDr{g0*0PJFzh#XdZxm0)DH$nG-AGo!dtJR$vY^F_P$~w~?FFYS<7V`|Gb!glQ-IV@Mb~fYVmTMpdaqL3~ z;+9poU?YY$HAB2n9*=bYKALEMyw#B%fxlYkSMoOD8jlB0Y=svJ*A%(A(M-L&JFTid zO~Uf@^a{Ii!@jq-{^RYmk?0jvUru=B%4!WdEG&TLx^_1yIXOb#Su`RLm_yL^Ab^Hz zC0HB&x<3{L)}cY35?lXr154eR{(ek^UWYUFYu$sEJ4Lhl?~&|}9wpAqT-|065b3CO z=&H3ric}%hQ<++Ge|ozA)rORl5On1XZ8mMKlIr+@oWb9u(N;V4F>!{Htg zY=R0#OAj+NMrPFb*=kQu&j`{L2=-9U!HqzP8o@ zMM;j>wr%I!6L<&W9Fw`FstZk-z5R6oSnNa@TVJj%Gzm{hiHd1k&Wbd%5SHD#wFp6R z(=d0Dre^hQpZg{M*CgOAh<*26Csn1FpgV-*>VC z{+T<#;porSJ{pY#mL(Z<=XQS1r%e0s=xAF|Uu2Y{c631rcEoi9495+YZEWEBn878K zSe#w`_;^zCKjpH^?>|6v(p%hR07QTbNaXsv1HhM`+I^LTzvC#P(C+dA{|ltK2shN&N$6@Mu$1g#x zdSGP7_Y7FLS)oa^d;nLk?^|C6lDps;`;{s%98z1p?i9HZG&#U@B1DU_dt7hQ~xAzSr(-}txQ#h zx1oBwa1A&q1^>k=I&%-&YLbRx-tK1gXcoUj4!P)cr`Dg3@xs!1i>_wlnbPreoeys- zG#&RgmKmSs)*^~W(eZKxUo=Ql@VnBy$x#-jd@>`O(Y!%X3QD+mSroz#*{aAZ?@qmP zV)@OE@Rd5Ut64uL52dA@eIJTz1#U9^;jcUw7yQQhCULaX)+1O{?m9f1k0hJ%GM%ux zIWmjrW@A#98DCP4`HnnqEq9!&At||``3m;+)o4~0@--({F-4FY#17p_%NlXj|JJFw zQYvW;O&i6CBnAlrBd73mX+!zXFPA!_*&jq|*T#5rgwh6v4wpEJVViUpQaOGlAoEc1 zBt9KQ$I8)sgd=&!hWH9jOA4kma5(8=xl`CiKSlDqZKG|is@4H7%;t-p1#7N3iZ-bq z#|p~KUHVny$n@)=QPlN4qy%LKDN)5xtC~t1IBy0lff9o=E(YObWh{!56PlO)#s(qJ z4M$`!9;NGEMK!%DcSvBOk(|(cx01U@0xP5x`f;>&MZx4X29r7#Ckkfs6wGo26(F|n zi!IRc_sY99`%V4gJQ{R!!BUWbYkXZ>)hIFJHL{1qH8rTKSpCZ(Z>do0D9yeW&>{)A z?IhJ3(B}TG=CbJ-+5~=Z5W#45yLjpfN<1^(sG{!(y6={D!2Sf?#!@$EJ;IwLT{&{X zfoCKvhiO1#1zme9Lo12E)~lOoCki*%6zmNzBK}fp8cO>8SnbIC0?S0N$4g5rb)kp- zx(=aBGnQU?E(%B+6>X|@;;{~ELkiX-Gjxp4@4&{MNLt?zB+zY? zXGm1JdH1CK&qPMhFZX}=1iESUoFrPB&@npnQS@UeiHE!9{_e?di4Bg?bLw$Cyr$O{ zp?J}JPq~|G6xB`YySr18UG1$LqIAvXU-Dp71JHc)T?=a)bs^OkNhNBIn{{jovxAyB zn}PTy20y_;B;rbJEe44;rB!K?+}%G$Y{nCj%E!6+O3q!*H6jh13?d!#xd>Fl;HyL~ zW92ONapFYtLaZ#ZCIaK_?M24lxOzCa?BXh#6BiYA0w=~ZhtHZ5?sC%3>(?qQ2Nl>U zf+076P}3x-s#X6~+IW6aAa@eyQKl-cNIUJ2Ys2O^u8XMfj3K4;LZ({RDcvYFfTqv= z`XThoCpSQgbVNLwzHB*s1UqQL-5f+hE$h0rNCZ%;E%rR&`;gxV?w; zHW0S=BL7($mdwhntf_gn>}zUneql1)H?X2HGs8~p<-8YF@v|+u1FK{-CQdy1D<46( z0Uce;=t@)PW9yjSPRIfRv$Ps5D?HN{^IGD%6lcQ|#Q9(p!it&^ft%HU`DAGneNDU| z?6PjzT;oKy$9@4pXTJw`jF)E&6In;Qh?lAfp15Iu)Z88XnwL;# ztz?TW)4hZAO@@hMs0OxE!zN~8Jh0iBt)ErNS!H8I$V|0dE$i0YpzFqBtAK2j-I6h! z@Su7G!zfcHETfPVY`4( zw0#WBcO?R;Bw;WT1~}G9{tRDVaRRE{o#RhURULtIJbW-VY7T7h{QveG_D&xA{wVSt tKX?B@z#jy>+sBo6{pRWatON8%sw^icGpFOD;5TXK&v>0~_&VhKe*krZ1FQf5 literal 0 HcmV?d00001 diff --git a/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png b/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png index b1448cd7cb2effd015e309ddc666fde950297f6b..b8ffa6e8576fe7a717bdcba664b67cb77f05438f 100644 GIT binary patch literal 1993 zcmeAS@N?(olHy`uVBq!ia0y~yU^&9Tz^KE)1{Bee&)>wrz&^p##WAFU@$ErIMg|22 zCWX)cpTBB9urnU0f+4|$8ALftfk=rzgb_$KDDr|hJeeRe!DCd* uXo!s_n9=+)T3n2lmZOyguu3GYu4KlRkg`HpSrmBX*|E4z}@l zKl|vXzwQe-A9I~&zW*8NqCu8-R%Th*pKfUXSmcAp;VG)J@m-(i3jO@8sG|#0HO`fx zi804;<*wnc4t#nq5}WCR!*!Vd_=!EXC&RH}Wo~*t=Tviv;mwHGBkK&&PkiFX&^Gw; ztO2B#kJ+O?11EN(>|hy6zp~wrvh%-ugtDw}QTpR8)CKV6H`|A{1F>BZ|GhP&+zh*5 zV{n<>e+hs$>Z+~BHx#;S!_GCaiP3N&6}B8gPgXEOWX$(`jAkuaYyrTUXaazm5cX{E z>*(Qz_f0y%UdBXOsZu?**~p4{LU)^-Dpke3X4`fiRTl^!yj=m{F#d)ikVMe5N`WP7 zAg~UqC%4VKR7H=!<-T%L*Hg&vo8uCm-5i8ciUXzq(B8en#&=bEJ#;k0YDdhKO?F;( zEz1bSx~N`VwF~aYxdI?93*%f0L!pJX%n1nVN>!A+W8eLMYS*n3tpS*h*aHv#Lx0*x>ySo0DYy zCh_$&QXuH5IxduRkPv_ISiPf^MnR9DA8dgKPD4rTN02+PON8qp-Yo@ka z$Q~tRED7 zkZ01+Mr~b&C(HN9#byswf-gMKe<4}HKZVstksUpen(&lILzn{?t6t^P6mnK(_LT6J zS9e`8Bk|a=&f1v)Z`%Bab&4Er8dWvxep4*07Q%RsnHU3-=nXZ+l_FZK&CDY5P!zGy zQQEArQ`gm<#$+bbecpBbaqGltwRB)0AgTV+#8tmDnH1894jWBE-Lz>{PBO;QJ#+V9 zT|LxtV&U^feT;~KoMj|-W>@GR-$1xfYi4G4G%B4RvDIx^kNmLN(GpA^FXw1Fk$0QP z?T*Eo!v5&kSbHi_TVfK<)Y{QT-Kc%fdvl}`nEQ(@lQPS;%`Spy(BH$Qv~5QFyRQ`% z;8D!*NOcaKq8N^B6NAVFeg~g5$xhD;;I!nQi)71j)`Sbgm&ix`Me^ZlYMJ zZj4IaTBr!2$XD@OG?*{il%T#Tq3<{3%@=v8b1hiI;|_n#<>qkg^EsP?P4nVnCB2&A zn}(rOXxbFw?*e6 zGHOdoOANtbVh|iTKP6XfPO_9YUHEkA^Iqo~_Qu_zp*?A;I|PwAL4Sv}zTqY1+F9oB z^LKShw`2uuEqjdhN>uHdJfvNp#+XzS^I#UG?y=fGG_fa3q`K1T@M62S%f!vE(VE0^ zhGJ0L68|*aSJu6-QjCu1HKWqzxaFMX7lXut(6P4F^r3y7)^!BK;YKgP6|2BN7zZpI z!H3PPIEn{u%JFMUl51ws@|*cWUUHFsOX@!R{bXPegFw|>3z;s|C(vLmJF69SdIv-N z*it2Hwb^3%B{8fZozrmWlo?TYWst~T zt?JEn*ePO+KzkkD;%XOVEwxA&CCa}~Dpihd%=N$ZO^*&qj#u27>do$|nrm7YTPDp6 zLhKG<4W7Bz_n1m6TJU4H52a&!U})B!jug&!zc`dp=aEbm+nvgHkcm=YvnV)v|2pM@ z@4w%;xaVNKBk{Aj9SmolQ>akihbk*wv#^%s sJ~7`3=h*g5bo)xa{WkgkzPC2bJ{A!wZ5^bGWl3aHLfR@Kl_s64rC0(?S)yf?ge5?TwXF_F zDS`rl5L!V}l!k}Bgb+Z7og_la5&{VtAOr{jvO&l)_j6|Y%-?zDNB{We-rW1$bMHCt zdEawyvOmH2TfTJQB?y8n15Tg(6oPjB1A=xOdEt35bHpK`0fJsR7I5-|b7?yi-Q*?* zBF=c{XR^GICDR@^~+Kz}VZBxCmTKGV-<8MPSePyLq{YldTm@~a=swcQMCXxPt~ z9Kodj!^(s3TR}n2lMh_4Ek;Fj?IQiXJ4NX+IXU^|ijAFSTbjy?vxrRf>DQ7wLJR5J z=pEqrE2qwZ7W(mxeQ{ zghz{aImQWs*y;M6P}lwV$jFb=Px<$ax_kZV1!gzi0nNQ}Cazf4n4(|-tHEV!2&%hB z-qzlY>At!b88Z$LY;d>pj34BXx1WQ$-mL>oYENx2QsADUtpz4aa$q9acj!wGA#DD0IRpMP#Rd_{Ru-b(BEGgNcZvHV?zg^hK3q< zK(qT=FH5W8^H3@oe`s=YK}{nq$f?JYzgjhGcM3aVQd!5!F{e(AN9x@Ks)a)A;8gQ6 zLVU*$�?&fuO)Z3IwJ3&5(5+a^f+0aMR+7TBC^y56`k9PSr4~mQ4xh{F(L?MvRU* z6WpGHvuXlizij1f1wnhy!VVx24Tk|@I5L@RYac@6a5(G|0S4vx%{MoY#uB-$au|u_ zKll!RSJrt@_$LTDTn!=|`m!&tT-Xwq!GeRldDC4^%^Fr8xj;-gywr3SJCJskf#J># z6bc&2A}s{Dy1}tJ+S-X|BO~={pN;-&xvk;d*;MzKXsfZ|U?jYZs1P-C6# zafApvo6jvj?i3MGCeXl}0<2L48Sh2F6cosvm9VSJpkA4-N5YQq!2qyg3Wn1W#<0d8 z`!WBwN=7tr1!N~~3DCtwNX63*0#obX1$E_N@8aU;fSdr(pYDTTC-(z@>g%~+pGW6m zz5SQ^t`zqG`sC);!al_PzQ_M0N;s}4HxT6Br_H}5aTE^dBMLas%K$fTHRjP@1ydAw zesEn-&5$v4tzuD6}hoMU#Ffh)(&>l}_6}fy-+9RXg4jMXS=kYu=n+U-1`Y} zxjLn%xU?)jxO{z;qvOOLn4!`A56+ZaKPb*;wktbau&a|)1$MO`Z4zjN%`vG~M5ATv zT#Lvb1YNag4RyT|KGjr-#A}C-*dE|*M4eG(7QJlAh~ceJ+g2syM>*}I8WxA7t@j}{KUX%i z3I^QoGly;$hm{5t9*&y7x4bdlgO`2#HA#MfynXuMj8`e_$!AQKGgdKuyE3n|K$@3< zGZ}loP}Ton6{TIF3Ogwj-?Yb$TXd7jK6N^`_H$Ak1m*1lD)agbHcKB9;{g0<{oKZpu$H5I8Meq2T8*tJVgie;UnHBiY014ca@K~R zf?pA0stF->TQmTmRArr})gwO3YQt1UmK)Zm9%Zpy5>YKHN9zjPR7C2mPlSg2FV`WK+}iJ zXsx>Y%*q68-)n@+sx@s|^ zQl7qTokj6AQeEa!(T4g=MN2{?z?Ss3czZ}WEXBt13x0ZB8vlz^>1tiyES_8&Ry@U zRhs(Td2pv%jrlvLQRw_Fv^kGvRw?_rtz!DVck8%0wkr+nNh`bTzdsC?fh+yUV z;AG}N77(@%3axHYa?b~a71EDShzqm#2NSRhJSxy~w5+!*VW>lr{yHuwWJ|aL%+ZDu z==}MiqH39@&Y?e|tap^aMQ1wl@uO7%w#p?J=HOKFvZ|;NGgLu-QYk<1nyBELb?SXi z2_0sZJ(7HZ6^eF6drHzX(pOm-WUJm>+r9;C-6k>Jv~f0QH#B?tkppz)(4#QZR^LYV z9+vr&SCRdhevJY*?vBW^w*#!2dt4(zppKq|sJ zLzjX8WFs19NeSE>7qa)6Yu5yt7L~Zv9Af0!Uo^4X8T(@qYK-+14qs z6I}+W>PsfuFlx~5rsMhpQ;Fk>};!o}%R4Xj>_;hTVc|&T?|! z!M(uq?kbrH3!bIxpd1S8r8VqJ-FAq-MwGQin@vXVzlDePirQNX{XJrbFEQx1cE*Xc zkM@WAs?6#@Dh)iD-sa=SYo$r9c@@W|4mY>Ju_ literal 0 HcmV?d00001 diff --git a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_empty_data.json similarity index 89% rename from test/interpreter_functional/snapshots/session/metric_single_metric_data.json rename to test/interpreter_functional/snapshots/baseline/metric_empty_data.json index f4a8cd1f14e1..c318121535c8 100644 --- a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_empty_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_invalid_data.json b/test/interpreter_functional/snapshots/baseline/metric_invalid_data.json index c7b4a0325dc9..f23b9b091577 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_invalid_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_invalid_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[],"meta":{},"rows":[],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +"[metricVis] > [visdimension] > Column name or index provided is invalid" \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_1.json b/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json similarity index 65% rename from test/interpreter_functional/snapshots/session/partial_test_1.json rename to test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json index 082c7b934c17..6dd90a4a6ca0 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_1.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json index 3e594380588d..b5ae1a2cb59f 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[],"meta":{},"rows":[],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +"[tagcloud] > [visdimension] > Column name or index provided is invalid" \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_empty_data.json b/test/interpreter_functional/snapshots/session/tagcloud_empty_data.json new file mode 100644 index 000000000000..6dd90a4a6ca0 --- /dev/null +++ b/test/interpreter_functional/snapshots/session/tagcloud_empty_data.json @@ -0,0 +1 @@ +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json b/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json index 3e594380588d..b5ae1a2cb59f 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[],"meta":{},"rows":[],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +"[tagcloud] > [visdimension] > Column name or index provided is invalid" \ No newline at end of file diff --git a/test/interpreter_functional/test_suites/run_pipeline/metric.ts b/test/interpreter_functional/test_suites/run_pipeline/metric.ts index bbaf0486f4fb..5483e09d6671 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/metric.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/metric.ts @@ -30,10 +30,13 @@ export default function ({ dataContext = await expectExpression('partial_metric_test', expression).getResponse(); }); - it('with invalid data', async () => { + it('with empty data', async () => { const expression = 'metricVis metric={visdimension 0}'; await ( - await expectExpression('metric_invalid_data', expression).toMatchSnapshot() + await expectExpression('metric_empty_data', expression, { + ...dataContext, + rows: [], + }).toMatchSnapshot() ).toMatchScreenshot(); }); @@ -78,5 +81,14 @@ export default function ({ ).toMatchScreenshot(); }); }); + + describe('throws error at metric', () => { + it('with invalid data', async () => { + const expression = 'metricVis metric={visdimension 0}'; + await ( + await expectExpression('metric_invalid_data', expression).toMatchSnapshot() + ).toMatchScreenshot(); + }); + }); }); } diff --git a/test/interpreter_functional/test_suites/run_pipeline/tag_cloud.ts b/test/interpreter_functional/test_suites/run_pipeline/tag_cloud.ts index 05bbd33fedad..3358e45dc02d 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/tag_cloud.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/tag_cloud.ts @@ -15,24 +15,25 @@ export default function ({ }: FtrProviderContext & { updateBaselines: boolean }) { let expectExpression: ExpectExpression; describe('tag cloud pipeline expression tests', () => { - before(() => { + let dataContext: ExpressionResult; + before(async () => { expectExpression = expectExpressionProvider({ getService, updateBaselines }); + + const expression = `kibana | kibana_context | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggCount id="1" enabled=true schema="metric"} + aggs={aggTerms id="2" enabled=true schema="segment" field="response.raw" size=4 order="desc" orderBy="1"}`; + // we execute the part of expression that fetches the data and store its response + dataContext = await expectExpression('partial_tagcloud_test', expression).getResponse(); }); describe('correctly renders tagcloud', () => { - let dataContext: ExpressionResult; - before(async () => { - const expression = `kibana | kibana_context | esaggs index={indexPatternLoad id='logstash-*'} - aggs={aggCount id="1" enabled=true schema="metric"} - aggs={aggTerms id="2" enabled=true schema="segment" field="response.raw" size=4 order="desc" orderBy="1"}`; - // we execute the part of expression that fetches the data and store its response - dataContext = await expectExpression('partial_tagcloud_test', expression).getResponse(); - }); - - it('with invalid data', async () => { + it('with empty data', async () => { const expression = 'tagcloud metric={visdimension 0}'; await ( - await expectExpression('tagcloud_invalid_data', expression).toMatchSnapshot() + await expectExpression('tagcloud_empty_data', expression, { + ...dataContext, + rows: [], + }).toMatchSnapshot() ).toMatchScreenshot(); }); @@ -66,5 +67,14 @@ export default function ({ ).toMatchScreenshot(); }); }); + + describe('throws error at tagcloud', () => { + it('with invalid data', async () => { + const expression = 'tagcloud metric={visdimension 0}'; + await ( + await expectExpression('tagcloud_invalid_data', expression).toMatchSnapshot() + ).toMatchScreenshot(); + }); + }); }); } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/index.ts index 492584904521..667854bf3e7e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/index.ts @@ -31,6 +31,7 @@ import { timeFilter } from './time_filter'; import { verticalBarChart } from './vert_bar_chart'; import { verticalProgressBar } from './vertical_progress_bar'; import { verticalProgressPill } from './vertical_progress_pill'; +import { tagCloud } from './tag_cloud'; import { SetupInitializer } from '../plugin'; import { ElementFactory } from '../../types'; @@ -60,6 +61,7 @@ const elementSpecs = [ verticalBarChart, verticalProgressBar, verticalProgressPill, + tagCloud, ]; const initializeElementFactories = [metricElementInitializer]; @@ -69,6 +71,5 @@ export const initializeElements: SetupInitializer = (core, plu ...elementSpecs, ...initializeElementFactories.map((factory) => factory(core, plugins)), ]; - return applyElementStrings(specs); }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/tag_cloud/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/tag_cloud/index.ts new file mode 100644 index 000000000000..a0b464390fa2 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/tag_cloud/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ElementFactory } from '../../../types'; + +export const tagCloud: ElementFactory = () => ({ + name: 'tagCloud', + displayName: 'Tag Cloud', + type: 'chart', + help: 'Tagcloud visualization', + icon: 'visTagCloud', + expression: `filters + | demodata + | head 150 + | ply by="country" expression={math "count(country)" | as "Count"} + | tagcloud metric={visdimension "Count"} bucket={visdimension "country"} + | render`, +}); diff --git a/x-pack/plugins/canvas/i18n/elements/element_strings.ts b/x-pack/plugins/canvas/i18n/elements/element_strings.ts index 87879c4c753c..e1540572f4af 100644 --- a/x-pack/plugins/canvas/i18n/elements/element_strings.ts +++ b/x-pack/plugins/canvas/i18n/elements/element_strings.ts @@ -222,4 +222,12 @@ export const getElementStrings = (): ElementStringDict => ({ defaultMessage: 'Displays progress as a portion of a vertical pill', }), }, + tagCloud: { + displayName: i18n.translate('xpack.canvas.elements.tagCloudDisplayName', { + defaultMessage: 'Tag Cloud', + }), + help: i18n.translate('xpack.canvas.elements.tagCloudHelpText', { + defaultMessage: 'Tagcloud visualization', + }), + }, }); diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index c149c6754486..b8f0b17f814d 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -105,6 +105,7 @@ export class CanvasPlugin mount: async (params: AppMountParameters) => { const { CanvasSrcPlugin } = await import('../canvas_plugin_src/plugin'); const srcPlugin = new CanvasSrcPlugin(); + srcPlugin.setup(coreSetup, { canvas: canvasApi }); setupExpressions({ coreSetup, setupPlugins }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ffd3adce378f..0a8d10d01469 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5021,7 +5021,6 @@ "visualizations.function.range.help": "範囲オブジェクトを生成します", "visualizations.function.range.to.help": "範囲の終了", "visualizations.function.visDimension.accessor.help": "使用するデータセット内の列 (列インデックスまたは列名) ", - "visualizations.function.visDimension.error.accessor": "入力された列名は無効です。", "visualizations.function.visDimension.format.help": "フォーマット", "visualizations.function.visDimension.formatParams.help": "フォーマットパラメーター", "visualizations.function.visDimension.help": "visConfig ディメンションオブジェクトを生成します", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0fc6f0ec119e..d29307eddd42 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5041,7 +5041,6 @@ "visualizations.function.range.help": "生成范围对象", "visualizations.function.range.to.help": "范围结束", "visualizations.function.visDimension.accessor.help": "要使用的数据集列(列索引或列名称)", - "visualizations.function.visDimension.error.accessor": "提供的列名称无效", "visualizations.function.visDimension.format.help": "格式", "visualizations.function.visDimension.formatParams.help": "格式参数", "visualizations.function.visDimension.help": "生成 visConfig 维度对象", From 6f31422d9f2b24a7b64f762f65762de27f365170 Mon Sep 17 00:00:00 2001 From: Gloria Hornero Date: Mon, 6 Sep 2021 11:38:53 +0200 Subject: [PATCH 45/47] adds missing field when creating the email connector (#111251) --- x-pack/plugins/security_solution/cypress/objects/connector.ts | 2 ++ .../security_solution/cypress/screens/create_new_rule.ts | 2 ++ .../plugins/security_solution/cypress/tasks/create_new_rule.ts | 2 ++ 3 files changed, 6 insertions(+) diff --git a/x-pack/plugins/security_solution/cypress/objects/connector.ts b/x-pack/plugins/security_solution/cypress/objects/connector.ts index a5244583bf49..5c2abeab0602 100644 --- a/x-pack/plugins/security_solution/cypress/objects/connector.ts +++ b/x-pack/plugins/security_solution/cypress/objects/connector.ts @@ -12,6 +12,7 @@ export interface EmailConnector { port: string; user: string; password: string; + service: string; } export const getEmailConnector = (): EmailConnector => ({ @@ -21,4 +22,5 @@ export const getEmailConnector = (): EmailConnector => ({ port: '80', user: 'username', password: 'password', + service: 'Other', }); diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index 551857ca3bfc..4748a48dbeb1 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -40,6 +40,8 @@ export const EMAIL_CONNECTOR_USER_INPUT = '[data-test-subj="emailUserInput"]'; export const EMAIL_CONNECTOR_PASSWORD_INPUT = '[data-test-subj="emailPasswordInput"]'; +export const EMAIL_CONNECTOR_SERVICE_SELECTOR = '[data-test-subj="emailServiceSelectInput"]'; + export const ADD_FALSE_POSITIVE_BTN = '[data-test-subj="detectionEngineStepAboutRuleFalsePositives"] .euiButtonEmpty__text'; 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 e2d27a11ed71..c1210bf457b6 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 @@ -92,6 +92,7 @@ import { EMAIL_CONNECTOR_PORT_INPUT, EMAIL_CONNECTOR_USER_INPUT, EMAIL_CONNECTOR_PASSWORD_INPUT, + EMAIL_CONNECTOR_SERVICE_SELECTOR, } from '../screens/create_new_rule'; import { LOADING_INDICATOR } from '../screens/security_header'; import { TOAST_ERROR } from '../screens/shared'; @@ -402,6 +403,7 @@ export const fillIndexAndIndicatorIndexPattern = ( export const fillEmailConnectorForm = (connector: EmailConnector = getEmailConnector()) => { cy.get(CONNECTOR_NAME_INPUT).type(connector.name); + cy.get(EMAIL_CONNECTOR_SERVICE_SELECTOR).select(connector.service); cy.get(EMAIL_CONNECTOR_FROM_INPUT).type(connector.from); cy.get(EMAIL_CONNECTOR_HOST_INPUT).type(connector.host); cy.get(EMAIL_CONNECTOR_PORT_INPUT).type(connector.port); From 00fac96d370fdb6df71c9f8aef2114e19d76717a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Mon, 6 Sep 2021 12:20:35 +0200 Subject: [PATCH 46/47] [APM] Uses doc link service instead of ElasticDocsLink for linking metadata (#110992) --- .../kibana-plugin-core-public.doclinksstart.links.md | 1 + .../public/kibana-plugin-core-public.doclinksstart.md | 2 +- src/core/public/doc_links/doc_links_service.ts | 3 +++ src/core/public/public.api.md | 1 + .../apm/public/components/shared/MetadataTable/index.tsx | 8 +++++--- 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index bc6075176cd2..253b0671cdd5 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -12,6 +12,7 @@ readonly links: { readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; + readonly metaData: string; }; readonly canvas: { readonly guide: string; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index aa3f95801804..7e409f23790f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
readonly suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly libbeat: {
readonly getStarted: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly privileges: string;
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
readonly autocompleteChanges: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
readonly ecs: {
readonly guide: string;
};
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly apm: {
readonly kibanaSettings: string;
readonly supportedServiceMaps: string;
readonly metaData: string;
};
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
readonly suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly libbeat: {
readonly getStarted: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly privileges: string;
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
readonly autocompleteChanges: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
readonly ecs: {
readonly guide: string;
};
} | | diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index f3ef7c550e57..73ba816ff9b4 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -24,6 +24,7 @@ export class DocLinksService { const KIBANA_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/`; const FLEET_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/fleet/${DOC_LINK_VERSION}/`; const PLUGIN_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/plugins/${DOC_LINK_VERSION}/`; + const APM_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/apm/`; return deepFreeze({ DOC_LINK_VERSION, @@ -33,6 +34,7 @@ export class DocLinksService { apm: { kibanaSettings: `${KIBANA_DOCS}apm-settings-in-kibana.html`, supportedServiceMaps: `${KIBANA_DOCS}service-maps.html#service-maps-supported`, + metaData: `${APM_DOCS}get-started/${DOC_LINK_VERSION}/metadata.html`, }, canvas: { guide: `${KIBANA_DOCS}canvas.html`, @@ -458,6 +460,7 @@ export interface DocLinksStart { readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; + readonly metaData: string; }; readonly canvas: { readonly guide: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index f18e1dc26bd8..d9c64f29eb68 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -476,6 +476,7 @@ export interface DocLinksStart { readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; + readonly metaData: string; }; readonly canvas: { readonly guide: string; diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx index 938d5f451943..45be525512d0 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx @@ -10,6 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, + EuiLink, EuiSpacer, EuiText, EuiTitle, @@ -19,11 +20,11 @@ import { isEmpty } from 'lodash'; import React, { useCallback } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; import { HeightRetainer } from '../HeightRetainer'; import { fromQuery, toQuery } from '../Links/url_helpers'; import { filterSectionsByTerm, SectionsWithRows } from './helper'; import { Section } from './Section'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; interface Props { sections: SectionsWithRows; @@ -34,6 +35,7 @@ export function MetadataTable({ sections }: Props) { const location = useLocation(); const { urlParams } = useUrlParams(); const { searchTerm = '' } = urlParams; + const { docLinks } = useApmPluginContext().core; const filteredSections = filterSectionsByTerm(sections, searchTerm); @@ -55,11 +57,11 @@ export function MetadataTable({ sections }: Props) { - + How to add labels and other data - + Date: Mon, 6 Sep 2021 13:44:47 +0200 Subject: [PATCH 47/47] [Canvas/Reporting] Migrate Canvas to V2 reporting (#109860) * first iteration of canvas reporting using v2 PDF generator * updated jest test * made v2 report URLs compatible with spaces and simplified some code * remove non-existent import * updated import of lib Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/share/public/index.ts | 1 + .../url_service/redirect/redirect_manager.ts | 12 +- x-pack/plugins/canvas/common/index.ts | 2 + x-pack/plugins/canvas/common/locator.ts | 45 ++++++ x-pack/plugins/canvas/kibana.json | 3 +- .../workpad_header/share_menu/share_menu.tsx | 4 +- .../workpad_header/share_menu/utils.test.ts | 132 +++++++++++++++--- .../workpad_header/share_menu/utils.ts | 25 ++-- x-pack/plugins/canvas/public/plugin.tsx | 11 +- .../public/services/kibana/reporting.ts | 2 +- .../canvas/public/services/reporting.ts | 2 +- x-pack/plugins/canvas/tsconfig.json | 11 +- .../reporting/common/build_kibana_path.ts | 18 +++ x-pack/plugins/reporting/common/constants.ts | 8 +- .../public/redirect/redirect_app.tsx | 2 +- .../chromium/driver/chromium_driver.ts | 2 +- .../common/v2/get_full_redirect_app_url.ts | 33 +++++ .../export_types/common/v2/get_full_urls.ts | 34 ----- .../server/export_types/png_v2/execute_job.ts | 7 +- .../printable_pdf_v2/execute_job.ts | 2 +- .../printable_pdf_v2/lib/generate_pdf.ts | 10 +- 21 files changed, 269 insertions(+), 97 deletions(-) create mode 100644 x-pack/plugins/canvas/common/locator.ts create mode 100644 x-pack/plugins/reporting/common/build_kibana_path.ts create mode 100644 x-pack/plugins/reporting/server/export_types/common/v2/get_full_redirect_app_url.ts delete mode 100644 x-pack/plugins/reporting/server/export_types/common/v2/get_full_urls.ts diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts index 1f999b59ddb6..74e849948d41 100644 --- a/src/plugins/share/public/index.ts +++ b/src/plugins/share/public/index.ts @@ -31,6 +31,7 @@ export { UrlGeneratorsService, } from './url_generators'; +export { RedirectOptions } from './url_service'; export { useLocatorUrl } from '../common/url_service/locators/use_locator_url'; import { SharePlugin } from './plugin'; diff --git a/src/plugins/share/public/url_service/redirect/redirect_manager.ts b/src/plugins/share/public/url_service/redirect/redirect_manager.ts index cc45e0d3126a..a5d895c7cbcf 100644 --- a/src/plugins/share/public/url_service/redirect/redirect_manager.ts +++ b/src/plugins/share/public/url_service/redirect/redirect_manager.ts @@ -15,7 +15,15 @@ import type { UrlService } from '../../../common/url_service'; import { render } from './render'; import { parseSearchParams } from './util/parse_search_params'; -export interface RedirectOptions { +/** + * @public + * Serializable locator parameters that can be used by the redirect service to navigate to a location + * in Kibana. + * + * When passed to the public {@link SharePluginSetup['navigate']} function, locator params will also be + * migrated. + */ +export interface RedirectOptions

{ /** Locator ID. */ id: string; @@ -23,7 +31,7 @@ export interface RedirectOptions { version: string; /** Locator params. */ - params: unknown & SerializableRecord; + params: P; } export interface RedirectManagerDependencies { diff --git a/x-pack/plugins/canvas/common/index.ts b/x-pack/plugins/canvas/common/index.ts index 51a53586dee3..5bae69e8601b 100644 --- a/x-pack/plugins/canvas/common/index.ts +++ b/x-pack/plugins/canvas/common/index.ts @@ -8,3 +8,5 @@ export const UI_SETTINGS = { ENABLE_LABS_UI: 'labs:canvas:enable_ui', }; + +export { CANVAS_APP_LOCATOR, CanvasAppLocator, CanvasAppLocatorParams } from './locator'; diff --git a/x-pack/plugins/canvas/common/locator.ts b/x-pack/plugins/canvas/common/locator.ts new file mode 100644 index 000000000000..147e4fd86098 --- /dev/null +++ b/x-pack/plugins/canvas/common/locator.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { LocatorDefinition, LocatorPublic } from 'src/plugins/share/common'; + +import { CANVAS_APP } from './lib/constants'; + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type CanvasAppLocatorParams = { + view: 'workpadPDF'; + id: string; + page: number; +}; + +export type CanvasAppLocator = LocatorPublic; + +export const CANVAS_APP_LOCATOR = 'CANVAS_APP_LOCATOR'; + +export class CanvasAppLocatorDefinition implements LocatorDefinition { + id = CANVAS_APP_LOCATOR; + + public async getLocation(params: CanvasAppLocatorParams) { + const app = CANVAS_APP; + + if (params.view === 'workpadPDF') { + const { id, page } = params; + + return { + app, + path: `#/export/workpad/pdf/${id}/page/${page}`, + state: {}, + }; + } + + return { + app, + path: '#/', + state: {}, + }; + } +} diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json index 201fb5ab8f78..772c030e1153 100644 --- a/x-pack/plugins/canvas/kibana.json +++ b/x-pack/plugins/canvas/kibana.json @@ -25,7 +25,8 @@ "features", "inspector", "presentationUtil", - "uiActions" + "uiActions", + "share" ], "optionalPlugins": ["home", "reporting", "usageCollection"], "requiredBundles": [ diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx index ca8f5fd4e3e4..50a3890673ff 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx @@ -46,9 +46,7 @@ export const ShareMenu = () => { ReportingPanelPDFComponent !== null ? ({ onClose }: { onClose: () => void }) => ( - getPdfJobParams(sharingData, platformService.getBasePathInterface()) - } + getJobParams={() => getPdfJobParams(sharingData, platformService.getKibanaVersion())} layoutOption="canvas" onClose={onClose} /> diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.test.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.test.ts index 51d1b9abc566..18c348aec18e 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.test.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.test.ts @@ -9,17 +9,11 @@ jest.mock('../../../../common/lib/fetch'); import { getPdfJobParams } from './utils'; import { workpads } from '../../../../__fixtures__/workpads'; -import { IBasePath } from 'kibana/public'; -const basePath = ({ - prepend: jest.fn().mockImplementation((s) => `basepath/s/spacey/${s}`), - get: () => 'basepath/s/spacey', - serverBasePath: `basepath`, -} as unknown) as IBasePath; const workpadSharingData = { workpad: workpads[0], pageCount: 12 }; test('getPdfJobParams returns the correct job params for canvas layout', () => { - const jobParams = getPdfJobParams(workpadSharingData, basePath); + const jobParams = getPdfJobParams(workpadSharingData, 'v99.99.99'); expect(jobParams).toMatchInlineSnapshot(` Object { "layout": Object { @@ -29,21 +23,117 @@ test('getPdfJobParams returns the correct job params for canvas layout', () => { }, "id": "canvas", }, - "objectType": "canvas workpad", - "relativeUrls": Array [ - "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/1", - "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/2", - "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/3", - "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/4", - "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/5", - "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/6", - "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/7", - "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/8", - "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/9", - "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/10", - "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/11", - "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/12", + "locatorParams": Array [ + Object { + "id": "CANVAS_APP_LOCATOR", + "params": Object { + "id": "base-workpad", + "page": 1, + "view": "workpadPDF", + }, + "version": "v99.99.99", + }, + Object { + "id": "CANVAS_APP_LOCATOR", + "params": Object { + "id": "base-workpad", + "page": 2, + "view": "workpadPDF", + }, + "version": "v99.99.99", + }, + Object { + "id": "CANVAS_APP_LOCATOR", + "params": Object { + "id": "base-workpad", + "page": 3, + "view": "workpadPDF", + }, + "version": "v99.99.99", + }, + Object { + "id": "CANVAS_APP_LOCATOR", + "params": Object { + "id": "base-workpad", + "page": 4, + "view": "workpadPDF", + }, + "version": "v99.99.99", + }, + Object { + "id": "CANVAS_APP_LOCATOR", + "params": Object { + "id": "base-workpad", + "page": 5, + "view": "workpadPDF", + }, + "version": "v99.99.99", + }, + Object { + "id": "CANVAS_APP_LOCATOR", + "params": Object { + "id": "base-workpad", + "page": 6, + "view": "workpadPDF", + }, + "version": "v99.99.99", + }, + Object { + "id": "CANVAS_APP_LOCATOR", + "params": Object { + "id": "base-workpad", + "page": 7, + "view": "workpadPDF", + }, + "version": "v99.99.99", + }, + Object { + "id": "CANVAS_APP_LOCATOR", + "params": Object { + "id": "base-workpad", + "page": 8, + "view": "workpadPDF", + }, + "version": "v99.99.99", + }, + Object { + "id": "CANVAS_APP_LOCATOR", + "params": Object { + "id": "base-workpad", + "page": 9, + "view": "workpadPDF", + }, + "version": "v99.99.99", + }, + Object { + "id": "CANVAS_APP_LOCATOR", + "params": Object { + "id": "base-workpad", + "page": 10, + "view": "workpadPDF", + }, + "version": "v99.99.99", + }, + Object { + "id": "CANVAS_APP_LOCATOR", + "params": Object { + "id": "base-workpad", + "page": 11, + "view": "workpadPDF", + }, + "version": "v99.99.99", + }, + Object { + "id": "CANVAS_APP_LOCATOR", + "params": Object { + "id": "base-workpad", + "page": 12, + "view": "workpadPDF", + }, + "version": "v99.99.99", + }, ], + "objectType": "canvas workpad", "title": "base workpad", } `); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts index bbd6b42a3833..311ef73e1c97 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts @@ -5,8 +5,9 @@ * 2.0. */ -import { IBasePath } from 'kibana/public'; -import rison from 'rison-node'; +import type { RedirectOptions } from 'src/plugins/share/public'; +import { CANVAS_APP_LOCATOR } from '../../../../common/locator'; +import { CanvasAppLocatorParams } from '../../../../common/locator'; import { CanvasWorkpad } from '../../../../types'; export interface CanvasWorkpadSharingData { @@ -16,11 +17,8 @@ export interface CanvasWorkpadSharingData { export function getPdfJobParams( { workpad: { id, name: title, width, height }, pageCount }: CanvasWorkpadSharingData, - basePath: IBasePath + version: string ) { - const urlPrefix = basePath.get().replace(basePath.serverBasePath, ''); // for Spaces prefix, which is included in basePath.get() - const canvasEntry = `${urlPrefix}/app/canvas#`; - // The viewport in Reporting by specifying the dimensions. In order for things to work, // we need a viewport that will include all of the pages in the workpad. The viewport // also needs to include any offset values from the 0,0 position, otherwise the cropped @@ -32,9 +30,18 @@ export function getPdfJobParams( // viewport size. // build a list of all page urls for exporting, they are captured one at a time - const workpadUrls = []; + + const locatorParams: Array> = []; for (let i = 1; i <= pageCount; i++) { - workpadUrls.push(rison.encode(`${canvasEntry}/export/workpad/pdf/${id}/page/${i}`)); + locatorParams.push({ + id: CANVAS_APP_LOCATOR, + version, + params: { + view: 'workpadPDF', + id, + page: i, + }, + }); } return { @@ -43,7 +50,7 @@ export function getPdfJobParams( id: 'canvas', }, objectType: 'canvas workpad', - relativeUrls: workpadUrls, + locatorParams, title, }; } diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index b8f0b17f814d..3b4a6b6f1ee4 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -6,6 +6,7 @@ */ import { BehaviorSubject } from 'rxjs'; +import type { SharePluginSetup } from 'src/plugins/share/public'; import { ChartsPluginSetup, ChartsPluginStart } from 'src/plugins/charts/public'; import { ReportingStart } from '../../reporting/public'; import { @@ -20,7 +21,8 @@ import { import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { initLoadingIndicator } from './lib/loading_indicator'; import { getSessionStorage } from './lib/storage'; -import { SESSIONSTORAGE_LASTPATH } from '../common/lib/constants'; +import { SESSIONSTORAGE_LASTPATH, CANVAS_APP } from '../common/lib/constants'; +import { CanvasAppLocatorDefinition } from '../common/locator'; import { featureCatalogueEntry } from './feature_catalogue_entry'; import { ExpressionsSetup, ExpressionsStart } from '../../../../src/plugins/expressions/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; @@ -43,6 +45,7 @@ export { CoreStart, CoreSetup }; // This interface will be built out as we require other plugins for setup export interface CanvasSetupDeps { data: DataPublicPluginSetup; + share: SharePluginSetup; expressions: ExpressionsSetup; home?: HomePublicPluginSetup; usageCollection?: UsageCollectionSetup; @@ -97,7 +100,7 @@ export class CanvasPlugin coreSetup.application.register({ category: DEFAULT_APP_CATEGORIES.kibana, - id: 'canvas', + id: CANVAS_APP, title: 'Canvas', euiIconType: 'logoKibana', order: 3000, @@ -145,6 +148,10 @@ export class CanvasPlugin setupPlugins.home.featureCatalogue.register(featureCatalogueEntry); } + if (setupPlugins.share) { + setupPlugins.share.url.locators.create(new CanvasAppLocatorDefinition()); + } + canvasApi.addArgumentUIs(async () => { // @ts-expect-error const { argTypeSpecs } = await import('./expression_types/arg_types'); diff --git a/x-pack/plugins/canvas/public/services/kibana/reporting.ts b/x-pack/plugins/canvas/public/services/kibana/reporting.ts index 432fe5675b22..02611acdea4d 100644 --- a/x-pack/plugins/canvas/public/services/kibana/reporting.ts +++ b/x-pack/plugins/canvas/public/services/kibana/reporting.ts @@ -22,7 +22,7 @@ export const reportingServiceFactory: CanvasReportingServiceFactory = ({ const { reporting } = startPlugins; const reportingEnabled = () => ({ - getReportingPanelPDFComponent: () => reporting?.components.ReportingPanelPDF || null, + getReportingPanelPDFComponent: () => reporting?.components.ReportingPanelPDFV2 || null, }); const reportingDisabled = () => ({ getReportingPanelPDFComponent: () => null }); diff --git a/x-pack/plugins/canvas/public/services/reporting.ts b/x-pack/plugins/canvas/public/services/reporting.ts index 5369dab32bf6..9ec5bd6a06a4 100644 --- a/x-pack/plugins/canvas/public/services/reporting.ts +++ b/x-pack/plugins/canvas/public/services/reporting.ts @@ -7,7 +7,7 @@ import { ReportingStart } from '../../../reporting/public'; -type ReportingPanelPDFComponent = ReportingStart['components']['ReportingPanelPDF']; +type ReportingPanelPDFComponent = ReportingStart['components']['ReportingPanelPDFV2']; export interface CanvasReportingService { getReportingPanelPDFComponent: () => ReportingPanelPDFComponent | null; } diff --git a/x-pack/plugins/canvas/tsconfig.json b/x-pack/plugins/canvas/tsconfig.json index 87fabe2730c1..5a5a1883240b 100644 --- a/x-pack/plugins/canvas/tsconfig.json +++ b/x-pack/plugins/canvas/tsconfig.json @@ -7,7 +7,7 @@ "declarationMap": true, // the plugin contains some heavy json files - "resolveJsonModule": false, + "resolveJsonModule": false }, "include": [ "../../../typings/**/*", @@ -20,13 +20,14 @@ "shareable_runtime/**/*", "storybook/**/*", "tasks/mocks/*", - "types/**/*", + "types/**/*" ], "references": [ { "path": "../../../src/core/tsconfig.json" }, - { "path": "../../../src/plugins/bfetch/tsconfig.json"}, + { "path": "../../../src/plugins/bfetch/tsconfig.json" }, { "path": "../../../src/plugins/charts/tsconfig.json" }, - { "path": "../../../src/plugins/data/tsconfig.json"}, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/share/tsconfig.json" }, { "path": "../../../src/plugins/discover/tsconfig.json" }, { "path": "../../../src/plugins/embeddable/tsconfig.json" }, { "path": "../../../src/plugins/expressions/tsconfig.json" }, @@ -48,6 +49,6 @@ { "path": "../features/tsconfig.json" }, { "path": "../lens/tsconfig.json" }, { "path": "../maps/tsconfig.json" }, - { "path": "../reporting/tsconfig.json" }, + { "path": "../reporting/tsconfig.json" } ] } diff --git a/x-pack/plugins/reporting/common/build_kibana_path.ts b/x-pack/plugins/reporting/common/build_kibana_path.ts new file mode 100644 index 000000000000..2cb37013300c --- /dev/null +++ b/x-pack/plugins/reporting/common/build_kibana_path.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +interface Args { + basePath: string; + appPath: string; + spaceId?: string; +} + +export const buildKibanaPath = ({ basePath, appPath, spaceId }: Args) => { + return spaceId === undefined || spaceId.toLowerCase() === 'default' + ? `${basePath}${appPath}` + : `${basePath}/s/${spaceId}${appPath}`; +}; diff --git a/x-pack/plugins/reporting/common/constants.ts b/x-pack/plugins/reporting/common/constants.ts index 0e7d8f1ffe9c..9224a23fcb33 100644 --- a/x-pack/plugins/reporting/common/constants.ts +++ b/x-pack/plugins/reporting/common/constants.ts @@ -110,12 +110,10 @@ export const REPORTING_REDIRECT_LOCATOR_STORE_KEY = '__REPORTING_REDIRECT_LOCATO /** * A way to get the client side route for the reporting redirect app. * - * This route currently expects a job ID and a locator that to use from that job so that it can redirect to the - * correct page. - * - * TODO: Accommodate 'forceNow' value that some visualizations may rely on + * TODO: Add a job ID and a locator to use so that we can redirect without expecting state to + * be injected to the page */ -export const getRedirectAppPathHome = () => { +export const getRedirectAppPath = () => { return '/app/management/insightsAndAlerting/reporting/r'; }; diff --git a/x-pack/plugins/reporting/public/redirect/redirect_app.tsx b/x-pack/plugins/reporting/public/redirect/redirect_app.tsx index 60b51c0f0789..3024404dc07b 100644 --- a/x-pack/plugins/reporting/public/redirect/redirect_app.tsx +++ b/x-pack/plugins/reporting/public/redirect/redirect_app.tsx @@ -49,7 +49,7 @@ export const RedirectApp: FunctionComponent = ({ share }) => { ]; if (!locatorParams) { - throw new Error('Could not find locator for report'); + throw new Error('Could not find locator params for report'); } share.navigate(locatorParams); diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts index f14bb249524e..df91b6fe0ba4 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts @@ -121,7 +121,7 @@ export class HeadlessChromiumDriver { (key: string, value: unknown) => { Object.defineProperty(window, key, { configurable: false, - writable: false, + writable: true, enumerable: true, value, }); diff --git a/x-pack/plugins/reporting/server/export_types/common/v2/get_full_redirect_app_url.ts b/x-pack/plugins/reporting/server/export_types/common/v2/get_full_redirect_app_url.ts new file mode 100644 index 000000000000..bb640eff667e --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/common/v2/get_full_redirect_app_url.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { format } from 'url'; +import { ReportingConfig } from '../../..'; +import { getRedirectAppPath } from '../../../../common/constants'; +import { buildKibanaPath } from '../../../../common/build_kibana_path'; + +export function getFullRedirectAppUrl(config: ReportingConfig, spaceId?: string) { + const [basePath, protocol, hostname, port] = [ + config.kbnConfig.get('server', 'basePath'), + config.get('kibanaServer', 'protocol'), + config.get('kibanaServer', 'hostname'), + config.get('kibanaServer', 'port'), + ] as string[]; + + const path = buildKibanaPath({ + basePath, + spaceId, + appPath: getRedirectAppPath(), + }); + + return format({ + protocol, + hostname, + port, + pathname: path, + }); +} diff --git a/x-pack/plugins/reporting/server/export_types/common/v2/get_full_urls.ts b/x-pack/plugins/reporting/server/export_types/common/v2/get_full_urls.ts deleted file mode 100644 index bcfb06784a6d..000000000000 --- a/x-pack/plugins/reporting/server/export_types/common/v2/get_full_urls.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { parse as urlParse, UrlWithStringQuery } from 'url'; -import { ReportingConfig } from '../../../'; -import { getAbsoluteUrlFactory } from '../get_absolute_url'; -import { validateUrls } from '../validate_urls'; - -export function getFullUrls(config: ReportingConfig, relativeUrls: string[]) { - const [basePath, protocol, hostname, port] = [ - config.kbnConfig.get('server', 'basePath'), - config.get('kibanaServer', 'protocol'), - config.get('kibanaServer', 'hostname'), - config.get('kibanaServer', 'port'), - ] as string[]; - const getAbsoluteUrl = getAbsoluteUrlFactory({ basePath, protocol, hostname, port }); - - validateUrls(relativeUrls); - - const urls = relativeUrls.map((relativeUrl) => { - const parsedRelative: UrlWithStringQuery = urlParse(relativeUrl); - return getAbsoluteUrl({ - path: parsedRelative.pathname === null ? undefined : parsedRelative.pathname, - hash: parsedRelative.hash === null ? undefined : parsedRelative.hash, - search: parsedRelative.search === null ? undefined : parsedRelative.search, - }); - }); - - return urls; -} diff --git a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts index 06fdcd93e497..5e3b3117f4bb 100644 --- a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts @@ -8,7 +8,7 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { catchError, finalize, map, mergeMap, takeUntil, tap } from 'rxjs/operators'; -import { PNG_JOB_TYPE_V2, getRedirectAppPathHome } from '../../../common/constants'; +import { PNG_JOB_TYPE_V2 } from '../../../common/constants'; import { TaskRunResult } from '../../lib/tasks'; import { RunTaskFn, RunTaskFnFactory } from '../../types'; import { @@ -18,7 +18,7 @@ import { generatePngObservableFactory, setForceNow, } from '../common'; -import { getFullUrls } from '../common/v2/get_full_urls'; +import { getFullRedirectAppUrl } from '../common/v2/get_full_redirect_app_url'; import { TaskPayloadPNGV2 } from './types'; export const runTaskFnFactory: RunTaskFnFactory< @@ -39,8 +39,7 @@ export const runTaskFnFactory: RunTaskFnFactory< map((decryptedHeaders) => omitBlockedHeaders(decryptedHeaders)), map((filteredHeaders) => getConditionalHeaders(config, filteredHeaders)), mergeMap((conditionalHeaders) => { - const relativeUrl = getRedirectAppPathHome(); - const [url] = getFullUrls(config, [relativeUrl]); + const url = getFullRedirectAppUrl(config, job.spaceId); const [locatorParams] = job.locatorParams.map(setForceNow(job.forceNow)); apmGetAssets?.end(); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts index 2211e7df0877..f2cf8026c901 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts @@ -49,7 +49,7 @@ export const runTaskFnFactory: RunTaskFnFactory< apmGeneratePdf = apmTrans?.startSpan('generate_pdf_pipeline', 'execute'); return generatePdfObservable( jobLogger, - jobId, + job, title, locatorParams.map(setForceNow(job.forceNow)), browserTimezone, diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts index 9be95223a886..424a347876a1 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts @@ -9,14 +9,14 @@ import { groupBy, zip } from 'lodash'; import * as Rx from 'rxjs'; import { mergeMap } from 'rxjs/operators'; import { ReportingCore } from '../../../'; -import { getRedirectAppPathHome } from '../../../../common/constants'; import { LocatorParams, UrlOrUrlLocatorTuple } from '../../../../common/types'; import { LevelLogger } from '../../../lib'; import { createLayout, LayoutParams } from '../../../lib/layouts'; import { getScreenshots$, ScreenshotResults } from '../../../lib/screenshots'; import { ConditionalHeaders } from '../../common'; import { PdfMaker } from '../../common/pdf'; -import { getFullUrls } from '../../common/v2/get_full_urls'; +import { getFullRedirectAppUrl } from '../../common/v2/get_full_redirect_app_url'; +import type { TaskPayloadPDFV2 } from '../types'; import { getTracker } from './tracker'; const getTimeRange = (urlScreenshots: ScreenshotResults[]) => { @@ -36,7 +36,7 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) { return function generatePdfObservable( logger: LevelLogger, - jobId: string, + job: TaskPayloadPDFV2, title: string, locatorParams: LocatorParams[], browserTimezone: string | undefined, @@ -56,9 +56,7 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) { /** * For each locator we get the relative URL to the redirect app */ - const relativeUrls = locatorParams.map(() => getRedirectAppPathHome()); - const urls = getFullUrls(reporting.getConfig(), relativeUrls); - + const urls = locatorParams.map(() => getFullRedirectAppUrl(reporting.getConfig(), job.spaceId)); const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, { logger, urlsOrUrlLocatorTuples: zip(urls, locatorParams) as UrlOrUrlLocatorTuple[],