From 201b352d7f2c2b093091a47020816005011c31f5 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 22 Mar 2022 07:14:52 -0700 Subject: [PATCH 01/64] [DOCS] Add create case and update case APIs (#127936) --- docs/api/cases.asciidoc | 7 +- docs/api/cases/cases-api-create.asciidoc | 237 ++++++++++++++++++++ docs/api/cases/cases-api-update.asciidoc | 271 +++++++++++++++++++++++ 3 files changed, 513 insertions(+), 2 deletions(-) create mode 100644 docs/api/cases/cases-api-create.asciidoc create mode 100644 docs/api/cases/cases-api-update.asciidoc diff --git a/docs/api/cases.asciidoc b/docs/api/cases.asciidoc index 5e412c61926dbf..00fbedc2d12994 100644 --- a/docs/api/cases.asciidoc +++ b/docs/api/cases.asciidoc @@ -5,7 +5,7 @@ You can create, manage, configure, and send cases to external systems with these APIs: * {security-guide}/cases-api-add-comment.html[Add comment] -* {security-guide}/cases-api-create.html[Create case] +* <> * {security-guide}/cases-api-delete-case.html[Delete case] * {security-guide}/cases-api-delete-all-comments.html[Delete all comments] * {security-guide}/cases-api-delete-comment.html[Delete comment] @@ -24,5 +24,8 @@ these APIs: * {security-guide}/cases-api-push.html[Push case] * {security-guide}/assign-connector.html[Set default Elastic Security UI connector] * {security-guide}/case-api-update-connector.html[Update case configurations] -* {security-guide}/cases-api-update.html[Update case] +* <> * {security-guide}/cases-api-update-comment.html[Update comment] + +include::cases/cases-api-create.asciidoc[leveloffset=+1] +include::cases/cases-api-update.asciidoc[leveloffset=+1] \ No newline at end of file diff --git a/docs/api/cases/cases-api-create.asciidoc b/docs/api/cases/cases-api-create.asciidoc new file mode 100644 index 00000000000000..f08b69998321ff --- /dev/null +++ b/docs/api/cases/cases-api-create.asciidoc @@ -0,0 +1,237 @@ +[[cases-api-create]] +== Create case API +++++ +Create case +++++ + +Creates a case. + +=== Request + +`POST :/api/cases` + +`POST :/s//api/cases` + +=== Prerequisite + +You must have `all` privileges for the *Cases* feature in the *Management*, +*{observability}*, or *Security* section of the +<>, depending on the +`owner` of the case you're creating. + +=== Path parameters + +``:: +(Optional, string) An identifier for the space. If it is not specified, the +default space is used. + +=== Request body + +`connector`:: +(Required, object) An object that contains the connector configuration. ++ +.Properties of `connector` +[%collapsible%open] +==== +`fields`:: +(Required, object) An object containing the connector fields. ++ +-- +To create a case without a connector, specify `null`. If you want to omit any +individual field, specify `null` as its value. + +For {ibm-r} connectors, specify: + +`issueTypes`::: +(Required, array of numbers) The type of the incident. + +`severityCode`::: +(Required, number) The severity code of the incident. + +For {jira} connectors, specify: + +`issueType`::: +(Required, string) The type of the issue. + +`parent`::: +(Required, string) The key of the parent issue, when the issue type is `Sub-task`. + +`priority`::: +(Required, string) The priority of the issue. + +For {sn-itsm} connectors, specify: + +`category`::: +(Required, string) The category of the incident. + +`impact`::: +(Required, string) The effect an incident had on business. + +`severity`::: +(Required, string) The severity of the incident. + +`subcategory`::: +(Required, string) The subcategory of the incident. + +`urgency`::: +(Required, string) The extent to which the incident resolution can be delayed. + +For {sn-sir} connectors, specify: + +`category`::: +(Required, string) The category of the incident. + +`destIp`::: +(Required, string) A comma separated list of destination IPs. + +`malwareHash`::: +(Required, string) A comma separated list of malware hashes. + +`malwareUrl`::: +(Required, string) A comma separated list of malware URLs. + +`priority`::: +(Required, string) The priority of the incident. + +`sourceIp`::: +(Required, string) A comma separated list of source IPs. + +`subcategory`::: +(Required, string) The subcategory of the incident. + +For {swimlane} connectors, specify: + +`caseId`::: +(Required, string) The case ID. +-- + +`id`:: +(Required, string) The identifier for the connector. To create a case without a +connector, use `none`. +//To retrieve connector IDs, use <>). + +`name`:: +(Required, string) The name of the connector. To create a case without a +connector, use `none`. + +`type`:: +(Required, string) The type of the connector. Valid values are: `.jira`, `.none`, +`.resilient`,`.servicenow`, `.servicenow-sir`, and `.swimlane`. To create a case +without a connector, use `.none`. +==== + +`description`:: +(Required, string) The description for the case. + +`owner`:: +(Required, string) The application that owns the case. Valid values are: +`cases`, `observability`, or `securitySolution`. This value affects +whether the case is visible in the {stack-manage-app}, {observability}, or +{security-app}. + +`settings`:: +(Required, object) +An object that contains the case settings. ++ +.Properties of `settings` +[%collapsible%open] +==== +`syncAlerts`:: +(Required, boolean) Turns alert syncing on or off. +==== + +`tags`:: +(Required, string array) The words and phrases that help +categorize cases. It can be an empty array. + +`title`:: +(Required, string) A title for the case. + +=== Response code + +`200`:: + Indicates a successful call. + +=== Example + +[source,sh] +-------------------------------------------------- +POST api/cases +{ + "description": "James Bond clicked on a highly suspicious email + banner advertising cheap holidays for underpaid civil servants.", + "title": "This case will self-destruct in 5 seconds", + "tags": [ + "phishing", + "social engineering" + ], + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "My connector", + "type": ".jira", + "fields": { + "issueType": "10006", + "priority": "High", + "parent": null + } + }, + "settings": { + "syncAlerts": true + }, + "owner": "securitySolution" +} +-------------------------------------------------- +// KIBANA + +The API returns a JSON object that includes the user who created the case and +the case identifier, version, and creation time. For example: + +[source,json] +-------------------------------------------------- +{ + "id": "66b9aa00-94fa-11ea-9f74-e7e108796192", <1> + "version": "WzUzMiwxXQ==", + "comments": [], + "totalComment": 0, + "totalAlerts": 0, + "title": "This case will self-destruct in 5 seconds", + "tags": [ + "phishing", + "social engineering", + "bubblegum" + ], + "settings": { + "syncAlerts": true + }, + "owner": "securitySolution", + "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active", + "closed_at": null, + "closed_by": null, + "created_at": "2022-05-13T09:16:17.416Z", + "created_by": { + "email": "ahunley@imf.usa.gov", + "full_name": "Alan Hunley", + "username": "ahunley" + }, + "status": "open", + "updated_at": null, + "updated_by": null, + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", <2> + "name": "My connector", + "type": ".jira", + "fields": { + "issueType": "10006", + "parent": null, + "priority": "High" + } + }, + "external_service": null <3> +} +-------------------------------------------------- + +<1> The case identifier is also its saved object ID (`savedObjectId`), which is +used when pushing cases to external systems. +<2> The default connector used to push cases to external services. +<3> The `external_service` object stores information about the incident after it +is pushed to an external incident management system. \ No newline at end of file diff --git a/docs/api/cases/cases-api-update.asciidoc b/docs/api/cases/cases-api-update.asciidoc new file mode 100644 index 00000000000000..ed0ef069e15f48 --- /dev/null +++ b/docs/api/cases/cases-api-update.asciidoc @@ -0,0 +1,271 @@ +[[cases-api-update]] +== Update cases API +++++ +Update cases +++++ + +Updates one or more cases. + +=== Request + +`PATCH :/api/cases` + +`PATCH :/s//api/cases` + +=== Prerequisite + +You must have `all` privileges for the *Cases* feature in the *Management*, +*{observability}*, or *Security* section of the +<>, depending on the +`owner` of the cases you're updating. + +=== Path parameters + +``:: +(Optional, string) An identifier for the space. If it is not specified, the +default space is used. + +=== Request body + +`cases`:: +(Required, array of objects) Array containing one or more case objects. ++ +.Properties of `cases` objects +[%collapsible%open] +==== +`connector`:: +(Optional, object) An object that contains the connector configuration. ++ +.Properties of `connector` +[%collapsible%open] +===== +`fields`:: +(Required, object) An object containing the connector fields. ++ +-- +To remove the connector, specify `null`. If you want to omit any individual +field, specify `null` as its value. + +For {ibm-r} connectors, specify: + +`issueTypes`::: +(Required, array of numbers) The issue types of the issue. + +`severityCode`::: +(Required, number) The severity code of the issue. + +For {jira} connectors, specify: + +`issueType`::: +(Required, string) The issue type of the issue. + +`parent`::: +(Required, string) The key of the parent issue, when the issue type is +`Sub-task`. + +`priority`::: +(Required, string) The priority of the issue. + +For {sn-itsm} connectors, specify: + +`category`::: +(Required, string) The category of the incident. + +`impact`::: +(Required, string) The effect an incident had on business. + +`severity`::: +(Required, string) The severity of the incident. + +`subcategory`::: +(Required, string) The subcategory of the incident. + +`urgency`::: +(Required, string) The extent to which the incident resolution can be delayed. + +For {sn-sir} connectors, specify: + +`category`::: +(Required, string) The category of the incident. + +`destIp`::: +(Required, string) A comma separated list of destination IPs. + +`malwareHash`::: +(Required, string) A comma separated list of malware hashes. + +`malwareUrl`::: +(Required, string) A comma separated list of malware URLs. + +`priority`::: +(Required, string) The priority of the incident. + +`sourceIp`::: +(Required, string) A comma separated list of source IPs. + +`subcategory`::: +(Required, string) The subcategory of the incident. + +For {swimlane} connectors, specify: + +`caseId`::: +(Required, string) The identifier for the case. +-- + +`id`:: +(Required, string) The identifier for the connector. To remove the connector, +use `none`. +//To retrieve connector IDs, use <>). + +`name`:: +(Required, string) The name of the connector. To remove the connector, use +`none`. + +`type`:: +(Required, string) The type of the connector. Valid values are: `.jira`, `.none`, +`.resilient`,`.servicenow`, `.servicenow-sir`, and `.swimlane`. To remove the +connector, use `.none`. + +===== + +`description`:: +(Optional, string) The updated case description. + +`id`:: +(Required, string) The identifier for the case. + +`settings`:: +(Optional, object) +An object that contains the case settings. ++ +.Properties of `settings` +[%collapsible%open] +===== +`syncAlerts`:: +(Required, boolean) Turn on or off synching with alerts. +===== + +`status`:: +(Optional, string) The case status. Valid values are: `closed`, `in-progress`, +and `open`. + +`tags`:: +(Optional, string array) The words and phrases that help categorize cases. + +`title`:: +(Optional, string) A title for the case. + +`version`:: +(Required, string) The current version of the case. +//To determine this value, use <> or <> +==== + +=== Response code + +`200`:: + Indicates a successful call. + +=== Example + +Update the description, tags, and connector of case ID +`a18b38a0-71b0-11ea-a0b2-c51ea50a58e2`: + +[source,sh] +-------------------------------------------------- +PATCH api/cases +{ + "cases": [ + { + "id": "a18b38a0-71b0-11ea-a0b2-c51ea50a58e2", + "version": "WzIzLDFd", + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "My connector", + "type": ".jira", + "fields": { + "issueType": "10006", + "priority": null, + "parent": null + } + }, + "description": "James Bond clicked on a highly suspicious email + banner advertising cheap holidays for underpaid civil servants. + Operation bubblegum is active. Repeat - operation bubblegum is + now active!", + "tags": [ + "phishing", + "social engineering", + "bubblegum" + ], + "settings": { + "syncAlerts": true + } + } + ] +} +-------------------------------------------------- +// KIBANA + +The API returns the updated case with a new `version` value. For example: + +[source,json] +-------------------------------------------------- +[ + { + "id": "66b9aa00-94fa-11ea-9f74-e7e108796192", + "version": "WzU0OCwxXQ==", + "comments": [], + "totalComment": 0, + "totalAlerts": 0, + "title": "This case will self-destruct in 5 seconds", + "tags": [ + "phishing", + "social engineering", + "bubblegum" + ], + "settings": { + "syncAlerts": true + }, + "owner": "securitySolution", + "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active!", + "closed_at": null, + "closed_by": null, + "created_at": "2022-05-13T09:16:17.416Z", + "created_by": { + "email": "ahunley@imf.usa.gov", + "full_name": "Alan Hunley", + "username": "ahunley" + }, + "status": "open", + "updated_at": "2022-05-13T09:48:33.043Z", + "updated_by": { + "email": "classified@hms.oo.gov.uk", + "full_name": "Classified", + "username": "M" + }, + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "My connector", + "type": ".jira", + "fields": { + "issueType": "10006", + "parent": null, + "priority": null, + } + }, + "external_service": { + "external_title": "IS-4", + "pushed_by": { + "full_name": "Classified", + "email": "classified@hms.oo.gov.uk", + "username": "M" + }, + "external_url": "https://hms.atlassian.net/browse/IS-4", + "pushed_at": "2022-05-13T09:20:40.672Z", + "connector_id": "05da469f-1fde-4058-99a3-91e4807e2de8", + "external_id": "10003", + "connector_name": "Jira" + } + } +] +-------------------------------------------------- From 7640031a5053369140693dfd8601fc47a1cbce07 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 22 Mar 2022 15:34:48 +0100 Subject: [PATCH 02/64] [Uptime] Fix pings over time histogram when filters are defined (#127757) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../ping_histogram/ping_histogram_container.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_histogram/ping_histogram_container.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_histogram/ping_histogram_container.tsx index d8060e27f1aa20..cd60dcf725074d 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_histogram/ping_histogram_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_histogram/ping_histogram_container.tsx @@ -40,9 +40,14 @@ const Container: React.FC = ({ height }) => { const { loading, pingHistogram: data } = useSelector(selectPingHistogram); useEffect(() => { - filterCheck(() => - dispatch(getPingHistogram.get({ monitorId, dateStart, dateEnd, query, filters: esKuery })) - ); + if (monitorId) { + // we don't need filter check on monitor details page, where we have monitorId defined + dispatch(getPingHistogram.get({ monitorId, dateStart, dateEnd, query, filters: esKuery })); + } else { + filterCheck(() => + dispatch(getPingHistogram.get({ monitorId, dateStart, dateEnd, query, filters: esKuery })) + ); + } }, [filterCheck, dateStart, dateEnd, monitorId, lastRefresh, esKuery, dispatch, query]); return ( Date: Tue, 22 Mar 2022 10:35:52 -0400 Subject: [PATCH 03/64] [Fleet] Add install all packages script (#128208) --- .../scripts/install_all_packages/index.js | 9 ++ .../install_all_packages.ts | 118 ++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 x-pack/plugins/fleet/scripts/install_all_packages/index.js create mode 100644 x-pack/plugins/fleet/scripts/install_all_packages/install_all_packages.ts diff --git a/x-pack/plugins/fleet/scripts/install_all_packages/index.js b/x-pack/plugins/fleet/scripts/install_all_packages/index.js new file mode 100644 index 00000000000000..aa620c4ea6a04e --- /dev/null +++ b/x-pack/plugins/fleet/scripts/install_all_packages/index.js @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +require('../../../../../src/setup_node_env'); +require('./install_all_packages').run(); diff --git a/x-pack/plugins/fleet/scripts/install_all_packages/install_all_packages.ts b/x-pack/plugins/fleet/scripts/install_all_packages/install_all_packages.ts new file mode 100644 index 00000000000000..7ff848f79185df --- /dev/null +++ b/x-pack/plugins/fleet/scripts/install_all_packages/install_all_packages.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 fetch from 'node-fetch'; +import { ToolingLog } from '@kbn/dev-utils'; +import ReadPackage from 'read-pkg'; + +const REGISTRY_URL = 'https://epr-snapshot.elastic.co'; +const KIBANA_URL = 'http://localhost:5601'; +const KIBANA_USERNAME = 'elastic'; +const KIBANA_PASSWORD = 'changeme'; + +const KIBANA_VERSION = ReadPackage.sync().version; + +const SKIP_PACKAGES: string[] = []; + +async function installPackage(name: string, version: string) { + const start = Date.now(); + const res = await fetch(`${KIBANA_URL}/api/fleet/epm/packages/${name}/${version}`, { + headers: { + accept: '*/*', + 'content-type': 'application/json', + 'kbn-xsrf': 'xyz', + Authorization: + 'Basic ' + Buffer.from(`${KIBANA_USERNAME}:${KIBANA_PASSWORD}`).toString('base64'), + }, + body: JSON.stringify({ force: true }), + method: 'POST', + }); + const end = Date.now(); + + const body = await res.json(); + + return { body, status: res.status, took: (end - start) / 1000 }; +} + +async function deletePackage(name: string, version: string) { + const res = await fetch(`${KIBANA_URL}/api/fleet/epm/packages/${name}-${version}`, { + headers: { + accept: '*/*', + 'content-type': 'application/json', + 'kbn-xsrf': 'xyz', + Authorization: + 'Basic ' + Buffer.from(`${KIBANA_USERNAME}:${KIBANA_PASSWORD}`).toString('base64'), + }, + method: 'DELETE', + }); + + const body = await res.json(); + + return { body, status: res.status }; +} + +async function getAllPackages() { + const res = await fetch( + `${REGISTRY_URL}/search?experimental=true&kibana.version=${KIBANA_VERSION}`, + { + headers: { + accept: '*/*', + }, + method: 'GET', + } + ); + const body = await res.json(); + return body; +} + +function logResult( + logger: ToolingLog, + pkg: { name: string; version: string }, + result: { took?: number; status?: number } +) { + const pre = `${pkg.name}-${pkg.version} ${result.took ? ` took ${result.took}s` : ''} : `; + if (result.status !== 200) { + logger.info('❌ ' + pre + JSON.stringify(result)); + } else { + logger.info('✅ ' + pre + 200); + } +} + +export async function run() { + const logger = new ToolingLog({ + level: 'info', + writeTo: process.stdout, + }); + const allPackages = await getAllPackages(); + + logger.info('INSTALLING packages'); + + for (const pkg of allPackages) { + if (SKIP_PACKAGES.includes(pkg.name)) { + logger.info(`Skipping ${pkg.name}`); + continue; + } + const result = await installPackage(pkg.name, pkg.version); + + logResult(logger, pkg, result); + } + + const deletePackages = process.argv.includes('--delete'); + + if (!deletePackages) return; + + logger.info('DELETING packages'); + for (const pkg of allPackages) { + if (SKIP_PACKAGES.includes(pkg.name)) { + logger.info(`Skipping ${pkg.name}`); + continue; + } + const result = await deletePackage(pkg.name, pkg.version); + + logResult(logger, pkg, result); + } +} From 4a0b376ad4ba237a1347f1d345ae2153c1e221bd Mon Sep 17 00:00:00 2001 From: srinjon <99788429+srinjon@users.noreply.github.com> Date: Tue, 22 Mar 2022 20:40:39 +0530 Subject: [PATCH 04/64] Update endpoints.mdx (#128114) Fixed a sentence formation Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- dev_docs/tutorials/endpoints.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev_docs/tutorials/endpoints.mdx b/dev_docs/tutorials/endpoints.mdx index 5f2fc7da010c7b..f6367580420db4 100644 --- a/dev_docs/tutorials/endpoints.mdx +++ b/dev_docs/tutorials/endpoints.mdx @@ -46,7 +46,7 @@ HTTP method. All these APIs share the same signature, and receive two parameters When invoked, the `handler` receive three parameters: `context`, `request`, and `response`, and must return a response that will be sent to serve the request. -- `context` is a request-bound context exposed for the request. It allows for example to use an elasticsearch client bound to the request's credentials. +- `context` is a request-bound context exposed for the request. For example, it allows to use an elasticsearch client bound to the request's credentials. - `request` contains information related to the request, such as the path and query parameter - `response` contains factory helpers to create the response to return from the endpoint From e02b367063c218bf6254e2093fc25d6c772ddaef Mon Sep 17 00:00:00 2001 From: Byron Hulcher Date: Tue, 22 Mar 2022 11:18:26 -0400 Subject: [PATCH 05/64] [Workplace Search] Submit `base_service_type` field when creating a pre-configured custom source (#128221) --- .../add_custom_source_logic.test.ts | 53 +++++++++++++++++++ .../add_source/add_custom_source_logic.ts | 16 ++++-- .../server/routes/workplace_search/sources.ts | 2 + 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source_logic.test.ts index 93609679858765..d019c66526e6cb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source_logic.test.ts @@ -150,6 +150,31 @@ describe('AddCustomSourceLogic', () => { expect(setButtonNotLoadingSpy).toHaveBeenCalled(); }); + it('submits a base service type for pre-configured sources', () => { + mount( + { + customSourceNameValue: MOCK_NAME, + }, + { + ...MOCK_PROPS, + sourceData: { + ...CUSTOM_SOURCE_DATA_ITEM, + serviceType: 'sharepoint-server', + }, + } + ); + + AddCustomSourceLogic.actions.createContentSource(); + + expect(http.post).toHaveBeenCalledWith('/internal/workplace_search/org/create_source', { + body: JSON.stringify({ + service_type: 'custom', + name: MOCK_NAME, + base_service_type: 'sharepoint-server', + }), + }); + }); + itShowsServerErrorAsFlashMessage(http.post, () => { AddCustomSourceLogic.actions.createContentSource(); }); @@ -173,6 +198,34 @@ describe('AddCustomSourceLogic', () => { ); }); + it('submits a base service type for pre-configured sources', () => { + mount( + { + customSourceNameValue: MOCK_NAME, + }, + { + ...MOCK_PROPS, + sourceData: { + ...CUSTOM_SOURCE_DATA_ITEM, + serviceType: 'sharepoint-server', + }, + } + ); + + AddCustomSourceLogic.actions.createContentSource(); + + expect(http.post).toHaveBeenCalledWith( + '/internal/workplace_search/account/create_source', + { + body: JSON.stringify({ + service_type: 'custom', + name: MOCK_NAME, + base_service_type: 'sharepoint-server', + }), + } + ); + }); + itShowsServerErrorAsFlashMessage(http.post, () => { AddCustomSourceLogic.actions.createContentSource(); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source_logic.ts index 5bf86f6df41c7a..c35436ccbf99ae 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source_logic.ts @@ -80,7 +80,7 @@ export const AddCustomSourceLogic = kea< ], sourceData: [props.sourceData], }), - listeners: ({ actions, values }) => ({ + listeners: ({ actions, values, props }) => ({ createContentSource: async () => { clearFlashMessages(); const { isOrganization } = AppLogic.values; @@ -90,14 +90,24 @@ export const AddCustomSourceLogic = kea< const { customSourceNameValue } = values; - const params = { + const baseParams = { service_type: 'custom', name: customSourceNameValue, }; + // pre-configured custom sources have a serviceType reflecting their target service + // we submit this as `base_service_type` to keep track of + const params = + props.sourceData.serviceType === 'custom' + ? baseParams + : { + ...baseParams, + base_service_type: props.sourceData.serviceType, + }; + try { const response = await HttpLogic.values.http.post(route, { - body: JSON.stringify({ ...params }), + body: JSON.stringify(params), }); actions.setNewCustomSource(response); } catch (e) { diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index ba1bd8119a3e52..10fad8b39ddae7 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -206,6 +206,7 @@ export function registerAccountCreateSourceRoute({ validate: { body: schema.object({ service_type: schema.string(), + base_service_type: schema.maybe(schema.string()), name: schema.maybe(schema.string()), login: schema.maybe(schema.string()), password: schema.maybe(schema.string()), @@ -566,6 +567,7 @@ export function registerOrgCreateSourceRoute({ validate: { body: schema.object({ service_type: schema.string(), + base_service_type: schema.maybe(schema.string()), name: schema.maybe(schema.string()), login: schema.maybe(schema.string()), password: schema.maybe(schema.string()), From d8a1827b44ec8a41a4297f2f081454f8d1206ef9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Mar 2022 11:20:04 -0400 Subject: [PATCH 06/64] Update dependency node-forge to ^1.3.0 (#128112) --- package.json | 4 ++-- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 9f1cdfab2e3050..a847db572fa4c6 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "**/json-schema": "^0.4.0", "**/minimatch": "^3.1.2", "**/minimist": "^1.2.5", - "**/node-forge": "^1.2.1", + "**/node-forge": "^1.3.0", "**/pdfkit/crypto-js": "4.0.0", "**/react-syntax-highlighter": "^15.3.1", "**/react-syntax-highlighter/**/highlight.js": "^10.4.1", @@ -315,7 +315,7 @@ "mustache": "^2.3.2", "nock": "12.0.3", "node-fetch": "^2.6.7", - "node-forge": "^1.2.1", + "node-forge": "^1.3.0", "nodemailer": "^6.6.2", "normalize-path": "^3.0.0", "object-hash": "^1.3.1", diff --git a/yarn.lock b/yarn.lock index 6df682be18360b..396cda03b1235c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20710,10 +20710,10 @@ node-fetch@2.6.1, node-fetch@^1.0.1, node-fetch@^2.3.0, node-fetch@^2.6.1, node- dependencies: whatwg-url "^5.0.0" -node-forge@^0.10.0, node-forge@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.2.1.tgz#82794919071ef2eb5c509293325cec8afd0fd53c" - integrity sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w== +node-forge@^0.10.0, node-forge@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.0.tgz#37a874ea723855f37db091e6c186e5b67a01d4b2" + integrity sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA== node-gyp-build@^4.2.3: version "4.2.3" From e9d0769a3d6c1dd586aeaba591a17c02489c2749 Mon Sep 17 00:00:00 2001 From: Ignacio Rivas Date: Tue, 22 Mar 2022 16:36:45 +0100 Subject: [PATCH 07/64] View request flyout (#127156) * wip: create code scaffold for component * Implement new flyout in ingest pipelines * Fix linter and i18n issues * Add base tests for new component * Wire everything together * Fix linter issues * Finish writing tests * fix linting issues * Refactor hook and fix small dependencies bug * commit using @elastic.co * Refactor out hook and fix linter issues * Enhance tests and fix typo * Refactor component name and fix tests * update snapshot * Address first round of CR * Update snapshot * Refactor apirequestflyout to consume applicationStart only * Fix import order --- src/plugins/es_ui_shared/kibana.json | 2 +- .../view_api_request_flyout.test.tsx.snap | 118 ++++++++++++++ .../view_api_request_flyout/index.ts | 9 ++ .../view_api_request_flyout.test.tsx | 93 +++++++++++ .../view_api_request_flyout.tsx | 147 ++++++++++++++++++ src/plugins/es_ui_shared/public/index.ts | 1 + .../helpers/pipeline_form.helpers.ts | 4 +- .../helpers/setup_environment.tsx | 16 ++ .../ingest_pipelines_create.test.tsx | 4 +- .../pipeline_request_flyout/index.ts | 2 +- .../pipeline_request_flyout.tsx | 113 ++++++-------- .../pipeline_request_flyout_provider.tsx | 46 ------ .../public/application/index.tsx | 2 + .../application/mount_management_section.ts | 2 + .../ingest_pipelines/public/shared_imports.ts | 1 + .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 18 files changed, 440 insertions(+), 123 deletions(-) create mode 100644 src/plugins/es_ui_shared/public/components/view_api_request_flyout/__snapshots__/view_api_request_flyout.test.tsx.snap create mode 100644 src/plugins/es_ui_shared/public/components/view_api_request_flyout/index.ts create mode 100644 src/plugins/es_ui_shared/public/components/view_api_request_flyout/view_api_request_flyout.test.tsx create mode 100644 src/plugins/es_ui_shared/public/components/view_api_request_flyout/view_api_request_flyout.tsx delete mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx diff --git a/src/plugins/es_ui_shared/kibana.json b/src/plugins/es_ui_shared/kibana.json index 2735b153f738c7..1a4ff33674f953 100644 --- a/src/plugins/es_ui_shared/kibana.json +++ b/src/plugins/es_ui_shared/kibana.json @@ -14,5 +14,5 @@ "static/forms/components", "static/forms/helpers/field_validators/types" ], - "requiredBundles": ["data"] + "requiredBundles": ["data", "kibanaReact"] } diff --git a/src/plugins/es_ui_shared/public/components/view_api_request_flyout/__snapshots__/view_api_request_flyout.test.tsx.snap b/src/plugins/es_ui_shared/public/components/view_api_request_flyout/__snapshots__/view_api_request_flyout.test.tsx.snap new file mode 100644 index 00000000000000..2d850ee8082f95 --- /dev/null +++ b/src/plugins/es_ui_shared/public/components/view_api_request_flyout/__snapshots__/view_api_request_flyout.test.tsx.snap @@ -0,0 +1,118 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ViewApiRequestFlyout is rendered 1`] = ` +
+ + +
+
+
+
+            
+              Hello world
+            
+          
+
+
+ + +
+ +
+ +`; diff --git a/src/plugins/es_ui_shared/public/components/view_api_request_flyout/index.ts b/src/plugins/es_ui_shared/public/components/view_api_request_flyout/index.ts new file mode 100644 index 00000000000000..deed3c5db27d6f --- /dev/null +++ b/src/plugins/es_ui_shared/public/components/view_api_request_flyout/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { ViewApiRequestFlyout } from './view_api_request_flyout'; diff --git a/src/plugins/es_ui_shared/public/components/view_api_request_flyout/view_api_request_flyout.test.tsx b/src/plugins/es_ui_shared/public/components/view_api_request_flyout/view_api_request_flyout.test.tsx new file mode 100644 index 00000000000000..4f6c954d4c37d9 --- /dev/null +++ b/src/plugins/es_ui_shared/public/components/view_api_request_flyout/view_api_request_flyout.test.tsx @@ -0,0 +1,93 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { mountWithI18nProvider } from '@kbn/test-jest-helpers'; +import { findTestSubject, takeMountedSnapshot } from '@elastic/eui/lib/test'; +import { compressToEncodedURIComponent } from 'lz-string'; + +import { ViewApiRequestFlyout } from './view_api_request_flyout'; +import type { UrlService } from 'src/plugins/share/common/url_service'; +import { ApplicationStart } from 'src/core/public'; +import { applicationServiceMock } from 'src/core/public/mocks'; + +const payload = { + title: 'Test title', + description: 'Test description', + request: 'Hello world', + closeFlyout: jest.fn(), +}; + +const urlServiceMock = { + locators: { + get: jest.fn().mockReturnValue({ + useUrl: jest.fn().mockImplementation((value) => { + return `devToolsUrl_${value?.loadFrom}`; + }), + }), + }, +} as any as UrlService; + +const applicationMock = { + ...applicationServiceMock.createStartContract(), + capabilities: { + dev_tools: { + show: true, + }, + }, +} as any as ApplicationStart; + +describe('ViewApiRequestFlyout', () => { + test('is rendered', () => { + const component = mountWithI18nProvider(); + expect(takeMountedSnapshot(component)).toMatchSnapshot(); + }); + + describe('props', () => { + test('on closeFlyout', async () => { + const component = mountWithI18nProvider(); + + await act(async () => { + findTestSubject(component, 'apiRequestFlyoutClose').simulate('click'); + }); + + expect(payload.closeFlyout).toBeCalled(); + }); + + test('doesnt have openInConsole when some optional props are not supplied', async () => { + const component = mountWithI18nProvider(); + + const openInConsole = findTestSubject(component, 'apiRequestFlyoutOpenInConsoleButton'); + expect(openInConsole.length).toEqual(0); + + // Flyout should *not* be wrapped with RedirectAppLinks + const redirectWrapper = findTestSubject(component, 'apiRequestFlyoutRedirectWrapper'); + expect(redirectWrapper.length).toEqual(0); + }); + + test('has openInConsole when all optional props are supplied', async () => { + const encodedRequest = compressToEncodedURIComponent(payload.request); + const component = mountWithI18nProvider( + + ); + + const openInConsole = findTestSubject(component, 'apiRequestFlyoutOpenInConsoleButton'); + expect(openInConsole.length).toEqual(1); + expect(openInConsole.props().href).toEqual(`devToolsUrl_data:text/plain,${encodedRequest}`); + + // Flyout should be wrapped with RedirectAppLinks + const redirectWrapper = findTestSubject(component, 'apiRequestFlyoutRedirectWrapper'); + expect(redirectWrapper.length).toEqual(1); + }); + }); +}); diff --git a/src/plugins/es_ui_shared/public/components/view_api_request_flyout/view_api_request_flyout.tsx b/src/plugins/es_ui_shared/public/components/view_api_request_flyout/view_api_request_flyout.tsx new file mode 100644 index 00000000000000..fa7bc6088f5c08 --- /dev/null +++ b/src/plugins/es_ui_shared/public/components/view_api_request_flyout/view_api_request_flyout.tsx @@ -0,0 +1,147 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n-react'; +import { compressToEncodedURIComponent } from 'lz-string'; + +import { + EuiFlyout, + EuiFlyoutProps, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiButtonEmpty, + EuiText, + EuiSpacer, + EuiCodeBlock, + EuiCopy, +} from '@elastic/eui'; +import type { UrlService } from 'src/plugins/share/common/url_service'; +import { ApplicationStart, APP_WRAPPER_CLASS } from '../../../../../core/public'; +import { RedirectAppLinks } from '../../../../kibana_react/public'; + +type FlyoutProps = Omit; +interface ViewApiRequestFlyoutProps { + title: string; + description: string; + request: string; + closeFlyout: () => void; + flyoutProps?: FlyoutProps; + application?: ApplicationStart; + urlService?: UrlService; +} + +export const ApiRequestFlyout: React.FunctionComponent = ({ + title, + description, + request, + closeFlyout, + flyoutProps, + urlService, + application, +}) => { + const getUrlParams = undefined; + const canShowDevtools = !!application?.capabilities?.dev_tools?.show; + const devToolsDataUri = compressToEncodedURIComponent(request); + + // Generate a console preview link if we have a valid locator + const consolePreviewLink = urlService?.locators.get('CONSOLE_APP_LOCATOR')?.useUrl( + { + loadFrom: `data:text/plain,${devToolsDataUri}`, + }, + getUrlParams, + [request] + ); + + // Check if both the Dev Tools UI and the Console UI are enabled. + const shouldShowDevToolsLink = canShowDevtools && consolePreviewLink !== undefined; + + return ( + + + +

{title}

+
+
+ + + +

{description}

+
+ + + +
+ + {(copy) => ( + + + + )} + + {shouldShowDevToolsLink && ( + + + + )} +
+ + + {request} + +
+ + + + + + +
+ ); +}; + +export const ViewApiRequestFlyout = (props: ViewApiRequestFlyoutProps) => { + if (props.application) { + return ( + + + + ); + } + + return ; +}; diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index c21587c9a60408..8a861ac9931700 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -25,6 +25,7 @@ export type { EuiCodeEditorProps } from './components/code_editor'; export { EuiCodeEditor } from './components/code_editor'; export type { Frequency } from './components/cron_editor'; export { CronEditor } from './components/cron_editor'; +export { ViewApiRequestFlyout } from './components/view_api_request_flyout'; export type { SendRequestConfig, diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts index 432b9046f10714..775d05a8651895 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts @@ -61,8 +61,8 @@ export type PipelineFormTestSubjects = | 'onFailureEditor' | 'testPipelineButton' | 'showRequestLink' - | 'requestFlyout' - | 'requestFlyout.title' + | 'apiRequestFlyout' + | 'apiRequestFlyout.apiRequestFlyoutTitle' | 'testPipelineFlyout' | 'testPipelineFlyout.title' | 'documentationLink'; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/setup_environment.tsx index 8e128692c41c51..96a0f9e23348a0 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/setup_environment.tsx @@ -11,6 +11,8 @@ import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import { LocationDescriptorObject } from 'history'; import { HttpSetup } from 'kibana/public'; +import { ApplicationStart } from 'src/core/public'; +import { MockUrlService } from 'src/plugins/share/common/mocks'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { sharePluginMock } from '../../../../../../src/plugins/share/public/mocks'; import { @@ -18,6 +20,7 @@ import { docLinksServiceMock, scopedHistoryMock, uiSettingsServiceMock, + applicationServiceMock, } from '../../../../../../src/core/public/mocks'; import { usageCollectionPluginMock } from '../../../../../../src/plugins/usage_collection/public/mocks'; @@ -38,6 +41,15 @@ history.createHref.mockImplementation((location: LocationDescriptorObject) => { return `${location.pathname}?${location.search}`; }); +const applicationMock = { + ...applicationServiceMock.createStartContract(), + capabilities: { + dev_tools: { + show: true, + }, + }, +} as any as ApplicationStart; + const appServices = { breadcrumbs: breadcrumbService, metric: uiMetricService, @@ -54,6 +66,10 @@ const appServices = { getMaxBytes: jest.fn().mockReturnValue(100), getMaxBytesFormatted: jest.fn().mockReturnValue('100'), }, + application: applicationMock, + share: { + url: new MockUrlService(), + }, }; export const setupEnvironment = () => { diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx index bb1d3f2503f9b8..5be5cecd750f62 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx @@ -79,8 +79,8 @@ describe('', () => { await actions.clickShowRequestLink(); // Verify request flyout opens - expect(exists('requestFlyout')).toBe(true); - expect(find('requestFlyout.title').text()).toBe('Request'); + expect(exists('apiRequestFlyout')).toBe(true); + expect(find('apiRequestFlyout.apiRequestFlyoutTitle').text()).toBe('Request'); }); describe('form validation', () => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/index.ts index 5905d10faad858..8368dbf93b96cd 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { PipelineRequestFlyoutProvider as PipelineRequestFlyout } from './pipeline_request_flyout_provider'; +export { PipelineRequestFlyout } from './pipeline_request_flyout'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx index feb7d55145083d..66d95c18663c03 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx @@ -5,86 +5,63 @@ * 2.0. */ -import React, { useRef } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; - -import { - EuiButtonEmpty, - EuiCodeBlock, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; +import React, { useState, useEffect, FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; import { Pipeline } from '../../../../../common/types'; +import { useFormContext, ViewApiRequestFlyout, useKibana } from '../../../../shared_imports'; + +import { ReadProcessorsFunction } from '../types'; interface Props { - pipeline: Pipeline; closeFlyout: () => void; + readProcessors: ReadProcessorsFunction; } -export const PipelineRequestFlyout: React.FunctionComponent = ({ +export const PipelineRequestFlyout: FunctionComponent = ({ closeFlyout, - pipeline, + readProcessors, }) => { - const { name, ...pipelineBody } = pipeline; - const endpoint = `PUT _ingest/pipeline/${name || ''}`; - const payload = JSON.stringify(pipelineBody, null, 2); - const request = `${endpoint}\n${payload}`; - // Hack so that copied-to-clipboard value updates as content changes - // Related issue: https://github.com/elastic/eui/issues/3321 - const uuid = useRef(0); - uuid.current++; + const { services } = useKibana(); + const form = useFormContext(); + const [formData, setFormData] = useState({} as Pipeline); + const pipeline = { ...formData, ...readProcessors() }; - return ( - - - -

- {name ? ( - - ) : ( - - )} -

-
-
+ useEffect(() => { + const subscription = form.subscribe(async ({ isValid, validate, data }) => { + const isFormValid = isValid ?? (await validate()); + if (isFormValid) { + setFormData(data.format() as Pipeline); + } + }); - - -

- -

-
+ return subscription.unsubscribe; + }, [form]); - - - {request} - -
+ const { name, ...pipelineBody } = pipeline; + const endpoint = `PUT _ingest/pipeline/${name || ''}`; + const request = `${endpoint}\n${JSON.stringify(pipelineBody, null, 2)}`; - - - - - -
+ const title = name + ? i18n.translate('xpack.ingestPipelines.requestFlyout.namedTitle', { + defaultMessage: "Request for '{name}'", + values: { name }, + }) + : i18n.translate('xpack.ingestPipelines.requestFlyout.unnamedTitle', { + defaultMessage: 'Request', + }); + + return ( + ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx deleted file mode 100644 index 0b91b07a5a5266..00000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState, useEffect, FunctionComponent } from 'react'; - -import { Pipeline } from '../../../../../common/types'; -import { useFormContext } from '../../../../shared_imports'; - -import { ReadProcessorsFunction } from '../types'; - -import { PipelineRequestFlyout } from './pipeline_request_flyout'; - -interface Props { - closeFlyout: () => void; - readProcessors: ReadProcessorsFunction; -} - -export const PipelineRequestFlyoutProvider: FunctionComponent = ({ - closeFlyout, - readProcessors, -}) => { - const form = useFormContext(); - const [formData, setFormData] = useState({} as Pipeline); - - useEffect(() => { - const subscription = form.subscribe(async ({ isValid, validate, data }) => { - const isFormValid = isValid ?? (await validate()); - if (isFormValid) { - setFormData(data.format() as Pipeline); - } - }); - - return subscription.unsubscribe; - }, [form]); - - return ( - - ); -}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/index.tsx b/x-pack/plugins/ingest_pipelines/public/application/index.tsx index bab3a1e0a074af..91c7665503a66a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/index.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/index.tsx @@ -10,6 +10,7 @@ import React, { ReactNode } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Observable } from 'rxjs'; +import { ApplicationStart } from 'src/core/public'; import { NotificationsSetup, IUiSettingsClient, CoreTheme } from 'kibana/public'; import { ManagementAppMountParams } from 'src/plugins/management/public'; import type { SharePluginStart } from 'src/plugins/share/public'; @@ -40,6 +41,7 @@ export interface AppServices { uiSettings: IUiSettingsClient; share: SharePluginStart; fileUpload: FileUploadPluginStart; + application: ApplicationStart; } export interface CoreServices { diff --git a/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts b/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts index f90b8077e6281d..81f7be35074d80 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts @@ -26,6 +26,7 @@ export async function mountManagementSection( const [coreStart, depsStart] = await getStartServices(); const { docLinks, + application, i18n: { Context: I18nContext }, } = coreStart; @@ -43,6 +44,7 @@ export async function mountManagementSection( uiSettings: coreStart.uiSettings, share: depsStart.share, fileUpload: depsStart.fileUpload, + application, }; return renderApp(element, I18nContext, services, { http }, { theme$ }); diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts index f4c24f622e7523..90ccf78355f1ac 100644 --- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -30,6 +30,7 @@ export { XJson, JsonEditor, attemptToURIDecode, + ViewApiRequestFlyout, } from '../../../../src/plugins/es_ui_shared/public/'; export type { diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index b4832c4cd24c2f..05ab45fc2756f1 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -13609,7 +13609,6 @@ "xpack.ingestPipelines.processors.label.urldecode": "Décodage d'URL", "xpack.ingestPipelines.processors.label.userAgent": "Agent utilisateur", "xpack.ingestPipelines.processors.uriPartsDescription": "Analyse une chaîne d'URI (Uniform Resource Identifier, identifiant uniforme de ressource) et extrait ses composants sous forme d'objet.", - "xpack.ingestPipelines.requestFlyout.closeButtonLabel": "Fermer", "xpack.ingestPipelines.requestFlyout.descriptionText": "Cette requête Elasticsearch créera ou mettra à jour le pipeline.", "xpack.ingestPipelines.requestFlyout.namedTitle": "Requête pour \"{name}\"", "xpack.ingestPipelines.requestFlyout.unnamedTitle": "Requête", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 846ef5ef2ad286..bcdafe3c8c050d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -15741,7 +15741,6 @@ "xpack.ingestPipelines.processors.label.urldecode": "URLデコード", "xpack.ingestPipelines.processors.label.userAgent": "ユーザーエージェント", "xpack.ingestPipelines.processors.uriPartsDescription": "Uniform Resource Identifier(URI)文字列を解析し、コンポーネントをオブジェクトとして抽出します。", - "xpack.ingestPipelines.requestFlyout.closeButtonLabel": "閉じる", "xpack.ingestPipelines.requestFlyout.descriptionText": "このElasticsearchリクエストは、このパイプラインを作成または更新します。", "xpack.ingestPipelines.requestFlyout.namedTitle": "「{name}」のリクエスト", "xpack.ingestPipelines.requestFlyout.unnamedTitle": "リクエスト", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3a9065a8780853..d2dbc9904b9a15 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -15765,7 +15765,6 @@ "xpack.ingestPipelines.processors.label.urldecode": "URL 解码", "xpack.ingestPipelines.processors.label.userAgent": "用户代理", "xpack.ingestPipelines.processors.uriPartsDescription": "解析统一资源标识符 (URI) 字符串并提取其组件作为对象。", - "xpack.ingestPipelines.requestFlyout.closeButtonLabel": "关闭", "xpack.ingestPipelines.requestFlyout.descriptionText": "此 Elasticsearch 请求将创建或更新管道。", "xpack.ingestPipelines.requestFlyout.namedTitle": "对“{name}”的请求", "xpack.ingestPipelines.requestFlyout.unnamedTitle": "请求", From 4b474815669648ca72cbbc7e366a647558907c97 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Tue, 22 Mar 2022 17:04:51 +0100 Subject: [PATCH 08/64] [Security Solution] [Timeline] Fields browser add a view all / selected option (#128049) * view selected option added * new header component * test fixed * Update x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table_header.test.tsx use not.toBeInTheDocument Co-authored-by: Pablo Machado * pass callback down instead of state setter Co-authored-by: Pablo Machado Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../integration/hosts/events_viewer.spec.ts | 14 +- .../timelines/fields_browser.spec.ts | 15 +- .../cypress/screens/fields_browser.ts | 5 + .../cypress/tasks/fields_browser.ts | 12 ++ .../body/column_headers/default_headers.ts | 3 - .../fields_browser/field_browser.test.tsx | 7 +- .../toolbar/fields_browser/field_browser.tsx | 16 +- .../fields_browser/field_table.test.tsx | 10 +- .../toolbar/fields_browser/field_table.tsx | 22 ++- .../field_table_header.test.tsx | 119 +++++++++++ .../fields_browser/field_table_header.tsx | 112 +++++++++++ .../toolbar/fields_browser/helpers.test.tsx | 186 ++++++------------ .../t_grid/toolbar/fields_browser/helpers.tsx | 80 ++++---- .../t_grid/toolbar/fields_browser/index.tsx | 42 ++-- .../toolbar/fields_browser/translations.ts | 12 ++ 15 files changed, 443 insertions(+), 212 deletions(-) create mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table_header.test.tsx create mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table_header.tsx diff --git a/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts index 47e71345ff0c49..25883b5156407e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts @@ -9,6 +9,7 @@ import { FIELDS_BROWSER_CHECKBOX, FIELDS_BROWSER_CONTAINER, FIELDS_BROWSER_SELECTED_CATEGORIES_BADGES, + FIELDS_BROWSER_VIEW_BUTTON, } from '../../screens/fields_browser'; import { HOST_GEO_CITY_NAME_HEADER, @@ -18,9 +19,10 @@ import { } from '../../screens/hosts/events'; import { + activateViewAll, + activateViewSelected, closeFieldsBrowser, filterFieldsBrowser, - toggleCategory, } from '../../tasks/fields_browser'; import { loginAndWaitForPage } from '../../tasks/login'; import { openEvents } from '../../tasks/hosts/main'; @@ -64,16 +66,20 @@ describe('Events Viewer', () => { cy.get(FIELDS_BROWSER_CONTAINER).should('not.exist'); }); + it('displays "view all" option by default', () => { + cy.get(FIELDS_BROWSER_VIEW_BUTTON).should('contain.text', 'View: all'); + }); + it('displays all categories (by default)', () => { cy.get(FIELDS_BROWSER_SELECTED_CATEGORIES_BADGES).should('be.empty'); }); - it('displays a checked checkbox for all of the default events viewer columns that are also in the default ECS category', () => { - const category = 'default ECS'; - toggleCategory(category); + it('displays only the default selected fields when "view selected" option is enabled', () => { + activateViewSelected(); defaultHeadersInDefaultEcsCategory.forEach((header) => cy.get(FIELDS_BROWSER_CHECKBOX(header.id)).should('be.checked') ); + activateViewAll(); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts index 89a9fc4c0c6ba1..580868fa0452c4 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts @@ -15,6 +15,7 @@ import { FIELDS_BROWSER_CATEGORIES_FILTER_CONTAINER, FIELDS_BROWSER_SELECTED_CATEGORIES_BADGES, FIELDS_BROWSER_CATEGORY_BADGE, + FIELDS_BROWSER_VIEW_BUTTON, } from '../../screens/fields_browser'; import { TIMELINE_FIELDS_BUTTON } from '../../screens/timeline'; import { cleanKibana } from '../../tasks/common'; @@ -29,6 +30,8 @@ import { removesMessageField, resetFields, toggleCategory, + activateViewSelected, + activateViewAll, } from '../../tasks/fields_browser'; import { loginAndWaitForPage } from '../../tasks/login'; import { openTimelineUsingToggle } from '../../tasks/security_main'; @@ -65,6 +68,10 @@ describe('Fields Browser', () => { cy.get(FIELDS_BROWSER_SELECTED_CATEGORIES_BADGES).should('be.empty'); }); + it('displays "view all" option by default', () => { + cy.get(FIELDS_BROWSER_VIEW_BUTTON).should('contain.text', 'View: all'); + }); + it('displays the expected count of categories that match the filter input', () => { const filterInput = 'host.mac'; @@ -80,15 +87,13 @@ describe('Fields Browser', () => { cy.get(FIELDS_BROWSER_FIELDS_COUNT).should('contain.text', '2'); }); - it('the `default ECS` category matches the default timeline header fields', () => { - const category = 'default ECS'; - toggleCategory(category); + it('displays only the selected fields when "view selected" option is enabled', () => { + activateViewSelected(); cy.get(FIELDS_BROWSER_FIELDS_COUNT).should('contain.text', `${defaultHeaders.length}`); - defaultHeaders.forEach((header) => { cy.get(`[data-test-subj="field-${header.id}-checkbox"]`).should('be.checked'); }); - toggleCategory(category); + activateViewAll(); }); it('creates the category badge when it is selected', () => { diff --git a/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts b/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts index 66a7ba50c8070e..a9898f73207d72 100644 --- a/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts +++ b/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts @@ -17,6 +17,11 @@ export const FIELDS_BROWSER_FIELDS_COUNT = `${FIELDS_BROWSER_CONTAINER} [data-te export const FIELDS_BROWSER_FILTER_INPUT = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="field-search"]`; +export const FIELDS_BROWSER_VIEW_BUTTON = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="viewSelectorButton"]`; +export const FIELDS_BROWSER_VIEW_MENU = '[data-test-subj="viewSelectorMenu"]'; +export const FIELDS_BROWSER_VIEW_ALL = `${FIELDS_BROWSER_VIEW_MENU} [data-test-subj="viewSelectorOption-all"]`; +export const FIELDS_BROWSER_VIEW_SELECTED = `${FIELDS_BROWSER_VIEW_MENU} [data-test-subj="viewSelectorOption-selected"]`; + export const FIELDS_BROWSER_HOST_GEO_CITY_NAME_CHECKBOX = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="field-host.geo.city_name-checkbox"]`; export const FIELDS_BROWSER_HOST_GEO_CITY_NAME_HEADER = diff --git a/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts b/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts index 04b59305b591a2..6abc4b11aa59e3 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts @@ -16,6 +16,9 @@ import { FIELDS_BROWSER_CATEGORIES_FILTER_BUTTON, FIELDS_BROWSER_CATEGORY_FILTER_OPTION, FIELDS_BROWSER_CATEGORIES_FILTER_SEARCH, + FIELDS_BROWSER_VIEW_ALL, + FIELDS_BROWSER_VIEW_BUTTON, + FIELDS_BROWSER_VIEW_SELECTED, } from '../screens/fields_browser'; export const addsFields = (fields: string[]) => { @@ -74,3 +77,12 @@ export const removesMessageField = () => { export const resetFields = () => { cy.get(FIELDS_BROWSER_RESET_FIELDS).click({ force: true }); }; + +export const activateViewSelected = () => { + cy.get(FIELDS_BROWSER_VIEW_BUTTON).click({ force: true }); + cy.get(FIELDS_BROWSER_VIEW_SELECTED).click({ force: true }); +}; +export const activateViewAll = () => { + cy.get(FIELDS_BROWSER_VIEW_BUTTON).click({ force: true }); + cy.get(FIELDS_BROWSER_VIEW_ALL).click({ force: true }); +}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/default_headers.ts b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/default_headers.ts index 9a32c514e7064b..a5fb5f4bacd43e 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/default_headers.ts +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/default_headers.ts @@ -53,6 +53,3 @@ export const defaultHeaders: ColumnHeaderOptions[] = [ initialWidth: DEFAULT_COLUMN_MIN_WIDTH, }, ]; - -/** The default category of fields shown in the Timeline */ -export const DEFAULT_CATEGORY_NAME = 'default ECS'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx index ed665155ddcf52..662608155d290c 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx @@ -12,7 +12,7 @@ import { TestProviders, mockBrowserFields, defaultHeaders } from '../../../../mo import { mockGlobalState } from '../../../../mock/global_state'; import { tGridActions } from '../../../../store/t_grid'; -import { FieldsBrowser } from './field_browser'; +import { FieldsBrowser, FieldsBrowserComponentProps } from './field_browser'; import { createStore, State } from '../../../../types'; import { createSecuritySolutionStorageMock } from '../../../../mock/mock_local_storage'; @@ -27,9 +27,8 @@ jest.mock('react-redux', () => { }); const timelineId = 'test'; const onHide = jest.fn(); -const testProps = { +const testProps: FieldsBrowserComponentProps = { columnHeaders: [], - browserFields: mockBrowserFields, filteredBrowserFields: mockBrowserFields, searchInput: '', appliedFilterInput: '', @@ -40,6 +39,8 @@ const testProps = { restoreFocusTo: React.createRef(), selectedCategoryIds: [], timelineId, + filterSelectedEnabled: false, + onFilterSelectedChange: jest.fn(), }; const { storage } = createSecuritySolutionStorageMock(); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx index c67d7cbe633b76..091f4cd79c52fc 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx @@ -33,7 +33,10 @@ import { CategoriesSelector } from './categories_selector'; import { FieldTable } from './field_table'; import { CategoriesBadges } from './categories_badges'; -type Props = Pick & { +export type FieldsBrowserComponentProps = Pick< + FieldBrowserProps, + 'timelineId' | 'width' | 'options' +> & { /** * The current timeline column headers */ @@ -44,6 +47,9 @@ type Props = Pick void; /** * When true, a busy spinner will be shown to indicate the field browser * is searching for fields that match the specified `searchInput` @@ -83,17 +89,19 @@ type Props = Pick = ({ +const FieldsBrowserComponent: React.FC = ({ + appliedFilterInput, columnHeaders, filteredBrowserFields, + filterSelectedEnabled, isSearching, + onFilterSelectedChange, setSelectedCategoryIds, onSearchInputChange, onHide, options, restoreFocusTo, searchInput, - appliedFilterInput, selectedCategoryIds, timelineId, width = FIELD_BROWSER_WIDTH, @@ -182,8 +190,10 @@ const FieldsBrowserComponent: React.FC = ({ timelineId={timelineId} columnHeaders={columnHeaders} filteredBrowserFields={filteredBrowserFields} + filterSelectedEnabled={filterSelectedEnabled} searchInput={appliedFilterInput} selectedCategoryIds={selectedCategoryIds} + onFilterSelectedChange={onFilterSelectedChange} getFieldTableColumns={getFieldTableColumns} onHide={onHide} /> diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.test.tsx index e6721c50f6e1c9..151ed99c3621c6 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.test.tsx @@ -46,16 +46,14 @@ const defaultProps: FieldTableProps = { filteredBrowserFields: {}, searchInput: '', timelineId, + filterSelectedEnabled: false, + onFilterSelectedChange: jest.fn(), onHide: jest.fn(), }; describe('FieldTable', () => { const timestampField = mockBrowserFields.base.fields![timestampFieldId]; const defaultPageSize = 10; - const totalFields = Object.values(mockBrowserFields).reduce( - (total, { fields }) => total + Object.keys(fields ?? {}).length, - 0 - ); beforeEach(() => { mockDispatch.mockClear(); @@ -69,7 +67,6 @@ describe('FieldTable', () => { ); expect(result.getByText('No items found')).toBeInTheDocument(); - expect(result.getByTestId('fields-count').textContent).toContain('0'); }); it('should render field table with fields of all categories', () => { @@ -80,7 +77,6 @@ describe('FieldTable', () => { ); expect(result.container.getElementsByClassName('euiTableRow').length).toBe(defaultPageSize); - expect(result.getByTestId('fields-count').textContent).toContain(totalFields); }); it('should render field table with fields of categories selected', () => { @@ -103,7 +99,6 @@ describe('FieldTable', () => { ); expect(result.container.getElementsByClassName('euiTableRow').length).toBe(fieldCount); - expect(result.getByTestId('fields-count').textContent).toContain(fieldCount); }); it('should render field table with custom columns', () => { @@ -125,7 +120,6 @@ describe('FieldTable', () => { ); - expect(result.getByTestId('fields-count').textContent).toContain(totalFields); expect(result.getAllByText('Custom column').length).toBeGreaterThan(0); expect(result.getAllByTestId('customColumn').length).toEqual(defaultPageSize); }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.tsx index f578d4e1b9dca3..684b09d0395ab9 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.tsx @@ -7,14 +7,14 @@ import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; -import { EuiInMemoryTable, EuiText } from '@elastic/eui'; +import { EuiInMemoryTable } from '@elastic/eui'; import { useDispatch } from 'react-redux'; import { BrowserFields, ColumnHeaderOptions } from '../../../../../common'; -import * as i18n from './translations'; import { getColumnHeader, getFieldColumns, getFieldItems, isActionsColumn } from './field_items'; import { CATEGORY_TABLE_CLASS_NAME, TABLE_HEIGHT } from './helpers'; import { tGridActions } from '../../../../store/t_grid'; import type { GetFieldTableColumns } from '../../../../../common/types/fields_browser'; +import { FieldTableHeader } from './field_table_header'; export interface FieldTableProps { timelineId: string; @@ -25,6 +25,9 @@ export interface FieldTableProps { * the filter input (as a substring). */ filteredBrowserFields: BrowserFields; + /** when true, show only the the selected field */ + filterSelectedEnabled: boolean; + onFilterSelectedChange: (enabled: boolean) => void; /** * Optional function to customize field table columns */ @@ -58,9 +61,11 @@ Count.displayName = 'Count'; const FieldTableComponent: React.FC = ({ columnHeaders, filteredBrowserFields, + filterSelectedEnabled, getFieldTableColumns, searchInput, selectedCategoryIds, + onFilterSelectedChange, timelineId, onHide, }) => { @@ -106,13 +111,13 @@ const FieldTableComponent: React.FC = ({ return ( <> - - {i18n.FIELDS_SHOWING} - {fieldItems.length} - {i18n.FIELDS_COUNT(fieldItems.length)} - + - + = ({ pagination={true} sorting={true} hasActions={hasActions} + compressed /> diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table_header.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table_header.test.tsx new file mode 100644 index 00000000000000..e7c8f5b7fe7a4c --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table_header.test.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../../mock'; +import { FieldTableHeader, FieldTableHeaderProps } from './field_table_header'; + +const mockOnFilterSelectedChange = jest.fn(); +const defaultProps: FieldTableHeaderProps = { + fieldCount: 0, + filterSelectedEnabled: false, + onFilterSelectedChange: mockOnFilterSelectedChange, +}; + +describe('FieldTableHeader', () => { + describe('FieldCount', () => { + it('should render empty field table', () => { + const result = render( + + + + ); + + expect(result.getByTestId('fields-showing').textContent).toBe('Showing 0 fields'); + }); + + it('should render field table with one singular field count value', () => { + const result = render( + + + + ); + + expect(result.getByTestId('fields-showing').textContent).toBe('Showing 1 field'); + }); + it('should render field table with multiple fields count value', () => { + const result = render( + + + + ); + + expect(result.getByTestId('fields-showing').textContent).toBe('Showing 4 fields'); + }); + }); + + describe('View selected', () => { + beforeEach(() => { + mockOnFilterSelectedChange.mockClear(); + }); + + it('should render "view all" option when filterSelected is not enabled', () => { + const result = render( + + + + ); + + expect(result.getByTestId('viewSelectorButton').textContent).toBe('View: all'); + }); + + it('should render "view selected" option when filterSelected is not enabled', () => { + const result = render( + + + + ); + + expect(result.getByTestId('viewSelectorButton').textContent).toBe('View: selected'); + }); + + it('should open the view selector with button click', async () => { + const result = render( + + + + ); + + expect(result.queryByTestId('viewSelectorMenu')).not.toBeInTheDocument(); + expect(result.queryByTestId('viewSelectorOption-all')).not.toBeInTheDocument(); + expect(result.queryByTestId('viewSelectorOption-selected')).not.toBeInTheDocument(); + + result.getByTestId('viewSelectorButton').click(); + + expect(result.getByTestId('viewSelectorMenu')).toBeInTheDocument(); + expect(result.getByTestId('viewSelectorOption-all')).toBeInTheDocument(); + expect(result.getByTestId('viewSelectorOption-selected')).toBeInTheDocument(); + }); + + it('should callback when "view all" option is clicked', () => { + const result = render( + + + + ); + + result.getByTestId('viewSelectorButton').click(); + result.getByTestId('viewSelectorOption-all').click(); + expect(mockOnFilterSelectedChange).toHaveBeenCalledWith(false); + }); + + it('should callback when "view selected" option is clicked', () => { + const result = render( + + + + ); + + result.getByTestId('viewSelectorButton').click(); + result.getByTestId('viewSelectorOption-selected').click(); + expect(mockOnFilterSelectedChange).toHaveBeenCalledWith(true); + }); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table_header.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table_header.tsx new file mode 100644 index 00000000000000..ed7cc1e55b9c0b --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table_header.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useState } from 'react'; +import styled from 'styled-components'; +import { + EuiText, + EuiPopover, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiHorizontalRule, +} from '@elastic/eui'; +import * as i18n from './translations'; + +export interface FieldTableHeaderProps { + fieldCount: number; + filterSelectedEnabled: boolean; + onFilterSelectedChange: (enabled: boolean) => void; +} + +const Count = styled.span` + font-weight: bold; +`; +Count.displayName = 'Count'; + +const FieldTableHeaderComponent: React.FC = ({ + fieldCount, + filterSelectedEnabled, + onFilterSelectedChange, +}) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const togglePopover = useCallback(() => { + setIsPopoverOpen((open) => !open); + }, []); + + const closePopover = useCallback(() => { + setIsPopoverOpen(false); + }, []); + + return ( + + + + {i18n.FIELDS_SHOWING} + {fieldCount} + {i18n.FIELDS_COUNT(fieldCount)} + + + + + {`${i18n.VIEW_LABEL}: ${ + filterSelectedEnabled ? i18n.VIEW_VALUE_SELECTED : i18n.VIEW_VALUE_ALL + }`} + + } + > + { + onFilterSelectedChange(false); + closePopover(); + }} + > + {`${i18n.VIEW_LABEL} ${i18n.VIEW_VALUE_ALL}`} + , + , + { + onFilterSelectedChange(true); + closePopover(); + }} + > + {`${i18n.VIEW_LABEL} ${i18n.VIEW_VALUE_SELECTED}`} + , + ]} + /> + + + + ); +}; + +export const FieldTableHeader = React.memo(FieldTableHeaderComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx index ad90956013e41d..8f4377ce020fdf 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx @@ -9,11 +9,12 @@ import { mockBrowserFields } from '../../../../mock'; import { categoryHasFields, - createVirtualCategory, getFieldCount, filterBrowserFieldsByFieldName, + filterSelectedBrowserFields, } from './helpers'; import { BrowserFields } from '../../../../../common/search_strategy'; +import { ColumnHeaderOptions } from '../../../../../common'; describe('helpers', () => { describe('categoryHasFields', () => { @@ -255,144 +256,83 @@ describe('helpers', () => { }); }); - describe('createVirtualCategory', () => { - test('it combines the specified fields into a virtual category when the input ONLY contains field names that contain dots (e.g. agent.hostname)', () => { - const expectedMatchingFields = { - fields: { - 'agent.hostname': { - aggregatable: true, - category: 'agent', - description: null, - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.hostname', - searchable: true, - type: 'string', - }, - 'client.domain': { - aggregatable: true, - category: 'client', - description: 'Client domain.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.domain', - searchable: true, - type: 'string', - }, - 'client.geo.country_iso_code': { - aggregatable: true, - category: 'client', - description: 'Country ISO code.', - example: 'CA', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.geo.country_iso_code', - searchable: true, - type: 'string', - }, - }, - }; - - const fieldIds = ['agent.hostname', 'client.domain', 'client.geo.country_iso_code']; + describe('filterSelectedBrowserFields', () => { + const columnHeaders = [ + { id: 'agent.ephemeral_id' }, + { id: 'agent.id' }, + { id: 'container.id' }, + ] as ColumnHeaderOptions[]; - expect( - createVirtualCategory({ - browserFields: mockBrowserFields, - fieldIds, - }) - ).toEqual(expectedMatchingFields); + test('it returns an empty collection when browserFields is empty', () => { + expect(filterSelectedBrowserFields({ browserFields: {}, columnHeaders: [] })).toEqual({}); }); - test('it combines the specified fields into a virtual category when the input includes field names from the base category that do NOT contain dots (e.g. @timestamp)', () => { - const expectedMatchingFields = { - fields: { - 'agent.hostname': { - aggregatable: true, - category: 'agent', - description: null, - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.hostname', - searchable: true, - type: 'string', - }, - '@timestamp': { - aggregatable: true, - category: 'base', - description: - 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', - example: '2016-05-23T08:05:34.853Z', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: '@timestamp', - searchable: true, - type: 'date', - }, - 'client.domain': { - aggregatable: true, - category: 'client', - description: 'Client domain.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.domain', - searchable: true, - type: 'string', - }, - }, - }; - - const fieldIds = ['agent.hostname', '@timestamp', 'client.domain']; + test('it returns an empty collection when browserFields is empty and columnHeaders is non empty', () => { + expect(filterSelectedBrowserFields({ browserFields: {}, columnHeaders })).toEqual({}); + }); + test('it returns an empty collection when browserFields is NOT empty and columnHeaders is empty', () => { expect( - createVirtualCategory({ + filterSelectedBrowserFields({ browserFields: mockBrowserFields, - fieldIds, + columnHeaders: [], }) - ).toEqual(expectedMatchingFields); + ).toEqual({}); }); - test('it combines the specified fields into a virtual category omitting the fields missing in the browser fields', () => { - const expectedMatchingFields = { - fields: { - '@timestamp': { - aggregatable: true, - category: 'base', - description: - 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', - example: '2016-05-23T08:05:34.853Z', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: '@timestamp', - searchable: true, - type: 'date', + test('it returns (only) non-empty categories, where each category contains only the fields matching the substring', () => { + const filtered: BrowserFields = { + agent: { + fields: { + 'agent.ephemeral_id': { + aggregatable: true, + category: 'agent', + description: + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + }, + 'agent.id': { + aggregatable: true, + category: 'agent', + description: + 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', + example: '8a4f500d', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.id', + searchable: true, + type: 'string', + }, }, - 'client.domain': { - aggregatable: true, - category: 'client', - description: 'Client domain.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.domain', - searchable: true, - type: 'string', + }, + container: { + fields: { + 'container.id': { + aggregatable: true, + category: 'container', + description: 'Unique container id.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.id', + searchable: true, + type: 'string', + }, }, }, }; - const fieldIds = ['agent.hostname', '@timestamp', 'client.domain']; - const { agent, ...mockBrowserFieldsWithoutAgent } = mockBrowserFields; - expect( - createVirtualCategory({ - browserFields: mockBrowserFieldsWithoutAgent, - fieldIds, + filterSelectedBrowserFields({ + browserFields: mockBrowserFields, + columnHeaders, }) - ).toEqual(expectedMatchingFields); + ).toEqual(filtered); }); }); }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx index 21829bda265e1f..c0e10760730266 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx @@ -6,13 +6,13 @@ */ import { EuiBadge, EuiLoadingSpinner } from '@elastic/eui'; -import { filter, get, pickBy } from 'lodash/fp'; +import { pickBy } from 'lodash/fp'; import styled from 'styled-components'; import { TimelineId } from '../../../../../public/types'; import type { BrowserField, BrowserFields } from '../../../../../common/search_strategy'; import { defaultHeaders } from '../../../../store/t_grid/defaults'; -import { DEFAULT_CATEGORY_NAME } from '../../body/column_headers/default_headers'; +import { ColumnHeaderOptions } from '../../../../../common'; export const LoadingSpinner = styled(EuiLoadingSpinner)` cursor: pointer; @@ -45,6 +45,9 @@ export const filterBrowserFieldsByFieldName = ({ substring: string; }): BrowserFields => { const trimmedSubstring = substring.trim(); + if (trimmedSubstring === '') { + return browserFields; + } // filter each category such that it only contains fields with field names // that contain the specified substring: @@ -53,11 +56,10 @@ export const filterBrowserFieldsByFieldName = ({ ...filteredCategories, [categoryId]: { ...browserFields[categoryId], - fields: filter( - (f) => f.name != null && f.name.includes(trimmedSubstring), + fields: pickBy( + ({ name }) => name != null && name.includes(trimmedSubstring), browserFields[categoryId].fields - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - ).reduce((filtered, field) => ({ ...filtered, [field.name!]: field }), {}), + ), }, }), {} @@ -73,46 +75,40 @@ export const filterBrowserFieldsByFieldName = ({ }; /** - * Returns a "virtual" category (e.g. default ECS) from the specified fieldIds + * Filters the selected `BrowserFields` to return a new collection where every + * category contains at least one field that is present in the `columnHeaders`. */ -export const createVirtualCategory = ({ +export const filterSelectedBrowserFields = ({ browserFields, - fieldIds, + columnHeaders, }: { browserFields: BrowserFields; - fieldIds: string[]; -}): Partial => ({ - fields: fieldIds.reduce>((fields, fieldId) => { - const splitId = fieldId.split('.'); // source.geo.city_name -> [source, geo, city_name] - const browserField = get( - [splitId.length > 1 ? splitId[0] : 'base', 'fields', fieldId], - browserFields - ); - - return { - ...fields, - ...(browserField - ? { - [fieldId]: { - ...browserField, - name: fieldId, - }, - } - : {}), - }; - }, {}), -}); - -/** Merges the specified browser fields with the default category (i.e. `default ECS`) */ -export const mergeBrowserFieldsWithDefaultCategory = ( - browserFields: BrowserFields -): BrowserFields => ({ - ...browserFields, - [DEFAULT_CATEGORY_NAME]: createVirtualCategory({ - browserFields, - fieldIds: defaultHeaders.map((header) => header.id), - }), -}); + columnHeaders: ColumnHeaderOptions[]; +}): BrowserFields => { + const selectedFieldIds = new Set(columnHeaders.map(({ id }) => id)); + + const filteredBrowserFields: BrowserFields = Object.keys(browserFields).reduce( + (filteredCategories, categoryId) => ({ + ...filteredCategories, + [categoryId]: { + ...browserFields[categoryId], + fields: pickBy( + ({ name }) => name != null && selectedFieldIds.has(name), + browserFields[categoryId].fields + ), + }, + }), + {} + ); + + // only pick non-empty categories from the filtered browser fields + const nonEmptyCategories: BrowserFields = pickBy( + (category) => categoryHasFields(category), + filteredBrowserFields + ); + + return nonEmptyCategories; +}; export const getAlertColumnHeader = (timelineId: string, fieldId: string) => timelineId === TimelineId.detectionsPage || timelineId === TimelineId.detectionsRulesDetailsPage diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.tsx index c5647c973b9d8c..68bf6ca43ede94 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.tsx @@ -13,7 +13,7 @@ import styled from 'styled-components'; import type { BrowserFields } from '../../../../../common/search_strategy/index_fields'; import type { FieldBrowserProps } from '../../../../../common/types/fields_browser'; import { FieldsBrowser } from './field_browser'; -import { filterBrowserFieldsByFieldName, mergeBrowserFieldsWithDefaultCategory } from './helpers'; +import { filterBrowserFieldsByFieldName, filterSelectedBrowserFields } from './helpers'; import * as i18n from './translations'; const FIELDS_BUTTON_CLASS_NAME = 'fields-button'; @@ -44,6 +44,8 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ const [appliedFilterInput, setAppliedFilterInput] = useState(''); /** all fields in this collection have field names that match the filterInput */ const [filteredBrowserFields, setFilteredBrowserFields] = useState(null); + /** when true, show only the the selected field */ + const [filterSelectedEnabled, setFilterSelectedEnabled] = useState(false); /** when true, show a spinner in the input to indicate the field browser is searching for matching field names */ const [isSearching, setIsSearching] = useState(false); /** this category will be displayed in the right-hand pane of the field browser */ @@ -67,14 +69,23 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ }; }, [debouncedApplyFilterInput]); + const selectionFilteredBrowserFields = useMemo( + () => + filterSelectedEnabled + ? filterSelectedBrowserFields({ browserFields, columnHeaders }) + : browserFields, + [browserFields, columnHeaders, filterSelectedEnabled] + ); + useEffect(() => { - const newFilteredBrowserFields = filterBrowserFieldsByFieldName({ - browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields), - substring: appliedFilterInput, - }); - setFilteredBrowserFields(newFilteredBrowserFields); + setFilteredBrowserFields( + filterBrowserFieldsByFieldName({ + browserFields: selectionFilteredBrowserFields, + substring: appliedFilterInput, + }) + ); setIsSearching(false); - }, [appliedFilterInput, browserFields]); + }, [appliedFilterInput, selectionFilteredBrowserFields]); /** Shows / hides the field browser */ const onShow = useCallback(() => { @@ -86,6 +97,7 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ setFilterInput(''); setAppliedFilterInput(''); setFilteredBrowserFields(null); + setFilterSelectedEnabled(false); setIsSearching(false); setSelectedCategoryIds([]); setShow(false); @@ -101,10 +113,13 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ [debouncedApplyFilterInput] ); - // only merge in the default category if the field browser is visible - const browserFieldsWithDefaultCategory = useMemo(() => { - return show ? mergeBrowserFieldsWithDefaultCategory(browserFields) : {}; - }, [show, browserFields]); + /** Invoked when the user changes the view all/selected value */ + const onFilterSelectedChange = useCallback( + (filterSelected: boolean) => { + setFilterSelectedEnabled(filterSelected); + }, + [setFilterSelectedEnabled] + ); return ( @@ -125,13 +140,14 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ {show && ( values: { field }, defaultMessage: 'View {field} column', }); + +export const VIEW_LABEL = i18n.translate('xpack.timelines.fieldBrowser.viewLabel', { + defaultMessage: 'View', +}); + +export const VIEW_VALUE_SELECTED = i18n.translate('xpack.timelines.fieldBrowser.viewSelected', { + defaultMessage: 'selected', +}); + +export const VIEW_VALUE_ALL = i18n.translate('xpack.timelines.fieldBrowser.viewAll', { + defaultMessage: 'all', +}); From b0c3aab4e07f86dd2d3d9f13b2e6e1cad5116c6c Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 22 Mar 2022 11:18:19 -0500 Subject: [PATCH 09/64] [cft] Reuse elasticsearch snapshot on upgrade (#128264) Same-version snapshot upgrades have been causing deployments to become unhealthy. For now, lets reuse the original snapshot while we look for a workaround. --- .buildkite/scripts/steps/cloud/build_and_deploy.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/.buildkite/scripts/steps/cloud/build_and_deploy.sh b/.buildkite/scripts/steps/cloud/build_and_deploy.sh index 2bb9bc90e88da7..91d207f8fcb313 100755 --- a/.buildkite/scripts/steps/cloud/build_and_deploy.sh +++ b/.buildkite/scripts/steps/cloud/build_and_deploy.sh @@ -75,7 +75,6 @@ if [ -z "${CLOUD_DEPLOYMENT_ID}" ]; then else ecctl deployment show "$CLOUD_DEPLOYMENT_ID" --generate-update-payload | jq ' .resources.kibana[0].plan.kibana.docker_image = "'$CLOUD_IMAGE'" | - .resources.elasticsearch[0].plan.elasticsearch.docker_image = "'$ELASTICSEARCH_CLOUD_IMAGE'" | (.. | select(.version? != null).version) = "'$VERSION'" ' > /tmp/deploy.json ecctl deployment update "$CLOUD_DEPLOYMENT_ID" --track --output json --file /tmp/deploy.json &> "$JSON_FILE" From f4f51e692e17e94d285a00cf82f8bceab0c939da Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Tue, 22 Mar 2022 17:30:21 +0100 Subject: [PATCH 10/64] User Page - KPIs and visualisations (#127617) * Create total users visualization * Organize visualizations --- .../common/experimental_features.ts | 1 + .../security_solution/index.ts | 8 ++ .../security_solution/users/index.ts | 5 + .../users/kpi/common/index.ts | 13 +++ .../users/kpi/total_users/index.ts | 19 ++++ .../integration/pagination/pagination.spec.ts | 8 +- .../cypress/screens/inspect.ts | 5 - .../security_solution/cypress/tasks/login.ts | 2 + .../navigation/tab_navigation/index.test.tsx | 8 +- .../common/components/stat_items/index.tsx | 9 +- .../users/kpi_total_users_area.ts | 84 +++++++++++++++ .../users/kpi_total_users_metric.ts | 55 ++++++++++ .../common/lib/kibana/kibana_react.mock.ts | 7 ++ .../kpi_hosts/authentications/index.tsx | 4 +- .../components/kpi_hosts/common/index.tsx | 22 ++-- .../components/kpi_hosts/hosts/index.tsx | 4 +- .../hosts/components/kpi_hosts/index.tsx | 81 +++++++------- .../kpi_hosts/risky_hosts/index.test.tsx | 2 +- .../kpi_hosts/risky_hosts/index.tsx | 4 +- .../components/kpi_hosts/unique_ips/index.tsx | 4 +- .../public/hosts/pages/details/index.tsx | 10 +- .../hosts/pages/details/nav_tabs.test.tsx | 20 +++- .../public/hosts/pages/details/nav_tabs.tsx | 14 ++- .../public/hosts/pages/hosts.tsx | 7 +- .../public/hosts/pages/index.tsx | 82 +++++++------- .../public/hosts/pages/nav_tabs.test.tsx | 49 ++++++++- .../public/hosts/pages/nav_tabs.tsx | 17 ++- .../components/kpi_network/common/index.tsx | 89 ---------------- .../components/kpi_network/dns/index.tsx | 5 +- .../kpi_network/network_events/index.tsx | 5 +- .../kpi_network/tls_handshakes/index.tsx | 4 +- .../kpi_network/unique_flows/index.tsx | 4 +- .../kpi_network/unique_private_ips/index.tsx | 4 +- .../users/components/kpi_users/index.tsx | 9 +- .../kpi_users/total_users/index.tsx | 100 ++++++++++++++++++ .../kpi_users/total_users/translations.ts | 19 ++++ .../public/users/pages/nav_tabs.test.tsx | 32 ++++++ .../public/users/pages/nav_tabs.tsx | 56 ++++++---- .../public/users/pages/navigation/types.ts | 5 +- .../public/users/pages/users.tsx | 10 +- .../security_solution/factory/users/index.ts | 2 + .../factory/users/kpi/total_users/index.ts | 50 +++++++++ .../query.build_total_users_kpi.dsl.ts | 65 ++++++++++++ .../test/security_solution_cypress/config.ts | 1 + 44 files changed, 748 insertions(+), 256 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/common/index.ts create mode 100644 x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/total_users/index.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_total_users_area.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_total_users_metric.ts delete mode 100644 x-pack/plugins/security_solution/public/network/components/kpi_network/common/index.tsx create mode 100644 x-pack/plugins/security_solution/public/users/components/kpi_users/total_users/index.tsx create mode 100644 x-pack/plugins/security_solution/public/users/components/kpi_users/total_users/translations.ts create mode 100644 x-pack/plugins/security_solution/public/users/pages/nav_tabs.test.tsx create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/kpi/total_users/index.ts create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/kpi/total_users/query.build_total_users_kpi.dsl.ts diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 733d90eee2e65c..3a932238f3a345 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -21,6 +21,7 @@ export const allowedExperimentalValues = Object.freeze({ detectionResponseEnabled: false, disableIsolationUIPendingStatuses: false, riskyHostsEnabled: false, + riskyUsersEnabled: false, securityRulesCancelEnabled: false, pendingActionResponsesWithAck: true, policyListEnabled: false, diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index eb659b37a68885..a7176b3d309306 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -83,6 +83,10 @@ import { } from './risk_score'; import { UsersQueries } from './users'; import { UserDetailsRequestOptions, UserDetailsStrategyResponse } from './users/details'; +import { + TotalUsersKpiRequestOptions, + TotalUsersKpiStrategyResponse, +} from './users/kpi/total_users'; export * from './cti'; export * from './hosts'; @@ -141,6 +145,8 @@ export type StrategyResponseType = T extends HostsQ ? HostsKpiUniqueIpsStrategyResponse : T extends UsersQueries.details ? UserDetailsStrategyResponse + : T extends UsersQueries.kpiTotalUsers + ? TotalUsersKpiStrategyResponse : T extends NetworkQueries.details ? NetworkDetailsStrategyResponse : T extends NetworkQueries.dns @@ -199,6 +205,8 @@ export type StrategyRequestType = T extends HostsQu ? HostsKpiUniqueIpsRequestOptions : T extends UsersQueries.details ? UserDetailsRequestOptions + : T extends UsersQueries.kpiTotalUsers + ? TotalUsersKpiRequestOptions : T extends NetworkQueries.details ? NetworkDetailsRequestOptions : T extends NetworkQueries.dns diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/index.ts index fd5c90031b9a5c..d8f6172dd80c2e 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/index.ts @@ -5,6 +5,11 @@ * 2.0. */ +import { TotalUsersKpiStrategyResponse } from './kpi/total_users'; + export enum UsersQueries { details = 'userDetails', + kpiTotalUsers = 'usersKpiTotalUsers', } + +export type UserskKpiStrategyResponse = Omit; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/common/index.ts new file mode 100644 index 00000000000000..27f83e2ec623a5 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/common/index.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 { Maybe } from '../../../..'; + +export interface KpiHistogramData { + x?: Maybe; + y?: Maybe; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/total_users/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/total_users/index.ts new file mode 100644 index 00000000000000..9069393102a5bf --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/total_users/index.ts @@ -0,0 +1,19 @@ +/* + * 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 { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { Inspect, Maybe } from '../../../../common'; +import { RequestBasicOptions } from '../../..'; +import { KpiHistogramData } from '../common'; + +export type TotalUsersKpiRequestOptions = RequestBasicOptions; + +export interface TotalUsersKpiStrategyResponse extends IEsSearchResponse { + users: Maybe; + usersHistogram: Maybe; + inspect?: Maybe; +} diff --git a/x-pack/plugins/security_solution/cypress/integration/pagination/pagination.spec.ts b/x-pack/plugins/security_solution/cypress/integration/pagination/pagination.spec.ts index 645b2916d554d2..23115e2598c690 100644 --- a/x-pack/plugins/security_solution/cypress/integration/pagination/pagination.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/pagination/pagination.spec.ts @@ -12,8 +12,8 @@ import { import { FIRST_PAGE_SELECTOR, THIRD_PAGE_SELECTOR } from '../../screens/pagination'; import { cleanKibana } from '../../tasks/common'; -import { waitForAuthenticationsToBeLoaded } from '../../tasks/hosts/authentications'; -import { openAuthentications, openUncommonProcesses } from '../../tasks/hosts/main'; +import { waitsForEventsToBeLoaded } from '../../tasks/hosts/events'; +import { openEvents, openUncommonProcesses } from '../../tasks/hosts/main'; import { waitForUncommonProcessesToBeLoaded } from '../../tasks/hosts/uncommon_processes'; import { loginAndWaitForPage } from '../../tasks/login'; import { goToFirstPage, goToThirdPage } from '../../tasks/pagination'; @@ -73,8 +73,8 @@ describe('Pagination', () => { .first() .invoke('text') .then((expectedThirdPageResult) => { - openAuthentications(); - waitForAuthenticationsToBeLoaded(); + openEvents(); + waitsForEventsToBeLoaded(); cy.get(FIRST_PAGE_SELECTOR).should('have.class', 'euiPaginationButton-isActive'); openUncommonProcesses(); waitForUncommonProcessesToBeLoaded(); diff --git a/x-pack/plugins/security_solution/cypress/screens/inspect.ts b/x-pack/plugins/security_solution/cypress/screens/inspect.ts index f0fbf7e6a30890..3ee675ae7ca8c7 100644 --- a/x-pack/plugins/security_solution/cypress/screens/inspect.ts +++ b/x-pack/plugins/security_solution/cypress/screens/inspect.ts @@ -21,11 +21,6 @@ export const INSPECT_HOSTS_BUTTONS_IN_SECURITY: InspectButtonMetadata[] = [ title: 'All Hosts Table', tabId: '[data-test-subj="navigation-allHosts"]', }, - { - id: '[data-test-subj="table-authentications-loading-false"]', - title: 'Authentications Table', - tabId: '[data-test-subj="navigation-authentications"]', - }, { id: '[data-test-subj="table-uncommonProcesses-loading-false"]', title: 'Uncommon processes Table', diff --git a/x-pack/plugins/security_solution/cypress/tasks/login.ts b/x-pack/plugins/security_solution/cypress/tasks/login.ts index 736418325d3d2a..de68a3f41d57d0 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/login.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/login.ts @@ -359,6 +359,8 @@ export const loginAndWaitForTimeline = (timelineId: string, role?: ROLES) => { export const loginAndWaitForHostDetailsPage = (hostName = 'suricata-iowa') => { loginAndWaitForPage(hostDetailsUrl(hostName)); + + cy.get('[data-test-subj="hostDetailsPage"]', { timeout: 12000 }).should('exist'); cy.get('[data-test-subj="loading-spinner"]', { timeout: 12000 }).should('not.exist'); }; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx index d2a17e87cffcfa..d90709f69ee034 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx @@ -52,13 +52,19 @@ const hostName = 'siem-window'; describe('Table Navigation', () => { const mockHasMlUserPermissions = true; const mockRiskyHostEnabled = true; + const mockProps: TabNavigationProps & RouteSpyState = { pageName: 'hosts', pathName: '/hosts', detailName: undefined, search: '', tabName: HostsTableType.authentications, - navTabs: navTabsHostDetails(hostName, mockHasMlUserPermissions, mockRiskyHostEnabled), + navTabs: navTabsHostDetails({ + hostName, + hasMlUserPermissions: mockHasMlUserPermissions, + isRiskyHostsEnabled: mockRiskyHostEnabled, + }), + [CONSTANTS.timerange]: { global: { [CONSTANTS.timerange]: { diff --git a/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx b/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx index 93a13dd5dee8b3..424920d34e2e87 100644 --- a/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx @@ -34,6 +34,7 @@ import { InspectButton } from '../inspect'; import { VisualizationActions, HISTOGRAM_ACTIONS_BUTTON_CLASS } from '../visualization_actions'; import { HoverVisibilityContainer } from '../hover_visibility_container'; import { LensAttributes } from '../visualization_actions/types'; +import { UserskKpiStrategyResponse } from '../../../../common/search_strategy/security_solution/users'; const FlexItem = styled(EuiFlexItem)` min-width: 0; @@ -125,12 +126,12 @@ export const barchartConfigs = (config?: { onElementClick?: ElementClickListener export const addValueToFields = ( fields: StatItem[], - data: HostsKpiStrategyResponse | NetworkKpiStrategyResponse + data: HostsKpiStrategyResponse | NetworkKpiStrategyResponse | UserskKpiStrategyResponse ): StatItem[] => fields.map((field) => ({ ...field, value: get(field.key, data) })); export const addValueToAreaChart = ( fields: StatItem[], - data: HostsKpiStrategyResponse | NetworkKpiStrategyResponse + data: HostsKpiStrategyResponse | NetworkKpiStrategyResponse | UserskKpiStrategyResponse ): ChartSeriesData[] => fields .filter((field) => get(`${field.key}Histogram`, data) != null) @@ -142,7 +143,7 @@ export const addValueToAreaChart = ( export const addValueToBarChart = ( fields: StatItem[], - data: HostsKpiStrategyResponse | NetworkKpiStrategyResponse + data: HostsKpiStrategyResponse | NetworkKpiStrategyResponse | UserskKpiStrategyResponse ): ChartSeriesData[] => { if (fields.length === 0) return []; return fields.reduce((acc: ChartSeriesData[], field: StatItem, idx: number) => { @@ -171,7 +172,7 @@ export const addValueToBarChart = ( export const useKpiMatrixStatus = ( mappings: Readonly, - data: HostsKpiStrategyResponse | NetworkKpiStrategyResponse, + data: HostsKpiStrategyResponse | NetworkKpiStrategyResponse | UserskKpiStrategyResponse, id: string, from: string, to: string, diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_total_users_area.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_total_users_area.ts new file mode 100644 index 00000000000000..482086289e14da --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_total_users_area.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 { LensAttributes } from '../../types'; + +export const kpiTotalUsersAreaLensAttributes: LensAttributes = { + description: '', + state: { + datasourceStates: { + indexpattern: { + layers: { + '416b6fad-1923-4f6a-a2df-b223bb287e30': { + columnOrder: [ + '5eea817b-67b7-4268-8ecb-7688d1094721', + 'b00c65ea-32be-4163-bfc8-f795b1ef9d06', + ], + columns: { + '5eea817b-67b7-4268-8ecb-7688d1094721': { + dataType: 'date', + isBucketed: true, + label: '@timestamp', + operationType: 'date_histogram', + params: { interval: 'auto' }, + scale: 'interval', + sourceField: '@timestamp', + }, + 'b00c65ea-32be-4163-bfc8-f795b1ef9d06': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: ' ', + operationType: 'unique_count', + scale: 'ratio', + sourceField: 'user.name', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + filters: [], + query: { language: 'kuery', query: '' }, + visualization: { + axisTitlesVisibilitySettings: { x: false, yLeft: false, yRight: false }, + fittingFunction: 'None', + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + labelsOrientation: { x: 0, yLeft: 0, yRight: 0 }, + layers: [ + { + accessors: ['b00c65ea-32be-4163-bfc8-f795b1ef9d06'], + layerId: '416b6fad-1923-4f6a-a2df-b223bb287e30', + layerType: 'data', + seriesType: 'area', + xAccessor: '5eea817b-67b7-4268-8ecb-7688d1094721', + }, + ], + legend: { isVisible: true, position: 'right' }, + preferredSeriesType: 'area', + tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, + valueLabels: 'hide', + yLeftExtent: { mode: 'full' }, + yRightExtent: { mode: 'full' }, + }, + }, + title: '[User] Users - area', + visualizationType: 'lnsXY', + references: [ + { + id: '{dataViewId}', + name: 'indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }, + { + id: '{dataViewId}', + name: 'indexpattern-datasource-layer-416b6fad-1923-4f6a-a2df-b223bb287e30', + type: 'index-pattern', + }, + ], +} as LensAttributes; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_total_users_metric.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_total_users_metric.ts new file mode 100644 index 00000000000000..7f1d2253eb3be5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_total_users_metric.ts @@ -0,0 +1,55 @@ +/* + * 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 { LensAttributes } from '../../types'; + +export const kpiTotalUsersMetricLensAttributes: LensAttributes = { + description: '', + state: { + datasourceStates: { + indexpattern: { + layers: { + '416b6fad-1923-4f6a-a2df-b223bb287e30': { + columnOrder: ['3e51b035-872c-4b44-824b-fe069c222e91'], + columns: { + '3e51b035-872c-4b44-824b-fe069c222e91': { + dataType: 'number', + isBucketed: false, + label: 'Unique count of user.name', + operationType: 'unique_count', + scale: 'ratio', + sourceField: 'user.name', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + filters: [], + query: { language: 'kuery', query: '' }, + visualization: { + accessor: '3e51b035-872c-4b44-824b-fe069c222e91', + layerId: '416b6fad-1923-4f6a-a2df-b223bb287e30', + layerType: 'data', + }, + }, + title: '[User] Users - metric', + visualizationType: 'lnsMetric', + references: [ + { + id: '{dataViewId}', + name: 'indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }, + { + id: '{dataViewId}', + name: 'indexpattern-datasource-layer-416b6fad-1923-4f6a-a2df-b223bb287e30', + type: 'index-pattern', + }, + ], +} as LensAttributes; diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts index 366cd271fb57df..b683f4bd1375ac 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts @@ -139,6 +139,13 @@ export const createStartServicesMock = ( next: jest.fn(), unsubscribe: jest.fn(), })), + pipe: jest.fn().mockImplementation(() => ({ + subscribe: jest.fn().mockImplementation(() => ({ + error: jest.fn(), + next: jest.fn(), + unsubscribe: jest.fn(), + })), + })), })), }, }, diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx index ce73b1cd07f61b..1158c842e04cbc 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx @@ -13,7 +13,7 @@ import { kpiUserAuthenticationsBarLensAttributes } from '../../../../common/comp import { kpiUserAuthenticationsMetricSuccessLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_user_authentications_metric_success'; import { kpiUserAuthenticationsMetricFailureLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_user_authentication_metric_failure'; import { useHostsKpiAuthentications } from '../../../containers/kpi_hosts/authentications'; -import { HostsKpiBaseComponentManage } from '../common'; +import { KpiBaseComponentManage } from '../common'; import { HostsKpiProps, HostsKpiChartColors } from '../types'; import * as i18n from './translations'; @@ -66,7 +66,7 @@ const HostsKpiAuthenticationsComponent: React.FC = ({ }); return ( - ; - data: HostsKpiStrategyResponse; + data: HostsKpiStrategyResponse | NetworkKpiStrategyResponse | UserskKpiStrategyResponse; loading?: boolean; id: string; from: string; @@ -40,7 +44,7 @@ interface HostsKpiBaseComponentProps { narrowDateRange: UpdateDateRange; } -export const HostsKpiBaseComponent = React.memo( +export const KpiBaseComponent = React.memo( ({ fieldsMapping, data, id, loading = false, from, to, narrowDateRange }) => { const { cases } = useKibana().services; const CasesContext = cases.ui.getCasesContext(); @@ -57,7 +61,7 @@ export const HostsKpiBaseComponent = React.memo( ); if (loading) { - return ; + return ; } return ( @@ -80,12 +84,12 @@ export const HostsKpiBaseComponent = React.memo( deepEqual(prevProps.data, nextProps.data) ); -HostsKpiBaseComponent.displayName = 'HostsKpiBaseComponent'; +KpiBaseComponent.displayName = 'KpiBaseComponent'; -export const HostsKpiBaseComponentManage = manageQuery(HostsKpiBaseComponent); +export const KpiBaseComponentManage = manageQuery(KpiBaseComponent); -export const HostsKpiBaseComponentLoader: React.FC = () => ( - +export const KpiBaseComponentLoader: React.FC = () => ( + diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.tsx index 4e73a429fbc1df..79118b66a3f719 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.tsx @@ -11,7 +11,7 @@ import { StatItems } from '../../../../common/components/stat_items'; import { kpiHostAreaLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_host_area'; import { kpiHostMetricLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_host_metric'; import { useHostsKpiHosts } from '../../../containers/kpi_hosts/hosts'; -import { HostsKpiBaseComponentManage } from '../common'; +import { KpiBaseComponentManage } from '../common'; import { HostsKpiProps, HostsKpiChartColors } from '../types'; import * as i18n from './translations'; @@ -51,7 +51,7 @@ const HostsKpiHostsComponent: React.FC = ({ }); return ( - ( ({ filterQuery, from, indexNames, to, setQuery, skip, narrowDateRange }) => { const [_, { isModuleEnabled }] = useHostRiskScore({}); + const usersEnabled = useIsExperimentalFeatureEnabled('usersEnabled'); return ( <> @@ -59,8 +61,21 @@ export const HostsKpiComponent = React.memo( skip={skip} /> + {!usersEnabled && ( + + + + )} - ( skip={skip} /> - - + + ); + } +); + +HostsKpiComponent.displayName = 'HostsKpiComponent'; + +export const HostsDetailsKpiComponent = React.memo( + ({ filterQuery, from, indexNames, to, setQuery, skip, narrowDateRange }) => { + const usersEnabled = useIsExperimentalFeatureEnabled('usersEnabled'); + return ( + + {!usersEnabled && ( + + ( skip={skip} /> - - + )} + + + + ); } ); -HostsKpiComponent.displayName = 'HostsKpiComponent'; - -export const HostsDetailsKpiComponent = React.memo( - ({ filterQuery, from, indexNames, to, setQuery, skip, narrowDateRange }) => ( - - - - - - - - - ) -); - HostsDetailsKpiComponent.displayName = 'HostsDetailsKpiComponent'; diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.test.tsx index c4fa134bd88e21..b000b2f22dc95e 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.test.tsx @@ -36,7 +36,7 @@ describe('RiskyHosts', () => { ); - expect(getByTestId('hostsKpiLoader')).toBeInTheDocument(); + expect(getByTestId('KpiLoader')).toBeInTheDocument(); }); test('it displays 0 risky hosts when initializing', () => { diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx index d4897702f94073..f515490252d400 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx @@ -22,7 +22,7 @@ import { BUTTON_CLASS as INPECT_BUTTON_CLASS, } from '../../../../common/components/inspect'; -import { HostsKpiBaseComponentLoader } from '../common'; +import { KpiBaseComponentLoader } from '../common'; import * as i18n from './translations'; import { useInspectQuery } from '../../../../common/hooks/use_inspect_query'; @@ -66,7 +66,7 @@ const RiskyHostsComponent: React.FC<{ useErrorToast(i18n.ERROR_TITLE, error); if (loading) { - return ; + return ; } const criticalRiskCount = data?.kpiRiskScore.Critical ?? 0; diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/index.tsx index 2d95e3c98f4aeb..ef7bdfa1dc0313 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/index.tsx @@ -13,7 +13,7 @@ import { kpiUniqueIpsBarLensAttributes } from '../../../../common/components/vis import { kpiUniqueIpsDestinationMetricLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_destination_metric'; import { kpiUniqueIpsSourceMetricLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_source_metric'; import { useHostsKpiUniqueIps } from '../../../containers/kpi_hosts/unique_ips'; -import { HostsKpiBaseComponentManage } from '../common'; +import { KpiBaseComponentManage } from '../common'; import { HostsKpiProps, HostsKpiChartColors } from '../types'; import * as i18n from './translations'; @@ -66,7 +66,7 @@ const HostsKpiUniqueIpsComponent: React.FC = ({ }); return ( - = ({ detailName, hostDeta diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.test.tsx index 90f3c223c5501f..8b951722439a6e 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.test.tsx @@ -11,7 +11,11 @@ import { navTabsHostDetails } from './nav_tabs'; describe('navTabsHostDetails', () => { const mockHostName = 'mockHostName'; test('it should skip anomalies tab if without mlUserPermission', () => { - const tabs = navTabsHostDetails(mockHostName, false, false); + const tabs = navTabsHostDetails({ + hasMlUserPermissions: false, + isRiskyHostsEnabled: false, + hostName: mockHostName, + }); expect(tabs).toHaveProperty(HostsTableType.authentications); expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses); expect(tabs).not.toHaveProperty(HostsTableType.anomalies); @@ -20,7 +24,12 @@ describe('navTabsHostDetails', () => { }); test('it should display anomalies tab if with mlUserPermission', () => { - const tabs = navTabsHostDetails(mockHostName, true, false); + const tabs = navTabsHostDetails({ + hasMlUserPermissions: true, + isRiskyHostsEnabled: false, + hostName: mockHostName, + }); + expect(tabs).toHaveProperty(HostsTableType.authentications); expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses); expect(tabs).toHaveProperty(HostsTableType.anomalies); @@ -29,7 +38,12 @@ describe('navTabsHostDetails', () => { }); test('it should display risky hosts tab if when risky hosts is enabled', () => { - const tabs = navTabsHostDetails(mockHostName, false, true); + const tabs = navTabsHostDetails({ + hasMlUserPermissions: false, + isRiskyHostsEnabled: true, + hostName: mockHostName, + }); + expect(tabs).toHaveProperty(HostsTableType.authentications); expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses); expect(tabs).not.toHaveProperty(HostsTableType.anomalies); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx index c58fbde09aef1d..33cafd8ef21149 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx @@ -14,11 +14,15 @@ import { HOSTS_PATH } from '../../../../common/constants'; const getTabsOnHostDetailsUrl = (hostName: string, tabName: HostsTableType) => `${HOSTS_PATH}/${hostName}/${tabName}`; -export const navTabsHostDetails = ( - hostName: string, - hasMlUserPermissions: boolean, - isRiskyHostsEnabled: boolean -): HostDetailsNavTab => { +export const navTabsHostDetails = ({ + hasMlUserPermissions, + isRiskyHostsEnabled, + hostName, +}: { + hostName: string; + hasMlUserPermissions: boolean; + isRiskyHostsEnabled: boolean; +}): HostDetailsNavTab => { const hiddenTabs = []; const hostDetailsNavTabs = { diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index 5ca7aa1f1dd497..3b57a22d15a6ab 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -151,6 +151,7 @@ const HostsComponent = () => { ); const riskyHostsFeatureEnabled = useIsExperimentalFeatureEnabled('riskyHostsEnabled'); + const usersEnabled = useIsExperimentalFeatureEnabled('usersEnabled'); useInvalidFilterQuery({ id: ID, filterQuery, kqlError, query, startDate: from, endDate: to }); @@ -214,7 +215,11 @@ const HostsComponent = () => { diff --git a/x-pack/plugins/security_solution/public/hosts/pages/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/index.tsx index 4eb8175aea4cb6..453d6182984c19 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/index.tsx @@ -34,48 +34,46 @@ const getHostDetailsTabPath = () => `${HostsTableType.risk}|` + `${HostsTableType.alerts})`; -export const HostsContainer = React.memo(() => { - return ( - - - - - - - - } - /> - ( - - )} - /> +export const HostsContainer = React.memo(() => ( + + + + + + + + } + /> + ( + + )} + /> - ( - - )} - /> - - ); -}); + ( + + )} + /> + +)); HostsContainer.displayName = 'HostsContainer'; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.test.tsx index 50e301d4b4f57a..b882dca3faaf12 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.test.tsx @@ -10,7 +10,11 @@ import { navTabsHosts } from './nav_tabs'; describe('navTabsHosts', () => { test('it should skip anomalies tab if without mlUserPermission', () => { - const tabs = navTabsHosts(false, false); + const tabs = navTabsHosts({ + hasMlUserPermissions: false, + isRiskyHostsEnabled: false, + isUsersEnabled: false, + }); expect(tabs).toHaveProperty(HostsTableType.hosts); expect(tabs).toHaveProperty(HostsTableType.authentications); expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses); @@ -19,15 +23,24 @@ describe('navTabsHosts', () => { }); test('it should display anomalies tab if with mlUserPermission', () => { - const tabs = navTabsHosts(true, false); + const tabs = navTabsHosts({ + hasMlUserPermissions: true, + isRiskyHostsEnabled: false, + isUsersEnabled: false, + }); expect(tabs).toHaveProperty(HostsTableType.hosts); expect(tabs).toHaveProperty(HostsTableType.authentications); expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses); expect(tabs).toHaveProperty(HostsTableType.anomalies); expect(tabs).toHaveProperty(HostsTableType.events); }); + test('it should skip risk tab if without hostRisk', () => { - const tabs = navTabsHosts(false, false); + const tabs = navTabsHosts({ + hasMlUserPermissions: false, + isRiskyHostsEnabled: false, + isUsersEnabled: false, + }); expect(tabs).toHaveProperty(HostsTableType.hosts); expect(tabs).toHaveProperty(HostsTableType.authentications); expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses); @@ -36,11 +49,39 @@ describe('navTabsHosts', () => { }); test('it should display risk tab if with hostRisk', () => { - const tabs = navTabsHosts(false, true); + const tabs = navTabsHosts({ + hasMlUserPermissions: false, + isRiskyHostsEnabled: true, + isUsersEnabled: false, + }); expect(tabs).toHaveProperty(HostsTableType.hosts); expect(tabs).toHaveProperty(HostsTableType.authentications); expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses); expect(tabs).toHaveProperty(HostsTableType.risk); expect(tabs).toHaveProperty(HostsTableType.events); }); + + test('it should skip authentications tab if isUsersEnabled is true', () => { + const tabs = navTabsHosts({ + hasMlUserPermissions: false, + isRiskyHostsEnabled: false, + isUsersEnabled: true, + }); + expect(tabs).toHaveProperty(HostsTableType.hosts); + expect(tabs).not.toHaveProperty(HostsTableType.authentications); + expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses); + expect(tabs).toHaveProperty(HostsTableType.events); + }); + + test('it should display authentications tab if isUsersEnabled is false', () => { + const tabs = navTabsHosts({ + hasMlUserPermissions: false, + isRiskyHostsEnabled: false, + isUsersEnabled: false, + }); + expect(tabs).toHaveProperty(HostsTableType.hosts); + expect(tabs).toHaveProperty(HostsTableType.authentications); + expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses); + expect(tabs).toHaveProperty(HostsTableType.events); + }); }); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.tsx index 0d8a5e252bfbb7..789273da073e94 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.tsx @@ -13,10 +13,15 @@ import { HOSTS_PATH } from '../../../common/constants'; const getTabsOnHostsUrl = (tabName: HostsTableType) => `${HOSTS_PATH}/${tabName}`; -export const navTabsHosts = ( - hasMlUserPermissions: boolean, - isRiskyHostsEnabled: boolean -): HostsNavTab => { +export const navTabsHosts = ({ + hasMlUserPermissions, + isRiskyHostsEnabled, + isUsersEnabled, +}: { + hasMlUserPermissions: boolean; + isRiskyHostsEnabled: boolean; + isUsersEnabled: boolean; +}): HostsNavTab => { const hiddenTabs = []; const hostsNavTabs = { [HostsTableType.hosts]: { @@ -71,5 +76,9 @@ export const navTabsHosts = ( hiddenTabs.push(HostsTableType.risk); } + if (isUsersEnabled) { + hiddenTabs.push(HostsTableType.authentications); + } + return omit(hiddenTabs, hostsNavTabs); }; diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/common/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/common/index.tsx deleted file mode 100644 index 8fbc75aff4e19c..00000000000000 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/common/index.tsx +++ /dev/null @@ -1,89 +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 React from 'react'; -import { EuiFlexItem, EuiLoadingSpinner, EuiFlexGroup } from '@elastic/eui'; -import styled from 'styled-components'; -import deepEqual from 'fast-deep-equal'; - -import { manageQuery } from '../../../../common/components/page/manage_query'; -import { NetworkKpiStrategyResponse } from '../../../../../common/search_strategy'; -import { - StatItemsComponent, - StatItemsProps, - useKpiMatrixStatus, - StatItems, -} from '../../../../common/components/stat_items'; -import { UpdateDateRange } from '../../../../common/components/charts/common'; -import { useKibana, useGetUserCasesPermissions } from '../../../../common/lib/kibana'; -import { APP_ID } from '../../../../../common/constants'; - -const kpiWidgetHeight = 228; - -export const FlexGroup = styled(EuiFlexGroup)` - min-height: ${kpiWidgetHeight}px; -`; - -FlexGroup.displayName = 'FlexGroup'; - -export const NetworkKpiBaseComponent = React.memo<{ - fieldsMapping: Readonly; - data: NetworkKpiStrategyResponse; - loading?: boolean; - id: string; - from: string; - to: string; - narrowDateRange: UpdateDateRange; -}>( - ({ fieldsMapping, data, id, loading = false, from, to, narrowDateRange }) => { - const { cases } = useKibana().services; - const CasesContext = cases.ui.getCasesContext(); - const userPermissions = useGetUserCasesPermissions(); - const userCanCrud = userPermissions?.crud ?? false; - - const statItemsProps: StatItemsProps[] = useKpiMatrixStatus( - fieldsMapping, - data, - id, - from, - to, - narrowDateRange - ); - - if (loading) { - return ( - - - - - - ); - } - - return ( - - - {statItemsProps.map((mappedStatItemProps) => ( - - ))} - - - ); - }, - (prevProps, nextProps) => - prevProps.fieldsMapping === nextProps.fieldsMapping && - prevProps.loading === nextProps.loading && - prevProps.id === nextProps.id && - prevProps.from === nextProps.from && - prevProps.to === nextProps.to && - prevProps.narrowDateRange === nextProps.narrowDateRange && - deepEqual(prevProps.data, nextProps.data) -); - -NetworkKpiBaseComponent.displayName = 'NetworkKpiBaseComponent'; - -export const NetworkKpiBaseComponentManage = manageQuery(NetworkKpiBaseComponent); diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.tsx index 2c9db1cde6daf0..6291e7fd4dc126 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.tsx @@ -9,8 +9,9 @@ import React from 'react'; import { StatItems } from '../../../../common/components/stat_items'; import { kpiDnsQueriesLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/network/kpi_dns_queries'; +import { KpiBaseComponentManage } from '../../../../hosts/components/kpi_hosts/common'; import { useNetworkKpiDns } from '../../../containers/kpi_network/dns'; -import { NetworkKpiBaseComponentManage } from '../common'; + import { NetworkKpiProps } from '../types'; import * as i18n from './translations'; @@ -46,7 +47,7 @@ const NetworkKpiDnsComponent: React.FC = ({ }); return ( - = ({ }); return ( - = ({ }); return ( - = ({ }); return ( - = ({ }); return ( - ( ({ filterQuery, from, indexNames, to, setQuery, skip, narrowDateRange }) => { @@ -19,7 +19,7 @@ export const UsersKpiComponent = React.memo( <> - ( skip={skip} /> - - + = [ + { + key: 'users', + fields: [ + { + key: 'users', + value: null, + color: euiColorVis1, + icon: 'storage', + lensAttributes: kpiTotalUsersMetricLensAttributes, + }, + ], + enableAreaChart: true, + description: i18n.USERS, + areaChartLensAttributes: kpiTotalUsersAreaLensAttributes, + }, +]; + +export interface UsersKpiProps { + filterQuery?: string; + from: string; + to: string; + indexNames: string[]; + narrowDateRange: UpdateDateRange; + setQuery: GlobalTimeArgs['setQuery']; + skip: boolean; +} + +const QUERY_ID = 'TotalUsersKpiQuery'; + +const TotalUsersKpiComponent: React.FC = ({ + filterQuery, + from, + indexNames, + to, + narrowDateRange, + setQuery, + skip, +}) => { + const { loading, result, search, refetch, inspect } = + useSearchStrategy({ + factoryQueryType: UsersQueries.kpiTotalUsers, + initialResult: { users: 0, usersHistogram: [] }, + errorMessage: i18n.ERROR_USERS_KPI, + }); + + useEffect(() => { + if (!skip) { + search({ + filterQuery, + defaultIndex: indexNames, + timerange: { + interval: '12h', + from, + to, + }, + }); + } + }, [search, from, to, filterQuery, indexNames, skip]); + + return ( + + ); +}; + +export const TotalUsersKpi = React.memo(TotalUsersKpiComponent); diff --git a/x-pack/plugins/security_solution/public/users/components/kpi_users/total_users/translations.ts b/x-pack/plugins/security_solution/public/users/components/kpi_users/total_users/translations.ts new file mode 100644 index 00000000000000..3bbcf3f08c1198 --- /dev/null +++ b/x-pack/plugins/security_solution/public/users/components/kpi_users/total_users/translations.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const USERS = i18n.translate('xpack.securitySolution.kpiUsers.totalUsers.title', { + defaultMessage: 'Users', +}); + +export const ERROR_USERS_KPI = i18n.translate( + 'xpack.securitySolution.kpiUsers.totalUsers.errorSearchDescription', + { + defaultMessage: `An error has occurred on total users kpi search`, + } +); diff --git a/x-pack/plugins/security_solution/public/users/pages/nav_tabs.test.tsx b/x-pack/plugins/security_solution/public/users/pages/nav_tabs.test.tsx new file mode 100644 index 00000000000000..492f85ec7ec027 --- /dev/null +++ b/x-pack/plugins/security_solution/public/users/pages/nav_tabs.test.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UsersTableType } from '../store/model'; +import { navTabsUsers } from './nav_tabs'; + +describe('navTabsUsers', () => { + test('it should display all tabs', () => { + const tabs = navTabsUsers(true, true); + expect(tabs).toHaveProperty(UsersTableType.allUsers); + expect(tabs).toHaveProperty(UsersTableType.anomalies); + expect(tabs).toHaveProperty(UsersTableType.risk); + }); + + test('it should not display anomalies tab if user has no ml permission', () => { + const tabs = navTabsUsers(false, true); + expect(tabs).toHaveProperty(UsersTableType.allUsers); + expect(tabs).not.toHaveProperty(UsersTableType.anomalies); + expect(tabs).toHaveProperty(UsersTableType.risk); + }); + + test('it should not display risk tab if isRiskyUserEnabled disabled', () => { + const tabs = navTabsUsers(true, false); + expect(tabs).toHaveProperty(UsersTableType.allUsers); + expect(tabs).toHaveProperty(UsersTableType.anomalies); + expect(tabs).not.toHaveProperty(UsersTableType.risk); + }); +}); diff --git a/x-pack/plugins/security_solution/public/users/pages/nav_tabs.tsx b/x-pack/plugins/security_solution/public/users/pages/nav_tabs.tsx index f991316983f49d..35124d1deddb12 100644 --- a/x-pack/plugins/security_solution/public/users/pages/nav_tabs.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/nav_tabs.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { omit } from 'lodash/fp'; import * as i18n from './translations'; import { UsersTableType } from '../store/model'; import { UsersNavTab } from './navigation/types'; @@ -12,23 +13,40 @@ import { USERS_PATH } from '../../../common/constants'; const getTabsOnUsersUrl = (tabName: UsersTableType) => `${USERS_PATH}/${tabName}`; -export const navTabsUsers: UsersNavTab = { - [UsersTableType.allUsers]: { - id: UsersTableType.allUsers, - name: i18n.NAVIGATION_ALL_USERS_TITLE, - href: getTabsOnUsersUrl(UsersTableType.allUsers), - disabled: false, - }, - [UsersTableType.anomalies]: { - id: UsersTableType.anomalies, - name: i18n.NAVIGATION_ANOMALIES_TITLE, - href: getTabsOnUsersUrl(UsersTableType.anomalies), - disabled: false, - }, - [UsersTableType.risk]: { - id: UsersTableType.risk, - name: i18n.NAVIGATION_RISK_TITLE, - href: getTabsOnUsersUrl(UsersTableType.risk), - disabled: false, - }, +export const navTabsUsers = ( + hasMlUserPermissions: boolean, + isRiskyUserEnabled: boolean +): UsersNavTab => { + const hiddenTabs = []; + + const userNavTabs = { + [UsersTableType.allUsers]: { + id: UsersTableType.allUsers, + name: i18n.NAVIGATION_ALL_USERS_TITLE, + href: getTabsOnUsersUrl(UsersTableType.allUsers), + disabled: false, + }, + [UsersTableType.anomalies]: { + id: UsersTableType.anomalies, + name: i18n.NAVIGATION_ANOMALIES_TITLE, + href: getTabsOnUsersUrl(UsersTableType.anomalies), + disabled: false, + }, + [UsersTableType.risk]: { + id: UsersTableType.risk, + name: i18n.NAVIGATION_RISK_TITLE, + href: getTabsOnUsersUrl(UsersTableType.risk), + disabled: false, + }, + }; + + if (!hasMlUserPermissions) { + hiddenTabs.push(UsersTableType.anomalies); + } + + if (!isRiskyUserEnabled) { + hiddenTabs.push(UsersTableType.risk); + } + + return omit(hiddenTabs, userNavTabs); }; diff --git a/x-pack/plugins/security_solution/public/users/pages/navigation/types.ts b/x-pack/plugins/security_solution/public/users/pages/navigation/types.ts index 1e4c28f38450ef..f3fd099d78548f 100644 --- a/x-pack/plugins/security_solution/public/users/pages/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/users/pages/navigation/types.ts @@ -10,7 +10,10 @@ import { ESTermQuery } from '../../../../common/typed_json'; import { DocValueFields } from '../../../../../timelines/common'; import { NavTab } from '../../../common/components/navigation/types'; -type KeyUsersNavTab = UsersTableType.allUsers | UsersTableType.anomalies | UsersTableType.risk; +type KeyUsersNavTabWithoutMlPermission = UsersTableType.allUsers & UsersTableType.risk; +type KeyUsersNavTabWithMlPermission = KeyUsersNavTabWithoutMlPermission & UsersTableType.anomalies; + +type KeyUsersNavTab = KeyUsersNavTabWithoutMlPermission | KeyUsersNavTabWithMlPermission; export type UsersNavTab = Record; export interface QueryTabBodyProps { diff --git a/x-pack/plugins/security_solution/public/users/pages/users.tsx b/x-pack/plugins/security_solution/public/users/pages/users.tsx index 91cdb5cc1e4304..bd6cc2d097c46c 100644 --- a/x-pack/plugins/security_solution/public/users/pages/users.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/users.tsx @@ -46,6 +46,9 @@ import { UpdateDateRange } from '../../common/components/charts/common'; import { LastEventIndexKey } from '../../../common/search_strategy'; import { generateSeverityFilter } from '../../hosts/store/helpers'; import { UsersTableType } from '../store/model'; +import { hasMlUserPermissions } from '../../../common/machine_learning/has_ml_user_permissions'; +import { useMlCapabilities } from '../../common/components/ml/hooks/use_ml_capabilities'; +import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; const ID = 'UsersQueryId'; @@ -157,6 +160,9 @@ const UsersComponent = () => { [dispatch] ); + const capabilities = useMlCapabilities(); + const riskyUsersFeatureEnabled = useIsExperimentalFeatureEnabled('riskyUsersEnabled'); + return ( <> {indicesExist ? ( @@ -191,7 +197,9 @@ const UsersComponent = () => { - + diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/index.ts index 211d9a71f8a552..2fe2f44c94e8dd 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/index.ts @@ -10,7 +10,9 @@ import { UsersQueries } from '../../../../../common/search_strategy/security_sol import { SecuritySolutionFactory } from '../types'; import { userDetails } from './details'; +import { totalUsersKpi } from './kpi/total_users'; export const usersFactory: Record> = { [UsersQueries.details]: userDetails, + [UsersQueries.kpiTotalUsers]: totalUsersKpi, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/kpi/total_users/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/kpi/total_users/index.ts new file mode 100644 index 00000000000000..50e4cfe50bca23 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/kpi/total_users/index.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +/* + * 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 { getOr } from 'lodash/fp'; + +import type { IEsSearchResponse } from '../../../../../../../../../../src/plugins/data/common'; +import { UsersQueries } from '../../../../../../../common/search_strategy/security_solution/users'; +import { + TotalUsersKpiRequestOptions, + TotalUsersKpiStrategyResponse, +} from '../../../../../../../common/search_strategy/security_solution/users/kpi/total_users'; + +import { inspectStringifyObject } from '../../../../../../utils/build_query'; +import { formatGeneralHistogramData } from '../../../hosts/kpi'; +import { SecuritySolutionFactory } from '../../../types'; +import { buildTotalUsersKpiQuery } from './query.build_total_users_kpi.dsl'; + +export const totalUsersKpi: SecuritySolutionFactory = { + buildDsl: (options: TotalUsersKpiRequestOptions) => buildTotalUsersKpiQuery(options), + parse: async ( + options: TotalUsersKpiRequestOptions, + response: IEsSearchResponse + ): Promise => { + const inspect = { + dsl: [inspectStringifyObject(buildTotalUsersKpiQuery(options))], + }; + + const usersHistogram = getOr( + null, + 'aggregations.users_histogram.buckets', + response.rawResponse + ); + return { + ...response, + inspect, + users: getOr(null, 'aggregations.users.value', response.rawResponse), + usersHistogram: formatGeneralHistogramData(usersHistogram), + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/kpi/total_users/query.build_total_users_kpi.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/kpi/total_users/query.build_total_users_kpi.dsl.ts new file mode 100644 index 00000000000000..d86763e4cd3f60 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/kpi/total_users/query.build_total_users_kpi.dsl.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HostsKpiHostsRequestOptions } from '../../../../../../../common/search_strategy/security_solution/hosts'; +import { createQueryFilterClauses } from '../../../../../../utils/build_query'; + +export const buildTotalUsersKpiQuery = ({ + filterQuery, + timerange: { from, to }, + defaultIndex, +}: HostsKpiHostsRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const dslQuery = { + index: defaultIndex, + allow_no_indices: true, + ignore_unavailable: true, + track_total_hits: false, + body: { + aggregations: { + users: { + cardinality: { + field: 'user.name', + }, + }, + users_histogram: { + auto_date_histogram: { + field: '@timestamp', + buckets: 6, + }, + aggs: { + count: { + cardinality: { + field: 'user.name', + }, + }, + }, + }, + }, + query: { + bool: { + filter, + }, + }, + size: 0, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/test/security_solution_cypress/config.ts b/x-pack/test/security_solution_cypress/config.ts index 47be0c9b2c8cec..e32283685f0b2b 100644 --- a/x-pack/test/security_solution_cypress/config.ts +++ b/x-pack/test/security_solution_cypress/config.ts @@ -51,6 +51,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { `--xpack.securitySolution.enableExperimental=${JSON.stringify([ 'riskyHostsEnabled', 'usersEnabled', + 'riskyUsersEnabled', 'ruleRegistryEnabled', ])}`, `--home.disableWelcomeScreen=true`, From ef570f6f0c48581423fbcf4b04e37215a2c7a2a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ece=20=C3=96zalp?= Date: Tue, 22 Mar 2022 12:58:26 -0400 Subject: [PATCH 11/64] [CTI] fixes rule preview incorrect interval and from values (#128003) Co-authored-by: Ece Ozalp Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/detection_engine/constants.ts | 14 +++++++++++ .../rules/use_preview_rule.ts | 25 ++++++++++++++++--- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/common/detection_engine/constants.ts b/x-pack/plugins/security_solution/common/detection_engine/constants.ts index 7f3c8228006735..b61cd34dc4790f 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/constants.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/constants.ts @@ -11,3 +11,17 @@ export enum RULE_PREVIEW_INVOCATION_COUNT { WEEK = 168, MONTH = 30, } + +export enum RULE_PREVIEW_INTERVAL { + HOUR = '5m', + DAY = '1h', + WEEK = '1h', + MONTH = '1d', +} + +export enum RULE_PREVIEW_FROM { + HOUR = 'now-6m', + DAY = 'now-65m', + WEEK = 'now-65m', + MONTH = 'now-25h', +} diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_preview_rule.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_preview_rule.ts index 43572ddaf4d37c..b610e96273ebd2 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_preview_rule.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_preview_rule.ts @@ -8,7 +8,11 @@ import { useEffect, useState } from 'react'; import { Unit } from '@elastic/datemath'; -import { RULE_PREVIEW_INVOCATION_COUNT } from '../../../../../common/detection_engine/constants'; +import { + RULE_PREVIEW_FROM, + RULE_PREVIEW_INTERVAL, + RULE_PREVIEW_INVOCATION_COUNT, +} from '../../../../../common/detection_engine/constants'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { PreviewResponse, @@ -31,16 +35,24 @@ export const usePreviewRule = (timeframe: Unit = 'h') => { const [isLoading, setIsLoading] = useState(false); const { addError } = useAppToasts(); let invocationCount = RULE_PREVIEW_INVOCATION_COUNT.HOUR; + let interval = RULE_PREVIEW_INTERVAL.HOUR; + let from = RULE_PREVIEW_FROM.HOUR; switch (timeframe) { case 'd': invocationCount = RULE_PREVIEW_INVOCATION_COUNT.DAY; + interval = RULE_PREVIEW_INTERVAL.DAY; + from = RULE_PREVIEW_FROM.DAY; break; case 'w': invocationCount = RULE_PREVIEW_INVOCATION_COUNT.WEEK; + interval = RULE_PREVIEW_INTERVAL.WEEK; + from = RULE_PREVIEW_FROM.WEEK; break; case 'M': invocationCount = RULE_PREVIEW_INVOCATION_COUNT.MONTH; + interval = RULE_PREVIEW_INTERVAL.MONTH; + from = RULE_PREVIEW_FROM.MONTH; break; } @@ -60,7 +72,14 @@ export const usePreviewRule = (timeframe: Unit = 'h') => { try { setIsLoading(true); const previewRuleResponse = await previewRule({ - rule: { ...transformOutput(rule), invocationCount }, + rule: { + ...transformOutput({ + ...rule, + interval, + from, + }), + invocationCount, + }, signal: abortCtrl.signal, }); if (isSubscribed) { @@ -82,7 +101,7 @@ export const usePreviewRule = (timeframe: Unit = 'h') => { isSubscribed = false; abortCtrl.abort(); }; - }, [rule, addError, invocationCount]); + }, [rule, addError, invocationCount, from, interval]); return { isLoading, response, rule, setRule }; }; From 725000d679348fe103aeba0c77c5a9e4d9a8486d Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 22 Mar 2022 18:51:26 +0100 Subject: [PATCH 12/64] [Lens] Implement null instead of zero switch (#127731) * implement null instead of zero switch * make default * fix tests * fix test * move into advanced options * show switch Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../search/aggs/buckets/multi_terms.test.ts | 3 + .../common/search/aggs/buckets/terms.test.ts | 3 + .../common/search/aggs/metrics/cardinality.ts | 2 + .../aggs/metrics/cardinality_fn.test.ts | 1 + .../search/aggs/metrics/cardinality_fn.ts | 7 ++ .../data/common/search/aggs/metrics/count.ts | 15 +++- .../search/aggs/metrics/count_fn.test.ts | 1 + .../common/search/aggs/metrics/count_fn.ts | 7 ++ .../search/aggs/metrics/metric_agg_type.ts | 20 ++++- .../data/common/search/aggs/metrics/sum.ts | 2 + .../common/search/aggs/metrics/sum_fn.test.ts | 1 + .../data/common/search/aggs/metrics/sum_fn.ts | 7 ++ src/plugins/data/common/search/aggs/types.ts | 3 +- .../search/tabify/response_writer.test.ts | 2 + .../main/utils/fetch_chart.test.ts | 4 +- .../main/utils/get_chart_agg_config.test.ts | 4 +- .../snapshots/baseline/combined_test2.json | 2 +- .../snapshots/baseline/combined_test3.json | 2 +- .../snapshots/baseline/final_output_test.json | 2 +- .../snapshots/baseline/metric_all_data.json | 2 +- .../snapshots/baseline/metric_empty_data.json | 2 +- .../baseline/metric_multi_metric_data.json | 2 +- .../baseline/metric_percentage_mode.json | 2 +- .../baseline/metric_single_metric_data.json | 2 +- .../snapshots/baseline/partial_test_1.json | 2 +- .../snapshots/baseline/partial_test_2.json | 2 +- .../snapshots/baseline/step_output_test2.json | 2 +- .../snapshots/baseline/step_output_test3.json | 2 +- .../snapshots/baseline/tagcloud_all_data.json | 2 +- .../baseline/tagcloud_empty_data.json | 2 +- .../snapshots/baseline/tagcloud_fontsize.json | 2 +- .../baseline/tagcloud_metric_data.json | 2 +- .../snapshots/baseline/tagcloud_options.json | 2 +- .../dimension_panel/advanced_options.tsx | 40 +++++----- .../dimension_panel/dimension_editor.tsx | 50 ++++++------ .../dimension_panel/dimension_panel.test.tsx | 2 +- .../droppable/droppable.test.ts | 4 +- .../operations/definitions/cardinality.tsx | 69 +++++++++++++++-- .../operations/definitions/column_types.ts | 14 ++-- .../operations/definitions/count.tsx | 76 ++++++++++++++++--- .../operations/definitions/formula/parse.ts | 13 ++-- .../operations/definitions/index.ts | 12 +++ .../operations/definitions/metrics.tsx | 72 +++++++++++++++--- .../operations/layer_helpers.test.ts | 1 + 44 files changed, 359 insertions(+), 110 deletions(-) diff --git a/src/plugins/data/common/search/aggs/buckets/multi_terms.test.ts b/src/plugins/data/common/search/aggs/buckets/multi_terms.test.ts index 7751c47575f429..f390bd860a2198 100644 --- a/src/plugins/data/common/search/aggs/buckets/multi_terms.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/multi_terms.test.ts @@ -100,6 +100,9 @@ describe('Multi Terms Agg', () => { "chain": Array [ Object { "arguments": Object { + "emptyAsNull": Array [ + false, + ], "enabled": Array [ true, ], diff --git a/src/plugins/data/common/search/aggs/buckets/terms.test.ts b/src/plugins/data/common/search/aggs/buckets/terms.test.ts index 524606f7c562f2..a2d9b94283e8bb 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms.test.ts @@ -114,6 +114,9 @@ describe('Terms Agg', () => { "chain": Array [ Object { "arguments": Object { + "emptyAsNull": Array [ + false, + ], "enabled": Array [ true, ], diff --git a/src/plugins/data/common/search/aggs/metrics/cardinality.ts b/src/plugins/data/common/search/aggs/metrics/cardinality.ts index 5a18924902fc38..f965deb6b0fe86 100644 --- a/src/plugins/data/common/search/aggs/metrics/cardinality.ts +++ b/src/plugins/data/common/search/aggs/metrics/cardinality.ts @@ -19,6 +19,7 @@ const uniqueCountTitle = i18n.translate('data.search.aggs.metrics.uniqueCountTit export interface AggParamsCardinality extends BaseAggParams { field: string; + emptyAsNull?: boolean; } export const getCardinalityMetricAgg = () => @@ -27,6 +28,7 @@ export const getCardinalityMetricAgg = () => valueType: 'number', expressionName: aggCardinalityFnName, title: uniqueCountTitle, + enableEmptyAsNull: true, makeLabel(aggConfig: IMetricAggConfig) { return i18n.translate('data.search.aggs.metrics.uniqueCountLabel', { defaultMessage: 'Unique count of {field}', diff --git a/src/plugins/data/common/search/aggs/metrics/cardinality_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/cardinality_fn.test.ts index 08d64e599d8a9e..2d1f8a7baa23b3 100644 --- a/src/plugins/data/common/search/aggs/metrics/cardinality_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/cardinality_fn.test.ts @@ -25,6 +25,7 @@ describe('agg_expression_functions', () => { "id": undefined, "params": Object { "customLabel": undefined, + "emptyAsNull": undefined, "field": "machine.os.keyword", "json": undefined, "timeShift": undefined, diff --git a/src/plugins/data/common/search/aggs/metrics/cardinality_fn.ts b/src/plugins/data/common/search/aggs/metrics/cardinality_fn.ts index 89006761407f74..cdff364f7c45e6 100644 --- a/src/plugins/data/common/search/aggs/metrics/cardinality_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/cardinality_fn.ts @@ -74,6 +74,13 @@ export const aggCardinality = (): FunctionDefinition => ({ 'Shift the time range for the metric by a set time, for example 1h or 7d. "previous" will use the closest time range from the date histogram or time range filter.', }), }, + emptyAsNull: { + types: ['boolean'], + help: i18n.translate('data.search.aggs.metrics.emptyAsNull.help', { + defaultMessage: + 'If set to true, a missing value is treated as null in the resulting data table. If set to false, a "zero" is filled in', + }), + }, }, fn: (input, args) => { const { id, enabled, schema, ...rest } = args; diff --git a/src/plugins/data/common/search/aggs/metrics/count.ts b/src/plugins/data/common/search/aggs/metrics/count.ts index fac1751290f70d..be3c6b7cdfb53f 100644 --- a/src/plugins/data/common/search/aggs/metrics/count.ts +++ b/src/plugins/data/common/search/aggs/metrics/count.ts @@ -7,10 +7,15 @@ */ import { i18n } from '@kbn/i18n'; +import { BaseAggParams } from '../types'; import { aggCountFnName } from './count_fn'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; +export interface AggParamsCount extends BaseAggParams { + emptyAsNull?: boolean; +} + export const getCountMetricAgg = () => new MetricAggType({ name: METRIC_TYPES.COUNT, @@ -20,6 +25,7 @@ export const getCountMetricAgg = () => }), hasNoDsl: true, json: false, + enableEmptyAsNull: true, makeLabel() { return i18n.translate('data.search.aggs.metrics.countLabel', { defaultMessage: 'Count', @@ -32,11 +38,16 @@ export const getCountMetricAgg = () => }, getValue(agg, bucket) { const timeShift = agg.getTimeShift(); + let value: unknown; if (!timeShift) { - return bucket.doc_count; + value = bucket.doc_count; } else { - return bucket[`doc_count_${timeShift.asMilliseconds()}`]; + value = bucket[`doc_count_${timeShift.asMilliseconds()}`]; + } + if (value === 0 && agg.params.emptyAsNull) { + return null; } + return value; }, isScalable() { return true; diff --git a/src/plugins/data/common/search/aggs/metrics/count_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/count_fn.test.ts index c6736c5b69f7d4..7a68b7a962373b 100644 --- a/src/plugins/data/common/search/aggs/metrics/count_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/count_fn.test.ts @@ -23,6 +23,7 @@ describe('agg_expression_functions', () => { "id": undefined, "params": Object { "customLabel": undefined, + "emptyAsNull": undefined, "timeShift": undefined, }, "schema": undefined, diff --git a/src/plugins/data/common/search/aggs/metrics/count_fn.ts b/src/plugins/data/common/search/aggs/metrics/count_fn.ts index a3a4bcc16a3913..c302e0abf7c5dc 100644 --- a/src/plugins/data/common/search/aggs/metrics/count_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/count_fn.ts @@ -61,6 +61,13 @@ export const aggCount = (): FunctionDefinition => ({ 'Shift the time range for the metric by a set time, for example 1h or 7d. "previous" will use the closest time range from the date histogram or time range filter.', }), }, + emptyAsNull: { + types: ['boolean'], + help: i18n.translate('data.search.aggs.metrics.emptyAsNull.help', { + defaultMessage: + 'If set to true, a missing value is treated as null in the resulting data table. If set to false, a "zero" is filled in', + }), + }, }, fn: (input, args) => { const { id, enabled, schema, ...rest } = args; diff --git a/src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts b/src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts index 5237c1ecffe584..c96ba217779a6f 100644 --- a/src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts +++ b/src/plugins/data/common/search/aggs/metrics/metric_agg_type.ts @@ -31,6 +31,7 @@ interface MetricAggTypeConfig extends AggTypeConfig> { isScalable?: () => boolean; subtype?: string; + enableEmptyAsNull?: boolean; } // TODO need to make a more explicit interface for this @@ -57,6 +58,17 @@ export class MetricAggType ); + if (config.enableEmptyAsNull) { + this.params.push( + new BaseParamType({ + name: 'emptyAsNull', + type: 'boolean', + default: false, + write: () => {}, + }) as MetricAggParam + ); + } + this.getValue = config.getValue || ((agg, bucket) => { @@ -67,9 +79,13 @@ export class MetricAggType { @@ -27,6 +28,7 @@ export const getSumMetricAgg = () => { expressionName: aggSumFnName, title: sumTitle, valueType: 'number', + enableEmptyAsNull: true, makeLabel(aggConfig) { return i18n.translate('data.search.aggs.metrics.sumLabel', { defaultMessage: 'Sum of {field}', diff --git a/src/plugins/data/common/search/aggs/metrics/sum_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/sum_fn.test.ts index f4d4fb5451dcda..3f36f98f40eac8 100644 --- a/src/plugins/data/common/search/aggs/metrics/sum_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/sum_fn.test.ts @@ -25,6 +25,7 @@ describe('agg_expression_functions', () => { "id": undefined, "params": Object { "customLabel": undefined, + "emptyAsNull": undefined, "field": "machine.os.keyword", "json": undefined, "timeShift": undefined, diff --git a/src/plugins/data/common/search/aggs/metrics/sum_fn.ts b/src/plugins/data/common/search/aggs/metrics/sum_fn.ts index d8e03d28bb12a7..063cc10839813c 100644 --- a/src/plugins/data/common/search/aggs/metrics/sum_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/sum_fn.ts @@ -69,6 +69,13 @@ export const aggSum = (): FunctionDefinition => ({ 'Shift the time range for the metric by a set time, for example 1h or 7d. "previous" will use the closest time range from the date histogram or time range filter.', }), }, + emptyAsNull: { + types: ['boolean'], + help: i18n.translate('data.search.aggs.metrics.emptyAsNull.help', { + defaultMessage: + 'If set to true, a missing value is treated as null in the resulting data table. If set to false, a "zero" is filled in', + }), + }, }, fn: (input, args) => { const { id, enabled, schema, ...rest } = args; diff --git a/src/plugins/data/common/search/aggs/types.ts b/src/plugins/data/common/search/aggs/types.ts index edc328bcb5099a..3781b35e5b767b 100644 --- a/src/plugins/data/common/search/aggs/types.ts +++ b/src/plugins/data/common/search/aggs/types.ts @@ -95,6 +95,7 @@ import { AggParamsDiversifiedSampler } from './buckets/diversified_sampler'; import { AggParamsSignificantText } from './buckets/significant_text'; import { AggParamsTopMetrics } from './metrics/top_metrics'; import { aggTopMetrics } from './metrics/top_metrics_fn'; +import { AggParamsCount } from './metrics'; export type { IAggConfig, AggConfigSerialized } from './agg_config'; export type { CreateAggConfigParams, IAggConfigs } from './agg_configs'; @@ -168,7 +169,7 @@ export interface AggParamsMapping { [BUCKET_TYPES.DIVERSIFIED_SAMPLER]: AggParamsDiversifiedSampler; [METRIC_TYPES.AVG]: AggParamsAvg; [METRIC_TYPES.CARDINALITY]: AggParamsCardinality; - [METRIC_TYPES.COUNT]: BaseAggParams; + [METRIC_TYPES.COUNT]: AggParamsCount; [METRIC_TYPES.GEO_BOUNDS]: AggParamsGeoBounds; [METRIC_TYPES.GEO_CENTROID]: AggParamsGeoCentroid; [METRIC_TYPES.MAX]: AggParamsMax; diff --git a/src/plugins/data/common/search/tabify/response_writer.test.ts b/src/plugins/data/common/search/tabify/response_writer.test.ts index ec131458b8510e..85f815447619a2 100644 --- a/src/plugins/data/common/search/tabify/response_writer.test.ts +++ b/src/plugins/data/common/search/tabify/response_writer.test.ts @@ -201,6 +201,7 @@ describe('TabbedAggResponseWriter class', () => { indexPatternId: '1234', params: { field: 'machine.os.raw', + emptyAsNull: false, }, type: 'cardinality', }, @@ -264,6 +265,7 @@ describe('TabbedAggResponseWriter class', () => { indexPatternId: '1234', params: { field: 'machine.os.raw', + emptyAsNull: false, }, type: 'cardinality', }, diff --git a/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts b/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts index 9f3a25e15d7410..17423fad1ae9e2 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts @@ -76,7 +76,9 @@ describe('test fetchCharts', () => { Object { "enabled": true, "id": "1", - "params": Object {}, + "params": Object { + "emptyAsNull": false, + }, "schema": "metric", "type": "count", }, diff --git a/src/plugins/discover/public/application/main/utils/get_chart_agg_config.test.ts b/src/plugins/discover/public/application/main/utils/get_chart_agg_config.test.ts index ccd7584d4bff36..ded1cf1d858aff 100644 --- a/src/plugins/discover/public/application/main/utils/get_chart_agg_config.test.ts +++ b/src/plugins/discover/public/application/main/utils/get_chart_agg_config.test.ts @@ -33,7 +33,9 @@ describe('getChartAggConfigs', () => { Object { "enabled": true, "id": "1", - "params": Object {}, + "params": Object { + "emptyAsNull": false, + }, "schema": "metric", "type": "count", }, diff --git a/test/interpreter_functional/snapshots/baseline/combined_test2.json b/test/interpreter_functional/snapshots/baseline/combined_test2.json index 3b030ec8fb5971..b4129ac898eed9 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test2.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test2.json @@ -1 +1 @@ -{"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"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"} \ No newline at end of file +{"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/combined_test3.json b/test/interpreter_functional/snapshots/baseline/combined_test3.json index dc39ecfc535940..dc1c037f45e950 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test3.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test3.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"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"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/final_output_test.json b/test/interpreter_functional/snapshots/baseline/final_output_test.json index dc39ecfc535940..dc1c037f45e950 100644 --- a/test/interpreter_functional/snapshots/baseline/final_output_test.json +++ b/test/interpreter_functional/snapshots/baseline/final_output_test.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"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"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_all_data.json b/test/interpreter_functional/snapshots/baseline/metric_all_data.json index a26b85daee9324..939e51b619928c 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_all_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"bytes","params":null},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"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":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"bytes","params":null},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"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 diff --git a/test/interpreter_functional/snapshots/baseline/metric_empty_data.json b/test/interpreter_functional/snapshots/baseline/metric_empty_data.json index 0917ecc8f9b2ed..6adb4e117d2c7d 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_empty_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_empty_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"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 +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"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_multi_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json index 44bc7717db04f3..4a324a133c057e 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"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":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"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 diff --git a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json index 99604aa3774753..944820d0ed16d0 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":{"colors":["rgb(0,0,0,0)","rgb(100, 100, 100)"],"continuity":"none","gradient":false,"range":"number","rangeMax":10000,"rangeMin":0,"stops":[0,10000]},"percentageMode":true,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"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":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":{"colors":["rgb(0,0,0,0)","rgb(100, 100, 100)"],"continuity":"none","gradient":false,"range":"number","rangeMax":10000,"rangeMin":0,"stops":[0,10000]},"percentageMode":true,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"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 diff --git a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json index 63c91a3cc749df..392649d410e158 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"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":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"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 diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_1.json b/test/interpreter_functional/snapshots/baseline/partial_test_1.json index e8a847b43de3b4..8ce0ee16a0b3b2 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_1.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_1.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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"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":{"ariaLabel":null,"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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":{"ariaLabel":null,"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_2.json b/test/interpreter_functional/snapshots/baseline/partial_test_2.json index dc39ecfc535940..dc1c037f45e950 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_2.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_2.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"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"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test2.json b/test/interpreter_functional/snapshots/baseline/step_output_test2.json index 3b030ec8fb5971..b4129ac898eed9 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test2.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test2.json @@ -1 +1 @@ -{"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"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"} \ No newline at end of file +{"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test3.json b/test/interpreter_functional/snapshots/baseline/step_output_test3.json index dc39ecfc535940..dc1c037f45e950 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test3.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test3.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"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"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json index 518eb529e70f40..837251a438911a 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_all_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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"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":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json index 7417545550cd82..5c3ca14f4eab73 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[],"type":"datatable"},"visParams":{"ariaLabel":null,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[],"type":"datatable"},"visParams":{"ariaLabel":null,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json index 986e6d19e91f33..5e99024d6e52bc 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"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":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json index cf0aa1162c23f4..e00233197bda38 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"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":{"ariaLabel":null,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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":{"ariaLabel":null,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json index 357ac0fc76784c..759b2752f93289 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_options.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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"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":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/advanced_options.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/advanced_options.tsx index db3dfe8901fb98..3d1928edf27dcb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/advanced_options.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/advanced_options.tsx @@ -8,15 +8,7 @@ import { EuiLink, EuiText, EuiPopover, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; - -interface AdvancedOption { - title: string; - dataTestSubj: string; - onClick: () => void; - showInPopover: boolean; - inlineElement: React.ReactElement | null; - helpPopup?: string | null; -} +import { AdvancedOption } from '../operations/definitions'; export function AdvancedOptions(props: { options: AdvancedOption[] }) { const [popoverOpen, setPopoverOpen] = useState(false); @@ -49,20 +41,24 @@ export function AdvancedOptions(props: { options: AdvancedOption[] }) { setPopoverOpen(false); }} > - {popoverOptions.map(({ dataTestSubj, onClick, title }, index) => ( + {popoverOptions.map(({ dataTestSubj, onClick, title, optionElement }, index) => ( - - { - setPopoverOpen(false); - onClick(); - }} - > - {title} - - + {optionElement ? ( + optionElement + ) : ( + + { + setPopoverOpen(false); + onClick(); + }} + > + {title} + + + )} {popoverOptions.length - 1 !== index && } ))} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 24f3a5a65c8d9a..a9e37e2d53d704 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -57,6 +57,7 @@ import { import type { TemporaryState } from './dimensions_editor_helpers'; import { FieldInput } from './field_input'; import { NameInput } from '../../shared_components'; +import { ParamEditorProps } from '../operations/definitions'; const operationPanels = getOperationDisplay(); @@ -422,6 +423,28 @@ export function DimensionEditor(props: DimensionEditorProps) { const FieldInputComponent = selectedOperationDefinition?.renderFieldInput || FieldInput; + const paramEditorProps: ParamEditorProps = { + layer: state.layers[layerId], + layerId, + activeData: props.activeData, + updateLayer: (setter) => { + if (temporaryQuickFunction) { + setTemporaryState('none'); + } + setStateWrapper(setter, { forceRender: temporaryQuickFunction }); + }, + columnId, + currentColumn: state.layers[layerId].columns[columnId], + dateRange, + indexPattern: currentIndexPattern, + operationDefinitionMap, + toggleFullscreen, + isFullscreen, + setIsCloseable, + paramEditorCustomProps, + ...services, + }; + const quickFunctions = ( <>
@@ -517,29 +540,7 @@ export function DimensionEditor(props: DimensionEditorProps) { /> ) : null} - {shouldDisplayExtraOptions && ( - { - if (temporaryQuickFunction) { - setTemporaryState('none'); - } - setStateWrapper(setter, { forceRender: temporaryQuickFunction }); - }} - columnId={columnId} - currentColumn={state.layers[layerId].columns[columnId]} - dateRange={dateRange} - indexPattern={currentIndexPattern} - operationDefinitionMap={operationDefinitionMap} - toggleFullscreen={toggleFullscreen} - isFullscreen={isFullscreen} - setIsCloseable={setIsCloseable} - paramEditorCustomProps={paramEditorCustomProps} - {...services} - /> - )} + {shouldDisplayExtraOptions && }
); @@ -767,6 +768,9 @@ export function DimensionEditor(props: DimensionEditorProps) { /> ) : null, }, + ...(operationDefinitionMap[selectedColumn.operationType].getAdvancedOptions?.( + paramEditorProps + ) || []), ]} /> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 70baa1d772e1ba..356ad5ac9543e4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -602,7 +602,7 @@ describe('IndexPatternDimensionEditorPanel', () => { col1: expect.objectContaining({ operationType: 'min', sourceField: 'bytes', - params: { format: { id: 'bytes' } }, + params: { format: { id: 'bytes' }, emptyAsNull: true }, // Other parts of this don't matter for this test }), }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts index 2b193eb01c9d61..ea3978ce8ca94d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts @@ -2306,7 +2306,9 @@ describe('IndexPatternDimensionEditorPanel', () => { sourceField: 'src', timeShift: undefined, dataType: 'number', - params: undefined, + params: { + emptyAsNull: true, + }, scale: 'ratio', }, }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx index c97447803524dd..8490b48ad320ef 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx @@ -6,10 +6,13 @@ */ import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { EuiSwitch } from '@elastic/eui'; +import { euiThemeVars } from '@kbn/ui-theme'; import { AggFunctionsMapping } from '../../../../../../../src/plugins/data/public'; import { buildExpressionFunction } from '../../../../../../../src/plugins/expressions/public'; -import { OperationDefinition } from './index'; -import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types'; +import { OperationDefinition, ParamEditorProps } from './index'; +import { FieldBasedIndexPatternColumn, FormatParams } from './column_types'; import { getFormatFromPreviousColumn, @@ -17,9 +20,11 @@ import { getSafeName, getFilter, combineErrorMessages, + isColumnOfType, } from './helpers'; import { adjustTimeScaleLabelSuffix } from '../time_scale_utils'; import { getDisallowedPreviousShiftMessage } from '../../time_shift_utils'; +import { updateColumnParam } from '../layer_helpers'; const supportedTypes = new Set([ 'string', @@ -51,10 +56,12 @@ function ofName(name: string, timeShift: string | undefined) { ); } -export interface CardinalityIndexPatternColumn - extends FormattedIndexPatternColumn, - FieldBasedIndexPatternColumn { +export interface CardinalityIndexPatternColumn extends FieldBasedIndexPatternColumn { operationType: typeof OPERATION_TYPE; + params?: { + emptyAsNull?: boolean; + format?: FormatParams; + }; } export const cardinalityOperation: OperationDefinition = { @@ -101,9 +108,58 @@ export const cardinalityOperation: OperationDefinition('unique_count', previousColumn) + ? previousColumn.params?.emptyAsNull + : !columnParams?.usedInMath, + }, }; }, + getAdvancedOptions: ({ + layer, + columnId, + currentColumn, + updateLayer, + }: ParamEditorProps) => { + return [ + { + dataTestSubj: 'hide-zero-values', + optionElement: ( + <> + { + updateLayer( + updateColumnParam({ + layer, + columnId, + paramName: 'emptyAsNull', + value: !currentColumn.params?.emptyAsNull, + }) + ); + }} + compressed + /> + + ), + title: '', + showInPopover: true, + inlineElement: null, + onClick: () => {}, + }, + ]; + }, toEsAggsFn: (column, columnId) => { return buildExpressionFunction('aggCardinality', { id: columnId, @@ -112,6 +168,7 @@ export const cardinalityOperation: OperationDefinition { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts index 2b11d182eeed06..333312116949fa 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts @@ -19,15 +19,17 @@ export interface BaseIndexPatternColumn extends Operation { timeShift?: string; } +export interface FormatParams { + id: string; + params?: { + decimals: number; + }; +} + // Formatting can optionally be added to any column export interface FormattedIndexPatternColumn extends BaseIndexPatternColumn { params?: { - format?: { - id: string; - params?: { - decimals: number; - }; - }; + format?: FormatParams; }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx index a35f8fbc08acfc..7ecd5a4970c95b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx @@ -6,31 +6,39 @@ */ import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { EuiSwitch } from '@elastic/eui'; import { AggFunctionsMapping } from '../../../../../../../src/plugins/data/public'; import { buildExpressionFunction } from '../../../../../../../src/plugins/expressions/public'; -import { OperationDefinition } from './index'; -import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types'; +import { OperationDefinition, ParamEditorProps } from './index'; +import { FieldBasedIndexPatternColumn, FormatParams } from './column_types'; import { IndexPatternField } from '../../types'; import { getInvalidFieldMessage, getFilter, - isColumnFormatted, combineErrorMessages, + getFormatFromPreviousColumn, + isColumnOfType, } from './helpers'; import { adjustTimeScaleLabelSuffix, adjustTimeScaleOnOtherColumnChange, } from '../time_scale_utils'; import { getDisallowedPreviousShiftMessage } from '../../time_shift_utils'; +import { updateColumnParam } from '../layer_helpers'; const countLabel = i18n.translate('xpack.lens.indexPattern.countOf', { defaultMessage: 'Count of records', }); -export type CountIndexPatternColumn = FormattedIndexPatternColumn & - FieldBasedIndexPatternColumn & { - operationType: 'count'; +export type CountIndexPatternColumn = FieldBasedIndexPatternColumn & { + operationType: 'count'; + params?: { + emptyAsNull?: boolean; + format?: FormatParams; }; +}; export const countOperation: OperationDefinition = { type: 'count', @@ -91,14 +99,57 @@ export const countOperation: OperationDefinition('count', previousColumn) + ? previousColumn.params?.emptyAsNull + : !columnParams?.usedInMath, + }, }; }, + getAdvancedOptions: ({ + layer, + columnId, + currentColumn, + updateLayer, + }: ParamEditorProps) => { + return [ + { + dataTestSubj: 'hide-zero-values', + optionElement: ( + <> + { + updateLayer( + updateColumnParam({ + layer, + columnId, + paramName: 'emptyAsNull', + value: !currentColumn.params?.emptyAsNull, + }) + ); + }} + compressed + /> + + ), + title: '', + showInPopover: true, + inlineElement: null, + onClick: () => {}, + }, + ]; + }, onOtherColumnChanged: (layer, thisColumnId, changedColumnId) => adjustTimeScaleOnOtherColumnChange( layer, @@ -112,6 +163,7 @@ export const countOperation: OperationDefinition { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts index a3b61429fb0bf4..81a6943e600d05 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts @@ -107,11 +107,14 @@ function extractColumns( ? indexPattern.getFieldByName(fieldName.value)! : documentField; - const mappedParams = mergeWithGlobalFilter( - nodeOperation, - getOperationParams(nodeOperation, namedArguments || []), - globalFilter - ); + const mappedParams = { + ...mergeWithGlobalFilter( + nodeOperation, + getOperationParams(nodeOperation, namedArguments || []), + globalFilter + ), + usedInMath: true, + }; const newCol = ( nodeOperation as OperationDefinition diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index dec70130d12823..87c7ab5913a20d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -191,6 +191,16 @@ export interface HelpProps { export type TimeScalingMode = 'disabled' | 'mandatory' | 'optional'; +export interface AdvancedOption { + title: string; + optionElement?: React.ReactElement; + dataTestSubj: string; + onClick: () => void; + showInPopover: boolean; + inlineElement: React.ReactElement | null; + helpPopup?: string | null; +} + interface BaseOperationDefinitionProps { type: C['operationType']; /** @@ -227,6 +237,7 @@ interface BaseOperationDefinitionProps * React component for operation specific settings shown in the flyout editor */ paramEditor?: React.ComponentType>; + getAdvancedOptions?: (params: ParamEditorProps) => AdvancedOption[] | undefined; /** * Returns true if the `column` can also be used on `newIndexPattern`. * If this function returns false, the column is removed when switching index pattern @@ -416,6 +427,7 @@ interface FieldBasedOperationDefinition C; /** diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx index f2ab811427ac57..2b46e52defdbab 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx @@ -6,30 +6,34 @@ */ import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { EuiSwitch } from '@elastic/eui'; +import { euiThemeVars } from '@kbn/ui-theme'; import { buildExpressionFunction } from '../../../../../../../src/plugins/expressions/public'; -import { OperationDefinition } from './index'; +import { OperationDefinition, ParamEditorProps } from './index'; import { getFormatFromPreviousColumn, getInvalidFieldMessage, getSafeName, getFilter, combineErrorMessages, + isColumnOfType, } from './helpers'; -import { - FormattedIndexPatternColumn, - FieldBasedIndexPatternColumn, - BaseIndexPatternColumn, -} from './column_types'; +import { FieldBasedIndexPatternColumn, BaseIndexPatternColumn, FormatParams } from './column_types'; import { adjustTimeScaleLabelSuffix, adjustTimeScaleOnOtherColumnChange, } from '../time_scale_utils'; import { getDisallowedPreviousShiftMessage } from '../../time_shift_utils'; +import { updateColumnParam } from '../layer_helpers'; -type MetricColumn = FormattedIndexPatternColumn & - FieldBasedIndexPatternColumn & { - operationType: T; +type MetricColumn = FieldBasedIndexPatternColumn & { + operationType: T; + params?: { + emptyAsNull?: boolean; + format?: FormatParams; }; +}; const typeToFn: Record = { min: 'aggMin', @@ -49,6 +53,7 @@ function buildMetricOperation>({ priority, optionalTimeScaling, supportsDate, + hideZeroOption, }: { type: T['operationType']; displayName: string; @@ -57,6 +62,7 @@ function buildMetricOperation>({ optionalTimeScaling?: boolean; description?: string; supportsDate?: boolean; + hideZeroOption?: boolean; }) { const labelLookup = (name: string, column?: BaseIndexPatternColumn) => { const label = ofName(name); @@ -115,7 +121,13 @@ function buildMetricOperation>({ timeScale: optionalTimeScaling ? previousColumn?.timeScale : undefined, filter: getFilter(previousColumn, columnParams), timeShift: columnParams?.shift || previousColumn?.timeShift, - params: getFormatFromPreviousColumn(previousColumn), + params: { + ...getFormatFromPreviousColumn(previousColumn), + emptyAsNull: + hideZeroOption && previousColumn && isColumnOfType(type, previousColumn) + ? previousColumn.params?.emptyAsNull + : !columnParams?.usedInMath, + }, } as T; }, onFieldChange: (oldColumn, field) => { @@ -126,6 +138,44 @@ function buildMetricOperation>({ sourceField: field.name, }; }, + getAdvancedOptions: ({ layer, columnId, currentColumn, updateLayer }: ParamEditorProps) => { + if (!hideZeroOption) return []; + return [ + { + dataTestSubj: 'hide-zero-values', + optionElement: ( + <> + { + updateLayer( + updateColumnParam({ + layer, + columnId, + paramName: 'emptyAsNull', + value: !currentColumn.params?.emptyAsNull, + }) + ); + }} + compressed + /> + + ), + title: '', + showInPopover: true, + inlineElement: null, + onClick: () => {}, + }, + ]; + }, toEsAggsFn: (column, columnId, _indexPattern) => { return buildExpressionFunction(typeToFn[type], { id: columnId, @@ -134,6 +184,7 @@ function buildMetricOperation>({ field: column.sourceField, // time shift is added to wrapping aggFilteredMetric if filter is set timeShift: column.filter ? undefined : column.timeShift, + emptyAsNull: hideZeroOption ? column.params?.emptyAsNull : undefined, }).toAst(); }, getErrorMessage: (layer, columnId, indexPattern) => @@ -242,6 +293,7 @@ export const sumOperation = buildMetricOperation({ defaultMessage: 'A single-value metrics aggregation that sums up numeric values that are extracted from the aggregated documents.', }), + hideZeroOption: true, }); export const medianOperation = buildMetricOperation({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index f7a8df3d5ef1f3..b6398970056e28 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -2162,6 +2162,7 @@ describe('state_helpers', () => { id: 'number', params: { decimals: 2 }, }, + emptyAsNull: true, }, }) ); From 5d519f3e72af628abdfa1d3d5d51712f1943d871 Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Tue, 22 Mar 2022 19:13:33 +0100 Subject: [PATCH 13/64] [Workplace Search] Add feedback link to pages with external source (#128290) --- .../components/add_source/add_source.test.tsx | 16 +++++++++ .../components/add_source/add_source.tsx | 1 + .../add_source/config_completed.test.tsx | 7 ++++ .../add_source/config_completed.tsx | 29 +++++++++++++++ .../components/overview.test.tsx | 18 +++++++++- .../content_sources/components/overview.tsx | 30 ++++++++++++++++ .../components/source_config.test.tsx | 10 +++++- .../settings/components/source_config.tsx | 35 ++++++++++++++++++- 8 files changed, 143 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx index 8e3171dc71bec7..d4b5a1dbd98297 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx @@ -134,12 +134,28 @@ describe('AddSourceList', () => { addSourceCurrentStep: AddSourceSteps.ConfigCompletedStep, }); const wrapper = shallow(); + expect(wrapper.find(ConfigCompleted).prop('showFeedbackLink')).toEqual(false); wrapper.find(ConfigCompleted).prop('advanceStep')(); expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/confluence_cloud/connect'); expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.ConnectInstanceStep); }); + it('renders Config Completed step with feedback for external connectors', () => { + setMockValues({ + ...mockValues, + sourceConfigData: { ...sourceConfigData, serviceType: 'external' }, + addSourceCurrentStep: AddSourceSteps.ConfigCompletedStep, + }); + const wrapper = shallow( + + ); + expect(wrapper.find(ConfigCompleted).prop('showFeedbackLink')).toEqual(true); + + expect(navigateToUrl).toHaveBeenCalledWith('/sources/add/confluence_cloud/connect'); + expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.ConnectInstanceStep); + }); + it('renders Save Config step', () => { setMockValues({ ...mockValues, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx index d2c665a4acd74e..4bdf8db217a7b3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx @@ -109,6 +109,7 @@ export const AddSource: React.FC = (props) => { advanceStep={goToConnectInstance} privateSourcesEnabled={privateSourcesEnabled} header={header} + showFeedbackLink={serviceType === 'external'} /> )} {addSourceCurrentStep === AddSourceSteps.ConnectInstanceStep && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.test.tsx index 163da5297e3708..0980bd8a61cd62 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.test.tsx @@ -9,6 +9,8 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { EuiCallOut } from '@elastic/eui'; + import { ConfigCompleted } from './config_completed'; describe('ConfigCompleted', () => { @@ -26,6 +28,7 @@ describe('ConfigCompleted', () => { expect(wrapper.find('[data-test-subj="OrgCanConnectMessage"]')).toHaveLength(1); expect(wrapper.find('[data-test-subj="PersonalConnectLinkMessage"]')).toHaveLength(0); + expect(wrapper.find(EuiCallOut)).toHaveLength(0); }); it('renders account context', () => { @@ -45,4 +48,8 @@ describe('ConfigCompleted', () => { expect(wrapper.find('[data-test-subj="PrivateDisabledMessage"]')).toHaveLength(1); }); + it('renders feedback callout when set', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx index 9b34053bfe524e..edd39409893a67 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { EuiButton, + EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiIcon, @@ -37,6 +38,7 @@ interface ConfigCompletedProps { accountContextOnly?: boolean; privateSourcesEnabled: boolean; advanceStep(): void; + showFeedbackLink?: boolean; } export const ConfigCompleted: React.FC = ({ @@ -45,6 +47,7 @@ export const ConfigCompleted: React.FC = ({ accountContextOnly, header, privateSourcesEnabled, + showFeedbackLink, }) => ( <> {header} @@ -166,5 +169,31 @@ export const ConfigCompleted: React.FC = ({ )}
+ {showFeedbackLink && ( + <> + + + + + {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.addSource.configCompleted.feedbackCallOutText', + { + defaultMessage: + 'Have feedback about deploying a {name} Connector Package? Let us know.', + values: { name }, + } + )} + + } + /> + + + + )} ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx index c9eb2e0afdf5eb..21a71308a18328 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx @@ -12,7 +12,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiConfirmModal, EuiEmptyPrompt, EuiPanel, EuiTable } from '@elastic/eui'; +import { EuiCallOut, EuiConfirmModal, EuiEmptyPrompt, EuiPanel, EuiTable } from '@elastic/eui'; import { ComponentLoader } from '../../../components/shared/component_loader'; @@ -121,6 +121,22 @@ describe('Overview', () => { expect(wrapper.find('[data-test-subj="DocumentPermissionsDisabled"]')).toHaveLength(1); }); + it('renders feedback callout for external sources', () => { + setMockValues({ + ...mockValues, + contentSource: { + ...fullContentSources[1], + serviceTypeSupportsPermissions: true, + custom: false, + serviceType: 'external', + }, + }); + + const wrapper = shallow(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + }); + it('handles confirmModal submission', () => { const wrapper = shallow(); const button = wrapper.find('[data-test-subj="SyncButton"]'); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx index e5c4b3a09f93f3..8f287537e41090 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx @@ -29,7 +29,9 @@ import { EuiText, EuiTextColor, EuiTitle, + EuiCallOut, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { CANCEL_BUTTON_LABEL, START_BUTTON_LABEL } from '../../../../shared/constants'; @@ -107,6 +109,8 @@ export const Overview: React.FC = () => { hasPermissions, isFederatedSource, isIndexedSource, + serviceType, + name, } = contentSource; const [isSyncing, setIsSyncing] = useState(false); @@ -582,6 +586,32 @@ export const Overview: React.FC = () => {
+ {serviceType === 'external' && ( + <> + + + + + {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.feedbackCallOutText', + { + defaultMessage: + 'Have feedback about deploying a {name} Connector Package? Let us know.', + values: { name }, + } + )} + + } + /> + + + + )} ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx index af8b8fe461f162..8399df946ea83c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx @@ -14,7 +14,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiConfirmModal } from '@elastic/eui'; +import { EuiCallOut, EuiConfirmModal } from '@elastic/eui'; import { SaveConfig } from '../../content_sources/components/add_source/save_config'; @@ -40,6 +40,7 @@ describe('SourceConfig', () => { saveConfig.prop('onDeleteConfig')!(); expect(wrapper.find(EuiConfirmModal)).toHaveLength(1); + expect(wrapper.find(EuiCallOut)).toHaveLength(0); }); it('renders a breadcrumb fallback while data is loading', () => { @@ -84,4 +85,11 @@ describe('SourceConfig', () => { expect(wrapper.find(EuiConfirmModal)).toHaveLength(0); }); + + it('shows feedback link for external sources', () => { + const wrapper = shallow( + + ); + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx index ea63f3bab77d98..6973732fa6727b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx @@ -9,7 +9,14 @@ import React, { useEffect, useState } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiConfirmModal } from '@elastic/eui'; +import { + EuiCallOut, + EuiConfirmModal, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiSpacer, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { WorkplaceSearchPageTemplate } from '../../../components/layout'; @@ -77,6 +84,32 @@ export const SourceConfig: React.FC = ({ sourceData }) => { )} )} + {serviceType === 'external' && ( + <> + + + + + {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.feedbackCallOutText', + { + defaultMessage: + 'Have feedback about deploying a {name} Connector Package? Let us know.', + values: { name }, + } + )} + + } + /> + + + + )} ); }; From 0dc168e086f09ae1605ddab0a3d4b9e6f5366d47 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Tue, 22 Mar 2022 19:49:08 +0100 Subject: [PATCH 14/64] [Cases] Initial functional tests for cases in the stack management page (#127858) * Adding data-test-subj to cases header component * adding casesApp service for functional tests * Adding test for create case * Add more tests * Add tests for cases list * Update tests file structure * Improve test structure * Add cleanup methods * Remove empty functions * Use api to create case to edit * move some repeated code to a service * Unify casesapp provider in a single namespace * Apply PR comment suggestions * Remove .only from test suite * Fix broken unit test * Attempt to fix flaky test * Another attempt to fix flaky test * Move checks up for flaky tests * increase timeout for flaky test * Try to fix flaky test * MOre fixes for flaky test * rename cases app and fix nitpicks * Rename variables * fix more nits * add more create case validatioons * add more create and edit case validations * Add extra validations to edit case * Fix typo * try to fix flaky test --- .../public/components/all_cases/table.tsx | 2 +- .../public/components/header_page/index.tsx | 4 +- .../utility_bar/utility_bar_action.tsx | 4 +- x-pack/test/functional/config.js | 3 + x-pack/test/functional/services/cases/api.ts | 45 +++++ .../test/functional/services/cases/common.ts | 160 +++++++++++++++++ .../test/functional/services/cases/helpers.ts | 26 +++ .../test/functional/services/cases/index.ts | 17 ++ x-pack/test/functional/services/index.ts | 2 + .../apps/cases/create_case_form.ts | 51 ++++++ .../apps/cases/edit_case_form.ts | 169 ++++++++++++++++++ .../apps/cases/index.ts | 17 ++ .../apps/cases/list_view.ts | 122 +++++++++++++ x-pack/test/functional_with_es_ssl/config.ts | 1 + 14 files changed, 619 insertions(+), 4 deletions(-) create mode 100644 x-pack/test/functional/services/cases/api.ts create mode 100644 x-pack/test/functional/services/cases/common.ts create mode 100644 x-pack/test/functional/services/cases/helpers.ts create mode 100644 x-pack/test/functional/services/cases/index.ts create mode 100644 x-pack/test/functional_with_es_ssl/apps/cases/create_case_form.ts create mode 100644 x-pack/test/functional_with_es_ssl/apps/cases/edit_case_form.ts create mode 100644 x-pack/test/functional_with_es_ssl/apps/cases/index.ts create mode 100644 x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts diff --git a/x-pack/plugins/cases/public/components/all_cases/table.tsx b/x-pack/plugins/cases/public/components/all_cases/table.tsx index 2a2cf79e6f690a..8190acce9e784b 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table.tsx @@ -88,7 +88,7 @@ export const CasesTable: FunctionComponent = ({ ) : ( -
+
= ({ @@ -73,6 +74,7 @@ const HeaderPageComponent: React.FC = ({ subtitle2, title, titleNode, + 'data-test-subj': dataTestSubj, }) => { const { releasePhase } = useCasesContext(); const { getAllCasesUrl, navigateToAllCases } = useAllCasesNavigation(); @@ -88,7 +90,7 @@ const HeaderPageComponent: React.FC = ({ ); return ( -
+
{showBackButton && ( diff --git a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_action.tsx b/x-pack/plugins/cases/public/components/utility_bar/utility_bar_action.tsx index 19cb8ef4f613be..e5bed87021491b 100644 --- a/x-pack/plugins/cases/public/components/utility_bar/utility_bar_action.tsx +++ b/x-pack/plugins/cases/public/components/utility_bar/utility_bar_action.tsx @@ -46,15 +46,15 @@ Popover.displayName = 'Popover'; export interface UtilityBarActionProps extends LinkIconProps { popoverContent?: (closePopover: () => void) => React.ReactNode; - dataTestSubj?: string; ownFocus?: boolean; + dataTestSubj?: string; } export const UtilityBarAction = React.memo( ({ + dataTestSubj, children, color, - dataTestSubj, disabled, href, iconSide, diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 1c627dc8af6daf..28000c3d4bac85 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -119,6 +119,9 @@ export default async function ({ readConfigFile }) { logstashPipelines: { pathname: '/app/management/ingest/pipelines', }, + cases: { + pathname: '/app/management/insightsAndAlerting/cases/', + }, maps: { pathname: '/app/maps', }, diff --git a/x-pack/test/functional/services/cases/api.ts b/x-pack/test/functional/services/cases/api.ts new file mode 100644 index 00000000000000..bacb08cd19b2d3 --- /dev/null +++ b/x-pack/test/functional/services/cases/api.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 pMap from 'p-map'; +import { CasePostRequest } from '../../../../plugins/cases/common/api'; +import { createCase, deleteAllCaseItems } from '../../../cases_api_integration/common/lib/utils'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { generateRandomCaseWithoutConnector } from './helpers'; + +export function CasesAPIServiceProvider({ getService }: FtrProviderContext) { + const kbnSupertest = getService('supertest'); + const es = getService('es'); + + return { + async createCaseWithData(overwrites: { title?: string } = {}) { + const caseData = { + ...generateRandomCaseWithoutConnector(), + ...overwrites, + } as CasePostRequest; + await createCase(kbnSupertest, caseData); + }, + + async createNthRandomCases(amount: number = 3) { + const cases: CasePostRequest[] = Array.from( + { length: amount }, + () => generateRandomCaseWithoutConnector() as CasePostRequest + ); + await pMap( + cases, + (caseData) => { + return createCase(kbnSupertest, caseData); + }, + { concurrency: 4 } + ); + }, + + async deleteAllCases() { + deleteAllCaseItems(es); + }, + }; +} diff --git a/x-pack/test/functional/services/cases/common.ts b/x-pack/test/functional/services/cases/common.ts new file mode 100644 index 00000000000000..ad5fbb7be7233b --- /dev/null +++ b/x-pack/test/functional/services/cases/common.ts @@ -0,0 +1,160 @@ +/* + * 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 { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper'; +import uuid from 'uuid'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function CasesCommonServiceProvider({ getService, getPageObject }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const comboBox = getService('comboBox'); + const header = getPageObject('header'); + return { + /** + * Opens the create case page pressing the "create case" button. + * + * Doesn't do navigation. Only works if you are already inside a cases app page. + * Does not work with the cases flyout. + */ + async openCreateCasePage() { + await testSubjects.click('createNewCaseBtn'); + await testSubjects.existOrFail('create-case-submit', { + timeout: 5000, + }); + }, + + /** + * it creates a new case from the create case page + * and leaves the navigation in the case view page + * + * Doesn't do navigation. Only works if you are already inside a cases app page. + * Does not work with the cases flyout. + */ + async createCaseFromCreateCasePage({ + title = 'test-' + uuid.v4(), + description = 'desc' + uuid.v4(), + tag = 'tagme', + }: { + title: string; + description: string; + tag: string; + }) { + await this.openCreateCasePage(); + + // case name + await testSubjects.setValue('input', title); + + // case tag + await comboBox.setCustom('comboBoxInput', tag); + + // case description + const descriptionArea = await find.byCssSelector('textarea.euiMarkdownEditorTextArea'); + await descriptionArea.focus(); + await descriptionArea.type(description); + + // save + await testSubjects.click('create-case-submit'); + + await testSubjects.existOrFail('case-view-title'); + }, + + /** + * Goes to the first case listed on the table. + * + * This will fail if the table doesn't have any case + */ + async goToFirstListedCase() { + await testSubjects.existOrFail('cases-table'); + await testSubjects.click('case-details-link'); + await testSubjects.existOrFail('case-view-title'); + }, + + /** + * Marks a case in progress via the status dropdown + */ + async markCaseInProgressViaDropdown() { + await this.openCaseSetStatusDropdown(); + + await testSubjects.click('case-view-status-dropdown-in-progress'); + + // wait for backend response + await testSubjects.existOrFail('header-page-supplements > status-badge-in-progress', { + timeout: 5000, + }); + }, + + /** + * Marks a case closed via the status dropdown + */ + async markCaseClosedViaDropdown() { + this.openCaseSetStatusDropdown(); + + await testSubjects.click('case-view-status-dropdown-closed'); + + // wait for backend response + await testSubjects.existOrFail('header-page-supplements > status-badge-closed', { + timeout: 5000, + }); + }, + + /** + * Marks a case open via the status dropdown + */ + async markCaseOpenViaDropdown() { + this.openCaseSetStatusDropdown(); + + await testSubjects.click('case-view-status-dropdown-open'); + + // wait for backend response + await testSubjects.existOrFail('header-page-supplements > status-badge-open', { + timeout: 5000, + }); + }, + + async bulkDeleteAllCases() { + await testSubjects.setCheckbox('checkboxSelectAll', 'check'); + const button = await find.byCssSelector('[aria-label="Bulk actions"]'); + await button.click(); + await testSubjects.click('cases-bulk-delete-button'); + await testSubjects.click('confirmModalConfirmButton'); + }, + + async selectAndDeleteAllCases() { + await header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('cases-table', { timeout: 20 * 1000 }); + let rows: WebElementWrapper[]; + do { + await header.waitUntilLoadingHasFinished(); + await testSubjects.missingOrFail('cases-table-loading', { timeout: 5000 }); + rows = await find.allByCssSelector('[data-test-subj*="cases-table-row-"', 100); + if (rows.length > 0) { + await this.bulkDeleteAllCases(); + // wait for a second + await new Promise((r) => setTimeout(r, 1000)); + await header.waitUntilLoadingHasFinished(); + } + } while (rows.length > 0); + }, + + async validateCasesTableHasNthRows(nrRows: number) { + await header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('cases-table', { timeout: 20 * 1000 }); + await testSubjects.missingOrFail('cases-table-loading', { timeout: 5000 }); + const rows = await find.allByCssSelector('[data-test-subj*="cases-table-row-"', 100); + expect(rows.length).equal(nrRows); + }, + + async openCaseSetStatusDropdown() { + const button = await find.byCssSelector( + '[data-test-subj="case-view-status-dropdown"] button' + ); + await button.click(); + }, + }; +} diff --git a/x-pack/test/functional/services/cases/helpers.ts b/x-pack/test/functional/services/cases/helpers.ts new file mode 100644 index 00000000000000..46def1da05790d --- /dev/null +++ b/x-pack/test/functional/services/cases/helpers.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import uuid from 'uuid'; + +export function generateRandomCaseWithoutConnector() { + return { + title: 'random-' + uuid.v4(), + tags: ['test', uuid.v4()], + description: 'This is a description with id: ' + uuid.v4(), + connector: { + id: 'none', + name: 'none', + type: '.none', + fields: null, + }, + settings: { + syncAlerts: false, + }, + owner: 'cases', + }; +} diff --git a/x-pack/test/functional/services/cases/index.ts b/x-pack/test/functional/services/cases/index.ts new file mode 100644 index 00000000000000..afe244a21842ea --- /dev/null +++ b/x-pack/test/functional/services/cases/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; +import { CasesAPIServiceProvider } from './api'; +import { CasesCommonServiceProvider } from './common'; + +export function CasesServiceProvider(context: FtrProviderContext) { + return { + api: CasesAPIServiceProvider(context), + common: CasesCommonServiceProvider(context), + }; +} diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index 96a6a88f112697..62e8ab1ac464da 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -69,6 +69,7 @@ import { import { SearchSessionsService } from './search_sessions'; import { ObservabilityProvider } from './observability'; import { CompareImagesProvider } from './compare_images'; +import { CasesServiceProvider } from './cases'; // define the name and providers for services that should be // available to your tests. If you don't specify anything here @@ -128,4 +129,5 @@ export const services = { searchSessions: SearchSessionsService, observability: ObservabilityProvider, compareImages: CompareImagesProvider, + cases: CasesServiceProvider, }; diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/create_case_form.ts b/x-pack/test/functional_with_es_ssl/apps/cases/create_case_form.ts new file mode 100644 index 00000000000000..252f639feef484 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/cases/create_case_form.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import uuid from 'uuid'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObject, getService }: FtrProviderContext) => { + describe('Create case', function () { + const common = getPageObject('common'); + const find = getService('find'); + const cases = getService('cases'); + const testSubjects = getService('testSubjects'); + + before(async () => { + await common.navigateToApp('cases'); + }); + + after(async () => { + await cases.api.deleteAllCases(); + }); + + it('creates a case from the stack management page', async () => { + const caseTitle = 'test-' + uuid.v4(); + await cases.common.createCaseFromCreateCasePage({ + title: caseTitle, + description: 'test description', + tag: 'tagme', + }); + + // validate title + const title = await find.byCssSelector('[data-test-subj="header-page-title"]'); + expect(await title.getVisibleText()).equal(caseTitle); + + // validate description + const description = await testSubjects.find('user-action-markdown'); + expect(await description.getVisibleText()).equal('test description'); + + // validate tag exists + await testSubjects.existOrFail('tag-tagme'); + + // validate no connector added + const button = await find.byCssSelector('[data-test-subj*="case-callout"] button'); + expect(await button.getVisibleText()).equal('Add connector'); + }); + }); +}; diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/edit_case_form.ts b/x-pack/test/functional_with_es_ssl/apps/cases/edit_case_form.ts new file mode 100644 index 00000000000000..adc7c3401aa969 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/cases/edit_case_form.ts @@ -0,0 +1,169 @@ +/* + * 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 uuid from 'uuid'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObject, getService }: FtrProviderContext) => { + const common = getPageObject('common'); + const header = getPageObject('header'); + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const cases = getService('cases'); + const retry = getService('retry'); + const comboBox = getService('comboBox'); + + describe('Edit case', () => { + // create the case to test on + before(async () => { + await common.navigateToApp('cases'); + await cases.api.createNthRandomCases(1); + }); + + after(async () => { + await cases.api.deleteAllCases(); + }); + + beforeEach(async () => { + await common.navigateToApp('cases'); + await cases.common.goToFirstListedCase(); + await header.waitUntilLoadingHasFinished(); + }); + + it('edits a case title from the case view page', async () => { + const newTitle = `test-${uuid.v4()}`; + + await testSubjects.click('editable-title-edit-icon'); + await testSubjects.setValue('editable-title-input-field', newTitle); + await testSubjects.click('editable-title-submit-btn'); + + // wait for backend response + await retry.tryForTime(5000, async () => { + const title = await find.byCssSelector('[data-test-subj="header-page-title"]'); + expect(await title.getVisibleText()).equal(newTitle); + }); + + // validate user action + await find.byCssSelector('[data-test-subj*="title-update-action"]'); + }); + + it('adds a comment to a case', async () => { + const commentArea = await find.byCssSelector( + '[data-test-subj="add-comment"] textarea.euiMarkdownEditorTextArea' + ); + await commentArea.focus(); + await commentArea.type('Test comment from automation'); + await testSubjects.click('submit-comment'); + + // validate user action + const newComment = await find.byCssSelector( + '[data-test-subj*="comment-create-action"] [data-test-subj="user-action-markdown"]' + ); + expect(await newComment.getVisibleText()).equal('Test comment from automation'); + }); + + it('adds a tag to a case', async () => { + const tag = uuid.v4(); + await testSubjects.click('tag-list-edit-button'); + await comboBox.setCustom('comboBoxInput', tag); + await testSubjects.click('edit-tags-submit'); + + // validate tag was added + await testSubjects.existOrFail('tag-' + tag); + + // validate user action + await find.byCssSelector('[data-test-subj*="tags-add-action"]'); + }); + + it('deletes a tag from a case', async () => { + await testSubjects.click('tag-list-edit-button'); + // find the tag button and click the close button + const button = await find.byCssSelector('[data-test-subj="comboBoxInput"] button'); + await button.click(); + await testSubjects.click('edit-tags-submit'); + + // validate user action + await find.byCssSelector('[data-test-subj*="tags-delete-action"]'); + }); + + it('changes a case status to in-progress via dropdown menu', async () => { + await cases.common.markCaseInProgressViaDropdown(); + // validate user action + await find.byCssSelector( + '[data-test-subj*="status-update-action"] [data-test-subj="status-badge-in-progress"]' + ); + // validates dropdown tag + await testSubjects.existOrFail('case-view-status-dropdown > status-badge-in-progress'); + }); + + it('changes a case status to closed via dropdown-menu', async () => { + await cases.common.markCaseClosedViaDropdown(); + + // validate user action + await find.byCssSelector( + '[data-test-subj*="status-update-action"] [data-test-subj="status-badge-closed"]' + ); + // validates dropdown tag + await testSubjects.existOrFail('case-view-status-dropdown > status-badge-closed'); + }); + + it("reopens a case from the 'reopen case' button", async () => { + await cases.common.markCaseClosedViaDropdown(); + await header.waitUntilLoadingHasFinished(); + await testSubjects.click('case-view-status-action-button'); + await header.waitUntilLoadingHasFinished(); + + await testSubjects.existOrFail('header-page-supplements > status-badge-open', { + timeout: 5000, + }); + + // validate user action + await find.byCssSelector( + '[data-test-subj*="status-update-action"] [data-test-subj="status-badge-open"]' + ); + // validates dropdown tag + await testSubjects.existOrFail('case-view-status-dropdown > status-badge-open'); + }); + + it("marks in progress a case from the 'mark in progress' button", async () => { + await cases.common.markCaseOpenViaDropdown(); + await header.waitUntilLoadingHasFinished(); + await testSubjects.click('case-view-status-action-button'); + await header.waitUntilLoadingHasFinished(); + + await testSubjects.existOrFail('header-page-supplements > status-badge-in-progress', { + timeout: 5000, + }); + + // validate user action + await find.byCssSelector( + '[data-test-subj*="status-update-action"] [data-test-subj="status-badge-in-progress"]' + ); + // validates dropdown tag + await testSubjects.existOrFail('case-view-status-dropdown > status-badge-in-progress'); + }); + + it("closes a case from the 'close case' button", async () => { + await cases.common.markCaseInProgressViaDropdown(); + await header.waitUntilLoadingHasFinished(); + await testSubjects.click('case-view-status-action-button'); + await header.waitUntilLoadingHasFinished(); + + await testSubjects.existOrFail('header-page-supplements > status-badge-closed', { + timeout: 5000, + }); + + // validate user action + await find.byCssSelector( + '[data-test-subj*="status-update-action"] [data-test-subj="status-badge-closed"]' + ); + // validates dropdown tag + await testSubjects.existOrFail('case-view-status-dropdown > status-badge-closed'); + }); + }); +}; diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/index.ts b/x-pack/test/functional_with_es_ssl/apps/cases/index.ts new file mode 100644 index 00000000000000..583fce960fbbdc --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/cases/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext) => { + describe('Cases', function () { + this.tags('ciGroup27'); + loadTestFile(require.resolve('./create_case_form')); + loadTestFile(require.resolve('./edit_case_form')); + loadTestFile(require.resolve('./list_view')); + }); +}; diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts b/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts new file mode 100644 index 00000000000000..66d1e83700dede --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts @@ -0,0 +1,122 @@ +/* + * 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 uuid from 'uuid'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObject, getService }: FtrProviderContext) => { + const common = getPageObject('common'); + const header = getPageObject('header'); + const testSubjects = getService('testSubjects'); + const cases = getService('cases'); + const retry = getService('retry'); + const browser = getService('browser'); + + describe('cases list', () => { + before(async () => { + await common.navigateToApp('cases'); + await cases.api.deleteAllCases(); + }); + + after(async () => { + await cases.api.deleteAllCases(); + }); + + beforeEach(async () => { + await common.navigateToApp('cases'); + }); + + it('displays an empty list with an add button correctly', async () => { + await testSubjects.existOrFail('cases-table-add-case'); + }); + + it('lists cases correctly', async () => { + const NUMBER_CASES = 2; + await cases.api.createNthRandomCases(NUMBER_CASES); + await common.navigateToApp('cases'); + await cases.common.validateCasesTableHasNthRows(NUMBER_CASES); + }); + + it('deletes a case correctly from the list', async () => { + await cases.api.createNthRandomCases(1); + await common.navigateToApp('cases'); + await testSubjects.click('action-delete'); + await testSubjects.click('confirmModalConfirmButton'); + await testSubjects.existOrFail('euiToastHeader'); + }); + + it('filters cases from the list with partial match', async () => { + await cases.api.deleteAllCases(); + await cases.api.createNthRandomCases(5); + const id = uuid.v4(); + const caseTitle = 'matchme-' + id; + await cases.api.createCaseWithData({ title: caseTitle }); + await common.navigateToApp('cases'); + await testSubjects.missingOrFail('cases-table-loading', { timeout: 5000 }); + + // search + const input = await testSubjects.find('search-cases'); + await input.type(caseTitle); + await input.pressKeys(browser.keys.ENTER); + + await retry.tryForTime(20000, async () => { + await cases.common.validateCasesTableHasNthRows(1); + }); + }); + + it('paginates cases correctly', async () => { + await cases.api.deleteAllCases(); + await cases.api.createNthRandomCases(8); + await common.navigateToApp('cases'); + await testSubjects.click('tablePaginationPopoverButton'); + await testSubjects.click('tablePagination-5-rows'); + await testSubjects.isEnabled('pagination-button-1'); + await testSubjects.click('pagination-button-1'); + await testSubjects.isEnabled('pagination-button-0'); + }); + + it('bulk delete cases from the list', async () => { + // deletes them from the API + await cases.api.deleteAllCases(); + await cases.api.createNthRandomCases(8); + await common.navigateToApp('cases'); + // deletes them from the UI + await cases.common.selectAndDeleteAllCases(); + await cases.common.validateCasesTableHasNthRows(0); + }); + + describe('changes status from the list', () => { + before(async () => { + await common.navigateToApp('cases'); + await cases.api.deleteAllCases(); + await cases.api.createNthRandomCases(1); + await common.navigateToApp('cases'); + }); + + it('to in progress', async () => { + await cases.common.openCaseSetStatusDropdown(); + await testSubjects.click('case-view-status-dropdown-in-progress'); + await header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('status-badge-in-progress'); + }); + + it('to closed', async () => { + await cases.common.openCaseSetStatusDropdown(); + await testSubjects.click('case-view-status-dropdown-closed'); + await header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('status-badge-closed'); + }); + + it('to open', async () => { + await cases.common.openCaseSetStatusDropdown(); + await testSubjects.click('case-view-status-dropdown-open'); + await header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('status-badge-open'); + }); + }); + }); +}; diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts index e537603a0113be..e906e239a88925 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -48,6 +48,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { resolve(__dirname, './apps/triggers_actions_ui'), resolve(__dirname, './apps/uptime'), resolve(__dirname, './apps/ml'), + resolve(__dirname, './apps/cases'), ], apps: { ...xpackFunctionalConfig.get('apps'), From 9449f1dcddaecca41970ff3b0d97ec52f9fb67fa Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 22 Mar 2022 20:11:26 +0100 Subject: [PATCH 15/64] [Lens] Xy gap settings (#127749) * add end value and fitting style settings * debug statement * fix tests * fix test and types * fix translation key * adjust copy Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/expressions/xy_chart/end_value.ts | 40 +++++ .../lens/common/expressions/xy_chart/index.ts | 1 + .../common/expressions/xy_chart/xy_args.ts | 3 + .../common/expressions/xy_chart/xy_chart.ts | 11 ++ .../__snapshots__/expression.test.tsx.snap | 6 + .../__snapshots__/to_expression.test.ts.snap | 6 + .../public/xy_visualization/expression.tsx | 36 ++++- .../xy_visualization/fitting_functions.ts | 18 ++- .../xy_visualization/to_expression.test.ts | 2 + .../public/xy_visualization/to_expression.ts | 2 + .../lens/public/xy_visualization/types.ts | 3 + .../visual_options_popover/index.tsx | 8 + .../missing_value_option.test.tsx | 13 +- .../missing_values_option.tsx | 151 ++++++++++++------ 14 files changed, 244 insertions(+), 56 deletions(-) create mode 100644 x-pack/plugins/lens/common/expressions/xy_chart/end_value.ts diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/end_value.ts b/x-pack/plugins/lens/common/expressions/xy_chart/end_value.ts new file mode 100644 index 00000000000000..1ef664cb2e2bac --- /dev/null +++ b/x-pack/plugins/lens/common/expressions/xy_chart/end_value.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export type EndValue = typeof endValueDefinitions[number]['id']; + +export const endValueDefinitions = [ + { + id: 'None', + title: i18n.translate('xpack.lens.endValue.none', { + defaultMessage: 'Hide', + }), + description: i18n.translate('xpack.lens.endValueDescription.none', { + defaultMessage: 'Do not extend series to the edge of the chart', + }), + }, + { + id: 'Zero', + title: i18n.translate('xpack.lens.endValue.zero', { + defaultMessage: 'Zero', + }), + description: i18n.translate('xpack.lens.endValueDescription.zero', { + defaultMessage: 'Extend series as zero to the edge of the chart', + }), + }, + { + id: 'Nearest', + title: i18n.translate('xpack.lens.endValue.nearest', { + defaultMessage: 'Nearest', + }), + description: i18n.translate('xpack.lens.endValueDescription.nearest', { + defaultMessage: 'Extend series with the first/last value to the edge of the chart', + }), + }, +] as const; diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/index.ts b/x-pack/plugins/lens/common/expressions/xy_chart/index.ts index a6f6c715c0ed1d..2f66c2c61a9f17 100644 --- a/x-pack/plugins/lens/common/expressions/xy_chart/index.ts +++ b/x-pack/plugins/lens/common/expressions/xy_chart/index.ts @@ -7,6 +7,7 @@ export * from './axis_config'; export * from './fitting_function'; +export * from './end_value'; export * from './grid_lines_config'; export * from './layer_config'; export * from './legend_config'; diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts b/x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts index 1334c1149f47b0..940896a2079e6f 100644 --- a/x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts +++ b/x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts @@ -7,6 +7,7 @@ import type { AxisExtentConfigResult, AxisTitlesVisibilityConfigResult } from './axis_config'; import type { FittingFunction } from './fitting_function'; +import type { EndValue } from './end_value'; import type { GridlinesConfigResult } from './grid_lines_config'; import type { DataLayerArgs } from './layer_config'; import type { LegendConfigResult } from './legend_config'; @@ -29,6 +30,8 @@ export interface XYArgs { valueLabels: ValueLabelConfig; layers: DataLayerArgs[]; fittingFunction?: FittingFunction; + endValue?: EndValue; + emphasizeFitting?: boolean; axisTitlesVisibilitySettings?: AxisTitlesVisibilityConfigResult; tickLabelsVisibilitySettings?: TickLabelsConfigResult; gridlinesVisibilitySettings?: GridlinesConfigResult; diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/xy_chart.ts b/x-pack/plugins/lens/common/expressions/xy_chart/xy_chart.ts index 481494d52966f2..d0f278d382be9d 100644 --- a/x-pack/plugins/lens/common/expressions/xy_chart/xy_chart.ts +++ b/x-pack/plugins/lens/common/expressions/xy_chart/xy_chart.ts @@ -10,6 +10,7 @@ import type { ExpressionValueSearchContext } from '../../../../../../src/plugins import type { LensMultiTable } from '../../types'; import type { XYArgs } from './xy_args'; import { fittingFunctionDefinitions } from './fitting_function'; +import { endValueDefinitions } from './end_value'; import { logDataTable } from '../expressions_utils'; export interface XYChartProps { @@ -87,6 +88,16 @@ export const xyChart: ExpressionFunctionDefinition< defaultMessage: 'Define how missing values are treated', }), }, + endValue: { + types: ['string'], + options: [...endValueDefinitions.map(({ id }) => id)], + help: '', + }, + emphasizeFitting: { + types: ['boolean'], + default: false, + help: '', + }, valueLabels: { types: ['string'], options: ['hide', 'inside'], diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap index b34d5e86393826..504a553c5a631e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap @@ -138,6 +138,7 @@ exports[`xy_expression XYChart component it renders area 1`] = ` enableHistogramMode={false} fit={ Object { + "endValue": undefined, "type": "none", } } @@ -198,6 +199,7 @@ exports[`xy_expression XYChart component it renders area 1`] = ` enableHistogramMode={false} fit={ Object { + "endValue": undefined, "type": "none", } } @@ -862,6 +864,7 @@ exports[`xy_expression XYChart component it renders line 1`] = ` enableHistogramMode={false} fit={ Object { + "endValue": undefined, "type": "none", } } @@ -922,6 +925,7 @@ exports[`xy_expression XYChart component it renders line 1`] = ` enableHistogramMode={false} fit={ Object { + "endValue": undefined, "type": "none", } } @@ -1094,6 +1098,7 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = ` enableHistogramMode={false} fit={ Object { + "endValue": undefined, "type": "none", } } @@ -1158,6 +1163,7 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = ` enableHistogramMode={false} fit={ Object { + "endValue": undefined, "type": "none", } } diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap index 5992d0bdb7264b..a1b0431f671383 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap @@ -33,6 +33,12 @@ Object { "description": Array [ "", ], + "emphasizeFitting": Array [ + true, + ], + "endValue": Array [ + "Nearest", + ], "fillOpacity": Array [ 0.3, ], diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 3e300778b85b95..72a3f5f4f69767 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -36,6 +36,7 @@ import { AreaSeriesProps, BarSeriesProps, LineSeriesProps, + ColorVariant, } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n-react'; import type { @@ -240,6 +241,8 @@ export function XYChart({ legend, layers, fittingFunction, + endValue, + emphasizeFitting, gridlinesVisibilitySettings, valueLabels, hideEndzones, @@ -857,15 +860,38 @@ export function XYChart({ areaSeriesStyle: { point: { visible: !xAccessor, - radius: 5, + radius: xAccessor && !emphasizeFitting ? 5 : 0, }, ...(args.fillOpacity && { area: { opacity: args.fillOpacity } }), + ...(emphasizeFitting && { + fit: { + area: { + opacity: args.fillOpacity || 0.5, + }, + line: { + visible: true, + stroke: ColorVariant.Series, + opacity: 1, + dash: [], + }, + }, + }), }, lineSeriesStyle: { point: { visible: !xAccessor, - radius: 5, + radius: xAccessor && !emphasizeFitting ? 5 : 0, }, + ...(emphasizeFitting && { + fit: { + line: { + visible: true, + stroke: ColorVariant.Series, + opacity: 1, + dash: [], + }, + }, + }), }, name(d) { // For multiple y series, the name of the operation is used on each, either: @@ -913,7 +939,7 @@ export function XYChart({ ); @@ -945,7 +971,7 @@ export function XYChart({ ); @@ -954,7 +980,7 @@ export function XYChart({ ); diff --git a/x-pack/plugins/lens/public/xy_visualization/fitting_functions.ts b/x-pack/plugins/lens/public/xy_visualization/fitting_functions.ts index 0b0878dfe96843..63a3b308d8ae8e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/fitting_functions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/fitting_functions.ts @@ -6,15 +6,25 @@ */ import { Fit } from '@elastic/charts'; -import { FittingFunction } from '../../common/expressions'; +import { EndValue, FittingFunction } from '../../common/expressions'; -export function getFitEnum(fittingFunction?: FittingFunction) { +export function getFitEnum(fittingFunction?: FittingFunction | EndValue) { if (fittingFunction) { return Fit[fittingFunction]; } return Fit.None; } -export function getFitOptions(fittingFunction?: FittingFunction) { - return { type: getFitEnum(fittingFunction) }; +export function getEndValue(endValue?: EndValue) { + if (endValue === 'Nearest') { + return Fit[endValue]; + } + if (endValue === 'Zero') { + return 0; + } + return undefined; +} + +export function getFitOptions(fittingFunction?: FittingFunction, endValue?: EndValue) { + return { type: getFitEnum(fittingFunction), endValue: getEndValue(endValue) }; } diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts index ac3fdcf30a4ad7..fa992d8829b202 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts @@ -54,6 +54,8 @@ describe('#toExpression', () => { valueLabels: 'hide', preferredSeriesType: 'bar', fittingFunction: 'Carry', + endValue: 'Nearest', + emphasizeFitting: true, tickLabelsVisibilitySettings: { x: false, yLeft: true, yRight: true }, labelsOrientation: { x: 0, diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index 37457c61b26033..a9c166a9c13eb1 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -194,6 +194,8 @@ export const buildExpression = ( }, ], fittingFunction: [state.fittingFunction || 'None'], + endValue: [state.endValue || 'None'], + emphasizeFitting: [state.emphasizeFitting || false], curveType: [state.curveType || 'LINEAR'], fillOpacity: [state.fillOpacity || 0.3], yLeftExtent: [ diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index b59d69bd8cbe66..2b9d5687979bed 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -27,6 +27,7 @@ import type { AxesSettingsConfig, FittingFunction, LabelsOrientationConfig, + EndValue, } from '../../common/expressions'; import type { ValueLabelConfig } from '../../common/types'; @@ -36,6 +37,8 @@ export interface XYState { legend: LegendConfig; valueLabels?: ValueLabelConfig; fittingFunction?: FittingFunction; + emphasizeFitting?: boolean; + endValue?: EndValue; yLeftExtent?: AxisExtentConfig; yRightExtent?: AxisExtentConfig; layers: XYLayerConfig[]; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/index.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/index.tsx index 0436a93be94ee3..0bdd513c1f881b 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/index.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/index.tsx @@ -115,9 +115,17 @@ export const VisualOptionsPopover: React.FC = ({ { setState({ ...state, fittingFunction: newVal }); }} + onEmphasizeFittingChange={(newVal) => { + setState({ ...state, emphasizeFitting: newVal }); + }} + onEndValueChange={(newVal) => { + setState({ ...state, endValue: newVal }); + }} /> { it('should show currently selected fitting function', () => { const component = shallow( - + ); - expect(component.find(EuiSuperSelect).prop('valueOfSelected')).toEqual('Carry'); + expect(component.find(EuiSuperSelect).first().prop('valueOfSelected')).toEqual('Carry'); }); it('should show the fitting option when enabled', () => { @@ -25,6 +30,8 @@ describe('Missing values option', () => { onFittingFnChange={jest.fn()} fittingFunction={'Carry'} isFittingEnabled={true} + onEmphasizeFittingChange={jest.fn()} + onEndValueChange={jest.fn()} /> ); @@ -37,6 +44,8 @@ describe('Missing values option', () => { onFittingFnChange={jest.fn()} fittingFunction={'Carry'} isFittingEnabled={false} + onEmphasizeFittingChange={jest.fn()} + onEndValueChange={jest.fn()} /> ); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/missing_values_option.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/missing_values_option.tsx index a858d1c879efe6..9bd59cb5c4a08e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/missing_values_option.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/missing_values_option.tsx @@ -7,69 +7,130 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiFormRow, EuiIconTip, EuiSuperSelect, EuiText } from '@elastic/eui'; -import { fittingFunctionDefinitions } from '../../../../common/expressions'; -import type { FittingFunction } from '../../../../common/expressions'; +import { EuiFormRow, EuiIconTip, EuiSuperSelect, EuiSwitch, EuiText } from '@elastic/eui'; +import { fittingFunctionDefinitions, endValueDefinitions } from '../../../../common/expressions'; +import type { FittingFunction, EndValue } from '../../../../common/expressions'; export interface MissingValuesOptionProps { fittingFunction?: FittingFunction; onFittingFnChange: (newMode: FittingFunction) => void; + emphasizeFitting?: boolean; + onEmphasizeFittingChange: (emphasize: boolean) => void; + endValue?: EndValue; + onEndValueChange: (endValue: EndValue) => void; isFittingEnabled?: boolean; } export const MissingValuesOptions: React.FC = ({ onFittingFnChange, fittingFunction, + emphasizeFitting, + onEmphasizeFittingChange, + onEndValueChange, + endValue, isFittingEnabled = true, }) => { return ( <> {isFittingEnabled && ( - + + {i18n.translate('xpack.lens.xyChart.missingValuesLabel', { + defaultMessage: 'Missing values', + })}{' '} + + + } + > + { + return { + value: id, + dropdownDisplay: ( + <> + {title} + +

{description}

+
+ + ), + inputDisplay: title, + }; + })} + valueOfSelected={fittingFunction || 'None'} + onChange={(value) => onFittingFnChange(value)} + itemLayoutAlign="top" + hasDividers + /> +
+ {fittingFunction && fittingFunction !== 'None' && ( <> - {i18n.translate('xpack.lens.xyChart.missingValuesLabel', { - defaultMessage: 'Missing values', - })}{' '} - + > + { + return { + value: id, + dropdownDisplay: ( + <> + {title} + +

{description}

+
+ + ), + inputDisplay: title, + }; + })} + valueOfSelected={endValue || 'None'} + onChange={(value) => onEndValueChange(value)} + itemLayoutAlign="top" + hasDividers + /> +
+ + { + onEmphasizeFittingChange(!emphasizeFitting); + }} + compressed + /> + - } - > - { - return { - value: id, - dropdownDisplay: ( - <> - {title} - -

{description}

-
- - ), - inputDisplay: title, - }; - })} - valueOfSelected={fittingFunction || 'None'} - onChange={(value) => onFittingFnChange(value)} - itemLayoutAlign="top" - hasDividers - /> - + )} + )} ); From 22e481af6b52da19a8da7c121b3e5910a4d9c4e6 Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Tue, 22 Mar 2022 12:25:36 -0700 Subject: [PATCH 16/64] Fix search_profiler ally test (#128084) * search_profiler * review comments * addressed review comments --- .../accessibility/apps/search_profiler.ts | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/x-pack/test/accessibility/apps/search_profiler.ts b/x-pack/test/accessibility/apps/search_profiler.ts index 6559d58be62987..47909662fb1327 100644 --- a/x-pack/test/accessibility/apps/search_profiler.ts +++ b/x-pack/test/accessibility/apps/search_profiler.ts @@ -13,16 +13,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const aceEditor = getService('aceEditor'); const a11y = getService('a11y'); - const flyout = getService('flyout'); + const esArchiver = getService('esArchiver'); - // FLAKY: https://github.com/elastic/kibana/issues/91939 - describe.skip('Accessibility Search Profiler Editor', () => { + describe('Accessibility Search Profiler Editor', () => { before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await PageObjects.common.navigateToApp('searchProfiler'); await a11y.testAppSnapshot(); expect(await testSubjects.exists('searchProfilerEditor')).to.be(true); }); + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); + }); + it('input the JSON in the aceeditor', async () => { const input = { query: { @@ -65,14 +69,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); - it('click on the open-close shard details link', async () => { - const openShardDetailslink = await testSubjects.findAll('openCloseShardDetails'); - await openShardDetailslink[0].click(); + it('close the flyout', async () => { + await testSubjects.click('euiFlyoutCloseButton'); await a11y.testAppSnapshot(); }); - it('close the fly out', async () => { - await flyout.ensureAllClosed(); + it('click on the open-close shard details link', async () => { + const openShardDetailslink = await testSubjects.findAll('openCloseShardDetails'); + await openShardDetailslink[0].click(); await a11y.testAppSnapshot(); }); @@ -80,16 +84,5 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('aggregationProfileTab'); await a11y.testAppSnapshot(); }); - - it('click on the view details link', async () => { - const viewShardDetailslink = await testSubjects.findAll('viewShardDetails'); - await viewShardDetailslink[0].click(); - await a11y.testAppSnapshot(); - }); - - it('close the fly out', async () => { - await flyout.ensureAllClosed(); - await a11y.testAppSnapshot(); - }); }); } From 12e789401894f0325306f0adc964aba999b9d2b0 Mon Sep 17 00:00:00 2001 From: Byron Hulcher Date: Tue, 22 Mar 2022 15:26:29 -0400 Subject: [PATCH 17/64] [Workplace Search] Update UX for custom api source creation flow (#127155) --- packages/kbn-doc-links/src/get_doc_links.ts | 1 + packages/kbn-doc-links/src/types.ts | 1 + .../shared/doc_links/doc_links.ts | 3 + .../applications/workplace_search/types.ts | 1 - .../add_custom_source.test.tsx | 10 +- .../add_custom_source.tsx | 10 +- .../add_custom_source_logic.test.ts | 21 +- .../add_custom_source_logic.ts | 8 +- .../configure_custom.test.tsx | 8 +- .../add_custom_source/configure_custom.tsx | 202 +++++++++++++++ .../add_source/add_custom_source/index.ts | 8 + .../add_custom_source/save_custom.test.tsx | 87 +++++++ .../add_custom_source/save_custom.tsx | 201 +++++++++++++++ .../add_source/config_completed.tsx | 2 +- .../add_source/configure_custom.tsx | 123 --------- .../components/add_source/constants.ts | 56 ----- .../add_source/save_custom.test.tsx | 55 ---- .../components/add_source/save_custom.tsx | 236 ------------------ .../components/source_identifier.tsx | 34 +-- .../views/content_sources/source_data.tsx | 13 +- .../translations/translations/fr-FR.json | 8 - .../translations/translations/ja-JP.json | 12 - .../translations/translations/zh-CN.json | 12 - 23 files changed, 542 insertions(+), 570 deletions(-) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/{ => add_custom_source}/add_custom_source.test.tsx (85%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/{ => add_custom_source}/add_custom_source.tsx (85%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/{ => add_custom_source}/add_custom_source_logic.test.ts (89%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/{ => add_custom_source}/add_custom_source_logic.ts (94%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/{ => add_custom_source}/configure_custom.test.tsx (84%) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/configure_custom.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/save_custom.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/save_custom.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 49708aa5fafc47..d0ff7dc704f760 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -139,6 +139,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { security: `${WORKPLACE_SEARCH_DOCS}workplace-search-security.html`, serviceNow: `${WORKPLACE_SEARCH_DOCS}workplace-search-servicenow-connector.html`, sharePoint: `${WORKPLACE_SEARCH_DOCS}workplace-search-sharepoint-online-connector.html`, + sharePointServer: `${WORKPLACE_SEARCH_DOCS}sharepoint-server.html`, slack: `${WORKPLACE_SEARCH_DOCS}workplace-search-slack-connector.html`, synch: `${WORKPLACE_SEARCH_DOCS}workplace-search-customizing-indexing-rules.html`, zendesk: `${WORKPLACE_SEARCH_DOCS}workplace-search-zendesk-connector.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index ef3b490bbb0946..fa1b4d6af41c81 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -128,6 +128,7 @@ export interface DocLinks { readonly security: string; readonly serviceNow: string; readonly sharePoint: string; + readonly sharePointServer: string; readonly slack: string; readonly synch: string; readonly zendesk: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts index f512a680efdfed..de29cf931c7702 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts @@ -59,6 +59,7 @@ class DocLinks { public workplaceSearchSecurity: string; public workplaceSearchServiceNow: string; public workplaceSearchSharePoint: string; + public workplaceSearchSharePointServer: string; public workplaceSearchSlack: string; public workplaceSearchSynch: string; public workplaceSearchZendesk: string; @@ -115,6 +116,7 @@ class DocLinks { this.workplaceSearchSecurity = ''; this.workplaceSearchServiceNow = ''; this.workplaceSearchSharePoint = ''; + this.workplaceSearchSharePointServer = ''; this.workplaceSearchSlack = ''; this.workplaceSearchSynch = ''; this.workplaceSearchZendesk = ''; @@ -174,6 +176,7 @@ class DocLinks { this.workplaceSearchSecurity = docLinks.links.workplaceSearch.security; this.workplaceSearchServiceNow = docLinks.links.workplaceSearch.serviceNow; this.workplaceSearchSharePoint = docLinks.links.workplaceSearch.sharePoint; + this.workplaceSearchSharePointServer = docLinks.links.workplaceSearch.sharePointServer; this.workplaceSearchSlack = docLinks.links.workplaceSearch.slack; this.workplaceSearchSynch = docLinks.links.workplaceSearch.synch; this.workplaceSearchZendesk = docLinks.links.workplaceSearch.zendesk; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index 6e0622c98be649..971b00b6529eee 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -66,7 +66,6 @@ export interface Configuration { needsConfiguration?: boolean; hasOauthRedirect: boolean; baseUrlTitle?: string; - helpText?: string; documentationUrl: string; applicationPortalUrl?: string; applicationLinkTitle?: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source.test.tsx similarity index 85% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source.test.tsx index b13cc6583cf2ff..b606f9d7f56fda 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source.test.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import '../../../../../__mocks__/shallow_useeffect.mock'; -import { setMockValues } from '../../../../../__mocks__/kea_logic'; -import { sourceConfigData } from '../../../../__mocks__/content_sources.mock'; +import '../../../../../../__mocks__/shallow_useeffect.mock'; +import { setMockValues } from '../../../../../../__mocks__/kea_logic'; +import { sourceConfigData } from '../../../../../__mocks__/content_sources.mock'; import React from 'react'; @@ -16,8 +16,8 @@ import { shallow } from 'enzyme'; import { WorkplaceSearchPageTemplate, PersonalDashboardLayout, -} from '../../../../components/layout'; -import { staticSourceData } from '../../source_data'; +} from '../../../../../components/layout'; +import { staticSourceData } from '../../../source_data'; import { AddCustomSource } from './add_custom_source'; import { AddCustomSourceSteps } from './add_custom_source_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source.tsx similarity index 85% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source.tsx index 6f7dc2bcdb342e..c2f6afba032c73 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source.tsx @@ -9,21 +9,19 @@ import React from 'react'; import { useValues } from 'kea'; -import { AppLogic } from '../../../../app_logic'; +import { AppLogic } from '../../../../../app_logic'; import { WorkplaceSearchPageTemplate, PersonalDashboardLayout, -} from '../../../../components/layout'; -import { NAV } from '../../../../constants'; +} from '../../../../../components/layout'; +import { NAV } from '../../../../../constants'; -import { SourceDataItem } from '../../../../types'; +import { SourceDataItem } from '../../../../../types'; import { AddCustomSourceLogic, AddCustomSourceSteps } from './add_custom_source_logic'; import { ConfigureCustom } from './configure_custom'; import { SaveCustom } from './save_custom'; -import './add_source.scss'; - interface Props { sourceData: SourceDataItem; initialValue?: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source_logic.test.ts similarity index 89% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source_logic.test.ts rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source_logic.test.ts index d019c66526e6cb..d2187bd0b21a15 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source_logic.test.ts @@ -9,22 +9,21 @@ import { LogicMounter, mockFlashMessageHelpers, mockHttpValues, -} from '../../../../../__mocks__/kea_logic'; -import { sourceConfigData } from '../../../../__mocks__/content_sources.mock'; +} from '../../../../../../__mocks__/kea_logic'; +import { sourceConfigData } from '../../../../../__mocks__/content_sources.mock'; -import { i18n } from '@kbn/i18n'; import { nextTick } from '@kbn/test-jest-helpers'; -import { docLinks } from '../../../../../shared/doc_links'; -import { itShowsServerErrorAsFlashMessage } from '../../../../../test_helpers'; +import { docLinks } from '../../../../../../shared/doc_links'; +import { itShowsServerErrorAsFlashMessage } from '../../../../../../test_helpers'; -jest.mock('../../../../app_logic', () => ({ +jest.mock('../../../../../app_logic', () => ({ AppLogic: { values: { isOrganization: true } }, })); -import { AppLogic } from '../../../../app_logic'; +import { AppLogic } from '../../../../../app_logic'; -import { SOURCE_NAMES } from '../../../../constants'; -import { CustomSource, SourceDataItem } from '../../../../types'; +import { SOURCE_NAMES } from '../../../../../constants'; +import { CustomSource, SourceDataItem } from '../../../../../types'; import { AddCustomSourceLogic, AddCustomSourceSteps } from './add_custom_source_logic'; @@ -36,10 +35,6 @@ const CUSTOM_SOURCE_DATA_ITEM: SourceDataItem = { isPublicKey: false, hasOauthRedirect: false, needsBaseUrl: false, - helpText: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.helpText.custom', { - defaultMessage: - 'To create a Custom API Source, provide a human-readable and descriptive name. The name will appear as-is in the various search experiences and management interfaces.', - }), documentationUrl: docLinks.workplaceSearchCustomSources, applicationPortalUrl: '', }, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source_logic.ts similarity index 94% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source_logic.ts rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source_logic.ts index c35436ccbf99ae..f85e0761f51b5b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source_logic.ts @@ -7,10 +7,10 @@ import { kea, MakeLogicType } from 'kea'; -import { flashAPIErrors, clearFlashMessages } from '../../../../../shared/flash_messages'; -import { HttpLogic } from '../../../../../shared/http'; -import { AppLogic } from '../../../../app_logic'; -import { CustomSource, SourceDataItem } from '../../../../types'; +import { flashAPIErrors, clearFlashMessages } from '../../../../../../shared/flash_messages'; +import { HttpLogic } from '../../../../../../shared/http'; +import { AppLogic } from '../../../../../app_logic'; +import { CustomSource, SourceDataItem } from '../../../../../types'; export interface AddCustomSourceProps { sourceData: SourceDataItem; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/configure_custom.test.tsx similarity index 84% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/configure_custom.test.tsx index 645226c546f102..3ed60614d294a1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/configure_custom.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import '../../../../../__mocks__/shallow_useeffect.mock'; -import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic'; +import '../../../../../../__mocks__/shallow_useeffect.mock'; +import { setMockActions, setMockValues } from '../../../../../../__mocks__/kea_logic'; import React from 'react'; @@ -14,7 +14,7 @@ import { shallow } from 'enzyme'; import { EuiForm, EuiFieldText } from '@elastic/eui'; -import { staticSourceData } from '../../source_data'; +import { staticSourceData } from '../../../source_data'; import { ConfigureCustom } from './configure_custom'; @@ -50,7 +50,7 @@ describe('ConfigureCustom', () => { const wrapper = shallow(); const preventDefault = jest.fn(); - wrapper.find('form').simulate('submit', { preventDefault }); + wrapper.find('EuiForm').simulate('submit', { preventDefault }); expect(preventDefault).toHaveBeenCalled(); expect(createContentSource).toHaveBeenCalled(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/configure_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/configure_custom.tsx new file mode 100644 index 00000000000000..024dd698cc0a25 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/configure_custom.tsx @@ -0,0 +1,202 @@ +/* + * 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, { ChangeEvent, FormEvent } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiLink, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { docLinks } from '../../../../../../shared/doc_links'; + +import connectionIllustration from '../../../../../assets/connection_illustration.svg'; +import { SOURCE_NAME_LABEL } from '../../../constants'; + +import { AddSourceHeader } from '../add_source_header'; +import { CONFIG_CUSTOM_BUTTON, CONFIG_CUSTOM_LINK_TEXT, CONFIG_INTRO_ALT_TEXT } from '../constants'; + +import { AddCustomSourceLogic } from './add_custom_source_logic'; + +export const ConfigureCustom: React.FC = () => { + const { setCustomSourceNameValue, createContentSource } = useActions(AddCustomSourceLogic); + const { customSourceNameValue, buttonLoading, sourceData } = useValues(AddCustomSourceLogic); + + const handleFormSubmit = (e: FormEvent) => { + e.preventDefault(); + createContentSource(); + }; + + const handleNameChange = (e: ChangeEvent) => + setCustomSourceNameValue(e.target.value); + + const { + serviceType, + configuration: { documentationUrl, githubRepository }, + name, + categories = [], + } = sourceData; + + return ( + <> + + + + +
+ {CONFIG_INTRO_ALT_TEXT} +
+
+ + + +

+ +

+
+ + + + {serviceType === 'custom' ? ( + <> +

+ +

+

+ + {CONFIG_CUSTOM_LINK_TEXT} + + ), + }} + /> +

+ + ) : ( + <> +

+ +

+

+ + + + ), + }} + /> +

+

+ + + +

+

+ + + +

+

+ + + +

+ + )} +
+ + + + + + + + {serviceType === 'custom' ? ( + CONFIG_CUSTOM_BUTTON + ) : ( + + )} + + +
+
+
+ + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/index.ts new file mode 100644 index 00000000000000..3565ea46632f70 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/index.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 { AddCustomSource } from './add_custom_source'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/save_custom.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/save_custom.test.tsx new file mode 100644 index 00000000000000..73add51b87955d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/save_custom.test.tsx @@ -0,0 +1,87 @@ +/* + * 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 { setMockValues } from '../../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiButtonTo } from '../../../../../../shared/react_router_helpers'; + +import { staticCustomSourceData } from '../../../source_data'; + +import { SourceIdentifier } from '../../source_identifier'; + +import { SaveCustom } from './save_custom'; + +const mockValues = { + newCustomSource: { + id: 'id', + accessToken: 'token', + name: 'name', + }, + sourceData: staticCustomSourceData, +}; + +describe('SaveCustom', () => { + describe('default behavior', () => { + let wrapper: ShallowWrapper; + + beforeAll(() => { + jest.clearAllMocks(); + setMockValues(mockValues); + + wrapper = shallow(); + }); + + it('contains a button back to the sources list', () => { + expect(wrapper.find(EuiButtonTo)).toHaveLength(1); + }); + + it('contains a source identifier', () => { + expect(wrapper.find(SourceIdentifier)).toHaveLength(1); + }); + + it('includes a link to generic documentation', () => { + expect(wrapper.find('[data-test-subj="GenericDocumentationLink"]')).toHaveLength(1); + }); + }); + + describe('for pre-configured custom sources', () => { + let wrapper: ShallowWrapper; + + beforeAll(() => { + jest.clearAllMocks(); + setMockValues({ + ...mockValues, + sourceData: { + ...staticCustomSourceData, + serviceType: 'sharepoint-server', + configuration: { + ...staticCustomSourceData.configuration, + githubRepository: 'elastic/sharepoint-server-connector', + }, + }, + }); + + wrapper = shallow(); + }); + + it('includes a to the github repository', () => { + expect(wrapper.find('[data-test-subj="GithubRepositoryLink"]')).toHaveLength(1); + }); + + it('includes a link to service-type specific documentation', () => { + expect(wrapper.find('[data-test-subj="PreconfiguredDocumentationLink"]')).toHaveLength(1); + }); + + it('includes a link to provide feedback', () => { + expect(wrapper.find('[data-test-subj="FeedbackCallout"]')).toHaveLength(1); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/save_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/save_custom.tsx new file mode 100644 index 00000000000000..8d0612f36fc0d7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/save_custom.tsx @@ -0,0 +1,201 @@ +/* + * 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 { useValues } from 'kea'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiSpacer, + EuiText, + EuiTextAlign, + EuiTitle, + EuiLink, + EuiPanel, + EuiHorizontalRule, + EuiCallOut, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { EuiButtonTo, EuiLinkTo } from '../../../../../../shared/react_router_helpers'; +import { AppLogic } from '../../../../../app_logic'; +import { API_KEY_LABEL } from '../../../../../constants'; +import { SOURCES_PATH, getSourcesPath, API_KEYS_PATH } from '../../../../../routes'; + +import { SourceIdentifier } from '../../source_identifier'; + +import { AddSourceHeader } from '../add_source_header'; +import { SAVE_CUSTOM_BODY1 as READY_TO_ACCEPT_REQUESTS_LABEL } from '../constants'; + +import { AddCustomSourceLogic } from './add_custom_source_logic'; + +export const SaveCustom: React.FC = () => { + const { newCustomSource, sourceData } = useValues(AddCustomSourceLogic); + const { isOrganization } = useValues(AppLogic); + const { + serviceType, + configuration: { githubRepository, documentationUrl }, + name, + categories = [], + } = sourceData; + + return ( + <> + + + + + + + + + + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.heading', + { + defaultMessage: '{name} Created', + values: { name: newCustomSource.name }, + } + )} +

+
+
+
+ + + {READY_TO_ACCEPT_REQUESTS_LABEL} + + + + + + + +
+
+
+ + + + {serviceType !== 'custom' && githubRepository ? ( + <> + + + + ), + }} + /> + + + + + ), + }} + /> + + ) : ( + + + + ), + }} + /> + )} + + + {API_KEY_LABEL} + + ), + }} + /> + + + + + +
+ {serviceType !== 'custom' && ( + <> + + + + + + + } + iconType="email" + /> + + + + )} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx index edd39409893a67..8af79587fefbb4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx @@ -62,7 +62,7 @@ export const ConfigCompleted: React.FC = ({ - + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx deleted file mode 100644 index bf5a7fea21333d..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx +++ /dev/null @@ -1,123 +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 React, { ChangeEvent, FormEvent } from 'react'; - -import { useActions, useValues } from 'kea'; - -import { - EuiButton, - EuiFieldText, - EuiForm, - EuiFormRow, - EuiLink, - EuiSpacer, - EuiText, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; - -import { docLinks } from '../../../../../shared/doc_links'; - -import { SOURCE_NAME_LABEL } from '../../constants'; - -import { AddCustomSourceLogic } from './add_custom_source_logic'; -import { AddSourceHeader } from './add_source_header'; -import { CONFIG_CUSTOM_BUTTON, CONFIG_CUSTOM_LINK_TEXT } from './constants'; - -export const ConfigureCustom: React.FC = () => { - const { setCustomSourceNameValue, createContentSource } = useActions(AddCustomSourceLogic); - const { customSourceNameValue, buttonLoading, sourceData } = useValues(AddCustomSourceLogic); - - const handleFormSubmit = (e: FormEvent) => { - e.preventDefault(); - createContentSource(); - }; - - const handleNameChange = (e: ChangeEvent) => - setCustomSourceNameValue(e.target.value); - - const { - serviceType, - configuration: { documentationUrl, helpText }, - name, - categories = [], - } = sourceData; - - return ( - <> - - -
- - -

{helpText}

-

- {serviceType === 'custom' ? ( - - {CONFIG_CUSTOM_LINK_TEXT} - - ), - }} - /> - ) : ( - - {CONFIG_CUSTOM_LINK_TEXT} - - ), - name, - }} - /> - )} -

-
- - - - - - - - {serviceType === 'custom' ? ( - CONFIG_CUSTOM_BUTTON - ) : ( - - )} - - -
-
- - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts index 5963f4cb256358..4499fc0483ce5b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts @@ -272,62 +272,6 @@ export const SAVE_CUSTOM_BODY1 = i18n.translate( } ); -export const SAVE_CUSTOM_BODY2 = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.body2', - { - defaultMessage: 'Be sure to copy your Source Identifier below.', - } -); - -export const SAVE_CUSTOM_RETURN_BUTTON = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.return.button', - { - defaultMessage: 'Return to Sources', - } -); - -export const SAVE_CUSTOM_VISUAL_WALKTHROUGH_TITLE = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.visualWalkthrough.title', - { - defaultMessage: 'Visual Walkthrough', - } -); - -export const SAVE_CUSTOM_VISUAL_WALKTHROUGH_LINK = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.visualWalkthrough.link', - { - defaultMessage: 'Check out the documentation', - } -); - -export const SAVE_CUSTOM_STYLING_RESULTS_TITLE = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.stylingResults.title', - { - defaultMessage: 'Styling Results', - } -); - -export const SAVE_CUSTOM_STYLING_RESULTS_LINK = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.stylingResults.link', - { - defaultMessage: 'Display Settings', - } -); - -export const SAVE_CUSTOM_DOC_PERMISSIONS_TITLE = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.docPermissions.title', - { - defaultMessage: 'Set document-level permissions', - } -); - -export const SAVE_CUSTOM_DOC_PERMISSIONS_LINK = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.docPermissions.link', - { - defaultMessage: 'Document-level permissions', - } -); - export const INCLUDED_FEATURES_TITLE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.contentSource.includedFeaturesTitle', { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.test.tsx deleted file mode 100644 index c05110bd4e6acf..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.test.tsx +++ /dev/null @@ -1,55 +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 { setMockValues } from '../../../../../__mocks__/kea_logic'; - -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { EuiPanel, EuiTitle } from '@elastic/eui'; - -import { EuiLinkTo } from '../../../../../shared/react_router_helpers'; - -import { LicenseBadge } from '../../../../components/shared/license_badge'; -import { staticCustomSourceData } from '../../source_data'; - -import { SaveCustom } from './save_custom'; - -describe('SaveCustom', () => { - const mockValues = { - newCustomSource: { - id: 'id', - accessToken: 'token', - name: 'name', - }, - sourceData: staticCustomSourceData, - isOrganization: true, - hasPlatinumLicense: true, - }; - - beforeEach(() => { - setMockValues(mockValues); - }); - - it('renders', () => { - const wrapper = shallow(); - - expect(wrapper.find(EuiPanel)).toHaveLength(1); - expect(wrapper.find(EuiTitle)).toHaveLength(4); - expect(wrapper.find(EuiLinkTo)).toHaveLength(1); - expect(wrapper.find(LicenseBadge)).toHaveLength(0); - }); - it('renders platinum license badge if license is not present', () => { - setMockValues({ ...mockValues, hasPlatinumLicense: false }); - const wrapper = shallow(); - - expect(wrapper.find(LicenseBadge)).toHaveLength(1); - expect(wrapper.find(EuiTitle)).toHaveLength(4); - expect(wrapper.find(EuiLinkTo)).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx deleted file mode 100644 index 14d088f377f5ed..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx +++ /dev/null @@ -1,236 +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 React from 'react'; - -import { useValues } from 'kea'; - -import { - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule, - EuiIcon, - EuiSpacer, - EuiText, - EuiTextAlign, - EuiTitle, - EuiLink, - EuiPanel, - EuiCode, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; - -import { docLinks } from '../../../../../shared/doc_links'; -import { LicensingLogic } from '../../../../../shared/licensing'; -import { EuiLinkTo } from '../../../../../shared/react_router_helpers'; -import { AppLogic } from '../../../../app_logic'; -import { LicenseBadge } from '../../../../components/shared/license_badge'; -import { - SOURCES_PATH, - SOURCE_DISPLAY_SETTINGS_PATH, - getContentSourcePath, - getSourcesPath, -} from '../../../../routes'; -import { LEARN_CUSTOM_FEATURES_BUTTON } from '../../constants'; - -import { SourceIdentifier } from '../source_identifier'; - -import { AddCustomSourceLogic } from './add_custom_source_logic'; -import { AddSourceHeader } from './add_source_header'; -import { - SAVE_CUSTOM_BODY1, - SAVE_CUSTOM_BODY2, - SAVE_CUSTOM_RETURN_BUTTON, - SAVE_CUSTOM_VISUAL_WALKTHROUGH_TITLE, - SAVE_CUSTOM_VISUAL_WALKTHROUGH_LINK, - SAVE_CUSTOM_STYLING_RESULTS_TITLE, - SAVE_CUSTOM_STYLING_RESULTS_LINK, - SAVE_CUSTOM_DOC_PERMISSIONS_TITLE, - SAVE_CUSTOM_DOC_PERMISSIONS_LINK, -} from './constants'; - -export const SaveCustom: React.FC = () => { - const { newCustomSource, sourceData } = useValues(AddCustomSourceLogic); - const { isOrganization } = useValues(AppLogic); - const { hasPlatinumLicense } = useValues(LicensingLogic); - const { - serviceType, - configuration: { githubRepository, documentationUrl }, - name, - categories = [], - } = sourceData; - - return ( - <> - - - - - - - - - - - - -

- {i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.heading', - { - defaultMessage: '{name} Created', - values: { name: newCustomSource.name }, - } - )} -

-
-
- - - {SAVE_CUSTOM_BODY1} - - {serviceType !== 'custom' && githubRepository && ( - <> - -
- - - {githubRepository} - - - - - )} - {SAVE_CUSTOM_BODY2} -
- - {SAVE_CUSTOM_RETURN_BUTTON} - -
-
-
-
- - - -
-
- - - - -
- -

{SAVE_CUSTOM_VISUAL_WALKTHROUGH_TITLE}

-
- - -

- {serviceType === 'custom' ? ( - - {SAVE_CUSTOM_VISUAL_WALKTHROUGH_LINK} - - ), - }} - /> - ) : ( - - {SAVE_CUSTOM_VISUAL_WALKTHROUGH_LINK} - - ), - name, - }} - /> - )} -

-
-
- -
- -

{SAVE_CUSTOM_STYLING_RESULTS_TITLE}

-
- - -

- - {SAVE_CUSTOM_STYLING_RESULTS_LINK} - - ), - }} - /> -

-
-
- -
- - {!hasPlatinumLicense && } - - -

{SAVE_CUSTOM_DOC_PERMISSIONS_TITLE}

-
- - -

- - {SAVE_CUSTOM_DOC_PERMISSIONS_LINK} - - ), - }} - /> -

-
- - {!hasPlatinumLicense && ( - - - {LEARN_CUSTOM_FEATURES_BUTTON} - - - )} -
-
-
-
-
- - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_identifier.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_identifier.tsx index 2c7784a554a259..83d11d781bbdab 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_identifier.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_identifier.tsx @@ -14,14 +14,11 @@ import { EuiCopy, EuiButtonIcon, EuiFieldText, - EuiSpacer, } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiLinkTo } from '../../../../shared/react_router_helpers'; +import { i18n } from '@kbn/i18n'; -import { API_KEY_LABEL, COPY_TOOLTIP, COPIED_TOOLTIP } from '../../../constants'; -import { API_KEYS_PATH } from '../../../routes'; +import { COPY_TOOLTIP, COPIED_TOOLTIP } from '../../../constants'; import { ID_LABEL } from '../constants'; @@ -50,24 +47,17 @@ export const SourceIdentifier: React.FC = ({ id }) => (
- +
- - -

- - {API_KEY_LABEL} - - ), - }} - /> -

-
); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx index a4c6b3c6fd4d04..361eccbe8da380 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx @@ -560,14 +560,7 @@ export const staticSourceData: SourceDataItem[] = [ isPublicKey: false, hasOauthRedirect: false, needsBaseUrl: false, - // helpText: i18n.translate( // TODO updatae this - // 'xpack.enterpriseSearch.workplaceSearch.sources.helpText.sharepointServer', - // { - // defaultMessage: - // "Here is some help text. It should probably give the user a heads up that they're going to have to deploy some code.", - // } - // ), - documentationUrl: docLinks.workplaceSearchCustomSources, // TODO update this + documentationUrl: docLinks.workplaceSearchSharePointServer, applicationPortalUrl: '', githubRepository: 'elastic/enterprise-search-sharepoint-server-connector', }, @@ -638,10 +631,6 @@ export const staticCustomSourceData: SourceDataItem = { isPublicKey: false, hasOauthRedirect: false, needsBaseUrl: false, - helpText: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.helpText.custom', { - defaultMessage: - 'To create a Custom API Source, provide a human-readable and descriptive name. The name will appear as-is in the various search experiences and management interfaces.', - }), documentationUrl: docLinks.workplaceSearchCustomSources, applicationPortalUrl: '', }, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 05ab45fc2756f1..4a9913fe97aba7 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -9395,15 +9395,7 @@ "xpack.enterpriseSearch.workplaceSearch.contentSource.saveConfig.oauthStep1": "Créer une application OAuth dans le compte {sourceName} de votre organisation", "xpack.enterpriseSearch.workplaceSearch.contentSource.saveConfig.oauthStep2": "Fournir les informations de configuration appropriées", "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.body1": "Vos points de terminaison sont prêts à accepter les requêtes.", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.body2": "Veillez à copier vos clés d'API ci-dessous.", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.displaySettings.text": "Utilisez {link} pour personnaliser le mode d'affichage de vos documents dans vos résultats de recherche. Par défaut, Workplace Search utilisera les champs par ordre alphabétique.", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.docPermissions.title": "Définir les autorisations de niveau document", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.documentation.text": "{link} pour en savoir plus sur les sources d'API personnalisées.", "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.heading": "{name} créé", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.permissions.text": "{link} gèrent le contenu de l'accès au contenu selon les attributs individuels ou de groupe. Autorisez ou refusez l'accès à des documents spécifiques.", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.return.button": "Retour aux sources", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.stylingResults.title": "Résultats de styles", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.visualWalkthrough.title": "Présentation visuelle", "xpack.enterpriseSearch.workplaceSearch.contentSource.schema.addField.button": "Ajouter un champ", "xpack.enterpriseSearch.workplaceSearch.contentSource.schema.empty.description": "Un schéma est créé à votre place une fois que vous avez indexé quelques documents. Cliquez ci-dessous pour créer des champs de schéma à l'avance.", "xpack.enterpriseSearch.workplaceSearch.contentSource.schema.empty.title": "La source de contenu ne possède pas de schéma", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index bcdafe3c8c050d..1dea85f7f14999 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11076,18 +11076,7 @@ "xpack.enterpriseSearch.workplaceSearch.contentSource.saveConfig.oauthStep1": "組織の{sourceName}アカウントでOAuthアプリを作成する", "xpack.enterpriseSearch.workplaceSearch.contentSource.saveConfig.oauthStep2": "適切な構成情報を入力する", "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.body1": "エンドポイントは要求を承認できます。", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.body2": "必ず以下のソースIDをコピーしてください。", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.displaySettings.text": "{link}を使用して、検索結果内でドキュメントが表示される方法をカスタマイズします。デフォルトでは、Workplace Searchは英字順でフィールドを使用します。", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.docPermissions.link": "ドキュメントレベルのアクセス権", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.docPermissions.title": "ドキュメントレベルのアクセス権を設定", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.documentation.text": "カスタムAPIソースの詳細については、{link}。", "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.heading": "{name}が作成されました", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.permissions.text": "{link}は個別またはグループの属性でコンテンツアクセスコンテンツを管理します。特定のドキュメントへのアクセスを許可または拒否。", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.return.button": "ソースに戻る", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.stylingResults.link": "表示設定", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.stylingResults.title": "スタイルの結果", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.visualWalkthrough.link": "ドキュメントを確認", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.visualWalkthrough.title": "表示の確認", "xpack.enterpriseSearch.workplaceSearch.contentSource.schema.addField.button": "フィールドの追加", "xpack.enterpriseSearch.workplaceSearch.contentSource.schema.empty.description": "一部のドキュメントにインデックスを作成すると、スキーマが作成されます。あらかじめスキーマフィールドを作成するには、以下をクリックします。", "xpack.enterpriseSearch.workplaceSearch.contentSource.schema.empty.title": "コンテンツソースにはスキーマがありません", @@ -11443,7 +11432,6 @@ "xpack.enterpriseSearch.workplaceSearch.sources.groupAccess.title": "グループアクセス", "xpack.enterpriseSearch.workplaceSearch.sources.helpText.custom": "カスタムAPIソースを作成するには、人間が読み取れるわかりやすい名前を入力します。この名前はさまざまな検索エクスペリエンスと管理インターフェースでそのまま表示されます。", "xpack.enterpriseSearch.workplaceSearch.sources.id.label": "ソース識別子", - "xpack.enterpriseSearch.workplaceSearch.sources.identifier.helpText": "ソースIDと{apiKeyLink}を使用して、このカスタムソースのドキュメントを同期します。", "xpack.enterpriseSearch.workplaceSearch.sources.incrementalSyncDescription": "前回の同期ジョブ以降に発生したドキュメント/更新を取得します", "xpack.enterpriseSearch.workplaceSearch.sources.incrementalSyncLabel": "差分同期", "xpack.enterpriseSearch.workplaceSearch.sources.items.header": "アイテム", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d2dbc9904b9a15..fabf4d7a2d5902 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11097,18 +11097,7 @@ "xpack.enterpriseSearch.workplaceSearch.contentSource.saveConfig.oauthStep1": "在组织的 {sourceName} 帐户中创建 OAuth 应用", "xpack.enterpriseSearch.workplaceSearch.contentSource.saveConfig.oauthStep2": "提供适当的配置信息", "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.body1": "您的终端已准备好接受请求。", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.body2": "确保复制下面的源标识符。", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.displaySettings.text": "请使用 {link} 定制您的文档在搜索结果内显示的方式。Workplace Search 默认按字母顺序使用字段。", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.docPermissions.link": "文档级权限", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.docPermissions.title": "设置文档级权限", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.documentation.text": "{link}以详细了解定制 API 源。", "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.heading": "{name} 已创建", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.permissions.text": "{link} 管理有关单个属性或组属性的内容访问内容。允许或拒绝对特定文档的访问。", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.return.button": "返回到源", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.stylingResults.link": "显示设置", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.stylingResults.title": "正在为结果应用样式", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.visualWalkthrough.link": "查阅文档", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.visualWalkthrough.title": "直观的演练", "xpack.enterpriseSearch.workplaceSearch.contentSource.schema.addField.button": "添加字段", "xpack.enterpriseSearch.workplaceSearch.contentSource.schema.empty.description": "您索引一些文档后,系统便会为您创建架构。单击下面,以提前创建架构字段。", "xpack.enterpriseSearch.workplaceSearch.contentSource.schema.empty.title": "内容源没有架构", @@ -11464,7 +11453,6 @@ "xpack.enterpriseSearch.workplaceSearch.sources.groupAccess.title": "组访问权限", "xpack.enterpriseSearch.workplaceSearch.sources.helpText.custom": "要创建定制 API 源,请提供可人工读取的描述性名称。名称在各种搜索体验和管理界面中都原样显示。", "xpack.enterpriseSearch.workplaceSearch.sources.id.label": "源标识符", - "xpack.enterpriseSearch.workplaceSearch.sources.identifier.helpText": "将源标识符用于 {apiKeyLink},以同步此定制源的文档。", "xpack.enterpriseSearch.workplaceSearch.sources.incrementalSyncDescription": "检索自上次同步作业以来的文档/更新", "xpack.enterpriseSearch.workplaceSearch.sources.incrementalSyncLabel": "增量同步", "xpack.enterpriseSearch.workplaceSearch.sources.items.header": "项", From 7d29236a8e61e2432889c0f4e93ef49e44646fe1 Mon Sep 17 00:00:00 2001 From: Rachel Shen Date: Tue, 22 Mar 2022 13:47:05 -0600 Subject: [PATCH 18/64] [shared-ux] Migrate add from library button to shared ux directory (#127605) --- .../kbn-shared-ux-components/src/index.ts | 5 ++ .../add_from_library.test.tsx.snap | 74 +++++++++++++++++++ .../add_from_library/add_from_library.mdx | 10 +++ .../add_from_library.stories.tsx | 25 +++++++ .../add_from_library.test.tsx | 19 +++++ .../add_from_library/add_from_library.tsx | 24 ++++++ .../icon_button_group.stories.tsx | 2 +- .../src/toolbar/buttons/primary/primary.mdx | 6 +- .../buttons/primary/primary.stories.tsx | 2 +- .../toolbar/buttons/primary/primary.test.tsx | 4 - .../src/toolbar/index.ts | 2 +- 11 files changed, 163 insertions(+), 10 deletions(-) create mode 100644 packages/kbn-shared-ux-components/src/toolbar/buttons/add_from_library/__snapshots__/add_from_library.test.tsx.snap create mode 100644 packages/kbn-shared-ux-components/src/toolbar/buttons/add_from_library/add_from_library.mdx create mode 100644 packages/kbn-shared-ux-components/src/toolbar/buttons/add_from_library/add_from_library.stories.tsx create mode 100644 packages/kbn-shared-ux-components/src/toolbar/buttons/add_from_library/add_from_library.test.tsx create mode 100644 packages/kbn-shared-ux-components/src/toolbar/buttons/add_from_library/add_from_library.tsx diff --git a/packages/kbn-shared-ux-components/src/index.ts b/packages/kbn-shared-ux-components/src/index.ts index a43b53a6e7cd1c..c5e719a904ebdc 100644 --- a/packages/kbn-shared-ux-components/src/index.ts +++ b/packages/kbn-shared-ux-components/src/index.ts @@ -41,6 +41,11 @@ export const ExitFullScreenButton = withSuspense(LazyExitFullScreenButton); */ export const ToolbarButton = withSuspense(LazyToolbarButton); +/** + * An example of the solution toolbar button + */ +export { AddFromLibraryButton } from './toolbar'; + /** * The Lazily-loaded `NoDataViews` component. Consumers should use `React.Suspennse` or the * `withSuspense` HOC to load this component. diff --git a/packages/kbn-shared-ux-components/src/toolbar/buttons/add_from_library/__snapshots__/add_from_library.test.tsx.snap b/packages/kbn-shared-ux-components/src/toolbar/buttons/add_from_library/__snapshots__/add_from_library.test.tsx.snap new file mode 100644 index 00000000000000..4cdc858c7e50c7 --- /dev/null +++ b/packages/kbn-shared-ux-components/src/toolbar/buttons/add_from_library/__snapshots__/add_from_library.test.tsx.snap @@ -0,0 +1,74 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` is rendered 1`] = ` + + + + + + + + + +`; diff --git a/packages/kbn-shared-ux-components/src/toolbar/buttons/add_from_library/add_from_library.mdx b/packages/kbn-shared-ux-components/src/toolbar/buttons/add_from_library/add_from_library.mdx new file mode 100644 index 00000000000000..f6a2f92cd41eb1 --- /dev/null +++ b/packages/kbn-shared-ux-components/src/toolbar/buttons/add_from_library/add_from_library.mdx @@ -0,0 +1,10 @@ +--- +id: sharedUX/Components/AddFromLibraryButton +slug: /shared-ux/components/toolbar/buttons/add_from_library +title: Add From Library Button +summary: An example of the primary button +tags: ['shared-ux', 'component'] +date: 2022-03-18 +--- + +This button is an example of the primary button. diff --git a/packages/kbn-shared-ux-components/src/toolbar/buttons/add_from_library/add_from_library.stories.tsx b/packages/kbn-shared-ux-components/src/toolbar/buttons/add_from_library/add_from_library.stories.tsx new file mode 100644 index 00000000000000..ea504315450282 --- /dev/null +++ b/packages/kbn-shared-ux-components/src/toolbar/buttons/add_from_library/add_from_library.stories.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { AddFromLibraryButton } from './add_from_library'; +import mdx from './add_from_library.mdx'; + +export default { + title: 'Toolbar/Buttons/Add From Library Button', + description: 'An implementation of the solution toolbar primary button', + parameters: { + docs: { + page: mdx, + }, + }, +}; + +export const Component = () => { + return ; +}; diff --git a/packages/kbn-shared-ux-components/src/toolbar/buttons/add_from_library/add_from_library.test.tsx b/packages/kbn-shared-ux-components/src/toolbar/buttons/add_from_library/add_from_library.test.tsx new file mode 100644 index 00000000000000..a2ba1d8bff174a --- /dev/null +++ b/packages/kbn-shared-ux-components/src/toolbar/buttons/add_from_library/add_from_library.test.tsx @@ -0,0 +1,19 @@ +/* + * 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 { mount as enzymeMount } from 'enzyme'; +import React from 'react'; +import { AddFromLibraryButton } from './add_from_library'; + +describe('', () => { + test('is rendered', () => { + const component = enzymeMount(); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/packages/kbn-shared-ux-components/src/toolbar/buttons/add_from_library/add_from_library.tsx b/packages/kbn-shared-ux-components/src/toolbar/buttons/add_from_library/add_from_library.tsx new file mode 100644 index 00000000000000..190edc8f294913 --- /dev/null +++ b/packages/kbn-shared-ux-components/src/toolbar/buttons/add_from_library/add_from_library.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { i18n } from '@kbn/i18n'; +import React from 'react'; +import { ToolbarButton, Props as ToolbarButtonProps } from '../primary/primary'; + +export type Props = Omit; + +const label = { + getLibraryButtonLabel: () => + i18n.translate('sharedUXComponents.toolbar.buttons.addFromLibrary.libraryButtonLabel', { + defaultMessage: 'Add from library', + }), +}; + +export const AddFromLibraryButton = ({ onClick, ...rest }: Props) => ( + +); diff --git a/packages/kbn-shared-ux-components/src/toolbar/buttons/icon_button_group/icon_button_group.stories.tsx b/packages/kbn-shared-ux-components/src/toolbar/buttons/icon_button_group/icon_button_group.stories.tsx index c30f015325672b..988a5bddd513f0 100644 --- a/packages/kbn-shared-ux-components/src/toolbar/buttons/icon_button_group/icon_button_group.stories.tsx +++ b/packages/kbn-shared-ux-components/src/toolbar/buttons/icon_button_group/icon_button_group.stories.tsx @@ -13,7 +13,7 @@ import { IconButtonGroup } from './icon_button_group'; import mdx from './icon_button_group.mdx'; export default { - title: 'Toolbar/Icon Button Group', + title: 'Toolbar/Buttons/Icon Button Group', description: 'A collection of buttons that is a part of a toolbar.', parameters: { docs: { diff --git a/packages/kbn-shared-ux-components/src/toolbar/buttons/primary/primary.mdx b/packages/kbn-shared-ux-components/src/toolbar/buttons/primary/primary.mdx index 489596e771c295..c1fa431f39bdc1 100644 --- a/packages/kbn-shared-ux-components/src/toolbar/buttons/primary/primary.mdx +++ b/packages/kbn-shared-ux-components/src/toolbar/buttons/primary/primary.mdx @@ -1,7 +1,7 @@ --- -id: sharedUX/Components/Toolbar/Primary_Button +id: sharedUX/Components/ToolbarButton slug: /shared-ux/components/toolbar/buttons/primary -title: Toolbar Button +title: Solution Toolbar Button summary: An opinionated implementation of the toolbar extracted to just the button. tags: ['shared-ux', 'component'] date: 2022-02-17 @@ -9,4 +9,4 @@ date: 2022-02-17 > This documentation is in-progress. -This button is a part of the toolbar component. This button has primary styling and requires a `label`. Interaction (`onClick`) handlers and `iconType`s are supported as an extension of EuiButtonProps. Icons are always on the left of any labels within the button. +This button is a part of the solution toolbar component. This button has primary styling and requires a label. OnClick handlers and icon types are supported as an extension of EuiButtonProps. Icons are always on the left of any labels within the button. diff --git a/packages/kbn-shared-ux-components/src/toolbar/buttons/primary/primary.stories.tsx b/packages/kbn-shared-ux-components/src/toolbar/buttons/primary/primary.stories.tsx index 0388cccb60c3f7..a81be610c1508c 100644 --- a/packages/kbn-shared-ux-components/src/toolbar/buttons/primary/primary.stories.tsx +++ b/packages/kbn-shared-ux-components/src/toolbar/buttons/primary/primary.stories.tsx @@ -12,7 +12,7 @@ import { ToolbarButton } from './primary'; import mdx from './primary.mdx'; export default { - title: 'Toolbar/Primary button', + title: 'Toolbar/Buttons/Primary button', description: 'A primary button that is a part of a toolbar.', parameters: { docs: { diff --git a/packages/kbn-shared-ux-components/src/toolbar/buttons/primary/primary.test.tsx b/packages/kbn-shared-ux-components/src/toolbar/buttons/primary/primary.test.tsx index e93d537a40ce5f..3e0e153f453e54 100644 --- a/packages/kbn-shared-ux-components/src/toolbar/buttons/primary/primary.test.tsx +++ b/packages/kbn-shared-ux-components/src/toolbar/buttons/primary/primary.test.tsx @@ -27,10 +27,6 @@ describe('', () => { enzymeMount({element}); }); - afterEach(() => { - jest.resetAllMocks(); - }); - test('is rendered', () => { const component = mount(); diff --git a/packages/kbn-shared-ux-components/src/toolbar/index.ts b/packages/kbn-shared-ux-components/src/toolbar/index.ts index e68abf2916a72d..513f81c1ddfc75 100644 --- a/packages/kbn-shared-ux-components/src/toolbar/index.ts +++ b/packages/kbn-shared-ux-components/src/toolbar/index.ts @@ -5,6 +5,6 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - export { ToolbarButton } from './buttons/primary/primary'; export { IconButtonGroup } from './buttons/icon_button_group/icon_button_group'; +export { AddFromLibraryButton } from './buttons/add_from_library/add_from_library'; From 72399c47aa265e4386d342804d6286f3763be92e Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Tue, 22 Mar 2022 13:47:30 -0700 Subject: [PATCH 19/64] ally geo smoke test (#127982) * ally geo * remove unwanted data * addressed review comments * added maps listing page tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/test/accessibility/apps/maps.ts | 127 +++++++++++++++++++++++++ x-pack/test/accessibility/config.ts | 1 + 2 files changed, 128 insertions(+) create mode 100644 x-pack/test/accessibility/apps/maps.ts diff --git a/x-pack/test/accessibility/apps/maps.ts b/x-pack/test/accessibility/apps/maps.ts new file mode 100644 index 00000000000000..079972273c19bb --- /dev/null +++ b/x-pack/test/accessibility/apps/maps.ts @@ -0,0 +1,127 @@ +/* + * 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 { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const a11y = getService('a11y'); + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const PageObjects = getPageObjects(['common', 'settings', 'header', 'home', 'maps']); + + describe('Maps app meets ally validations', () => { + before(async () => { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.home.addSampleDataSet('flights'); + await PageObjects.common.navigateToApp('maps'); + }); + + after(async () => { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.home.removeSampleDataSet('flights'); + }); + + it('loads maps workpads', async function () { + await PageObjects.maps.loadSavedMap('[Flights] Origin Time Delayed'); + await a11y.testAppSnapshot(); + }); + + it('click map settings', async function () { + await testSubjects.click('openSettingsButton'); + await a11y.testAppSnapshot(); + }); + + it('map save button', async function () { + await testSubjects.click('mapSaveButton'); + await a11y.testAppSnapshot(); + }); + + it('map cancel button', async function () { + await testSubjects.click('saveCancelButton'); + await a11y.testAppSnapshot(); + }); + + it('map inspect button', async function () { + await testSubjects.click('openInspectorButton'); + await a11y.testAppSnapshot(); + }); + + it('map inspect view chooser ', async function () { + await testSubjects.click('inspectorViewChooser'); + await a11y.testAppSnapshot(); + }); + + it('map inspector view chooser requests', async function () { + await testSubjects.click('inspectorViewChooserRequests'); + await a11y.testAppSnapshot(); + }); + + it('map inspector view chooser requests', async function () { + await PageObjects.maps.openInspectorMapView(); + await a11y.testAppSnapshot(); + }); + + it('map inspector close', async function () { + await testSubjects.click('euiFlyoutCloseButton'); + await a11y.testAppSnapshot(); + }); + + it('full screen button should exist', async () => { + await testSubjects.click('mapsFullScreenMode'); + await a11y.testAppSnapshot(); + }); + + it('displays exit full screen logo button', async () => { + await testSubjects.click('exitFullScreenModeLogo'); + await a11y.testAppSnapshot(); + }); + + it(`allows a map to be created`, async () => { + await PageObjects.maps.openNewMap(); + await a11y.testAppSnapshot(); + await PageObjects.maps.expectExistAddLayerButton(); + await a11y.testAppSnapshot(); + await PageObjects.maps.saveMap('my test map'); + await a11y.testAppSnapshot(); + }); + + it('maps listing page', async function () { + await PageObjects.common.navigateToApp('maps'); + await retry.waitFor( + 'maps workpads visible', + async () => await testSubjects.exists('itemsInMemTable') + ); + await a11y.testAppSnapshot(); + }); + + it('provides bulk selection', async function () { + await testSubjects.click('checkboxSelectAll'); + await a11y.testAppSnapshot(); + }); + + it('provides bulk delete', async function () { + await testSubjects.click('deleteSelectedItems'); + await a11y.testAppSnapshot(); + }); + + it('single delete modal', async function () { + await testSubjects.click('confirmModalConfirmButton'); + await a11y.testAppSnapshot(); + }); + + it('single cancel modal', async function () { + await testSubjects.click('confirmModalCancelButton'); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index a85259d465084e..e85b8a9ef17d81 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -35,6 +35,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/lens'), require.resolve('./apps/upgrade_assistant'), require.resolve('./apps/canvas'), + require.resolve('./apps/maps'), require.resolve('./apps/security_solution'), require.resolve('./apps/ml_embeddables_in_dashboard'), require.resolve('./apps/remote_clusters'), From 9845a5c21769850b1b3580cf1f08417102596ece Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 22 Mar 2022 16:50:33 -0400 Subject: [PATCH 20/64] [Security team: AWP] [Session view] Add alert fly out callback (#127991) * Add alertFlyoutCallback to process_tree_alert * Add useCallback hook to callback functions * Rename to loadAlertDetails and add handleOnAlertDetailsClosed * Finish functionality * Fix jest tests * Add tests for updateAlertEventStatus * Fix PR comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/session_view/common/constants.ts | 24 ++++ .../constants/session_view_process.mock.ts | 3 + .../common/types/process_tree/index.ts | 8 ++ .../components/process_tree/helpers.test.ts | 53 ++++++++- .../public/components/process_tree/helpers.ts | 33 +++++- .../public/components/process_tree/hooks.ts | 29 ++++- .../components/process_tree/index.test.tsx | 107 ++++-------------- .../public/components/process_tree/index.tsx | 20 +++- .../process_tree_alert/index.test.tsx | 56 +++++---- .../components/process_tree_alert/index.tsx | 36 ++++-- .../process_tree_alerts/index.test.tsx | 17 +-- .../components/process_tree_alerts/index.tsx | 21 +++- .../process_tree_node/index.test.tsx | 1 + .../components/process_tree_node/index.tsx | 29 +++-- .../public/components/session_view/hooks.ts | 55 ++++++++- .../public/components/session_view/index.tsx | 46 ++++++-- .../session_view/public/methods/index.tsx | 8 +- x-pack/plugins/session_view/public/types.ts | 6 + .../server/routes/alert_status_route.ts | 56 +++++++++ .../session_view/server/routes/index.ts | 2 + 20 files changed, 441 insertions(+), 169 deletions(-) create mode 100644 x-pack/plugins/session_view/server/routes/alert_status_route.ts diff --git a/x-pack/plugins/session_view/common/constants.ts b/x-pack/plugins/session_view/common/constants.ts index 4ca130c6af7b4e..42e1d33ab6dba5 100644 --- a/x-pack/plugins/session_view/common/constants.ts +++ b/x-pack/plugins/session_view/common/constants.ts @@ -6,11 +6,18 @@ */ export const PROCESS_EVENTS_ROUTE = '/internal/session_view/process_events_route'; +export const ALERT_STATUS_ROUTE = '/internal/session_view/alert_status_route'; export const SESSION_ENTRY_LEADERS_ROUTE = '/internal/session_view/session_entry_leaders_route'; export const PROCESS_EVENTS_INDEX = 'logs-endpoint.events.process-default'; export const ALERTS_INDEX = '.siem-signals-default'; export const ENTRY_SESSION_ENTITY_ID_PROPERTY = 'process.entry_leader.entity_id'; +export const ALERT_UUID_PROPERTY = 'kibana.alert.uuid'; export const KIBANA_DATE_FORMAT = 'MMM DD, YYYY @ hh:mm:ss.SSS'; +export const ALERT_STATUS = { + OPEN: 'open', + ACKNOWLEDGED: 'acknowledged', + CLOSED: 'closed', +}; // We fetch a large number of events per page to mitigate a few design caveats in session viewer // 1. Due to the hierarchical nature of the data (e.g we are rendering a time ordered pid tree) there are common scenarios where there @@ -26,6 +33,23 @@ export const KIBANA_DATE_FORMAT = 'MMM DD, YYYY @ hh:mm:ss.SSS'; // search functionality will instead use a separate ES backend search to avoid this. // 3. Fewer round trips to the backend! export const PROCESS_EVENTS_PER_PAGE = 1000; + +// As an initial approach, we won't be implementing pagination for alerts. +// Instead we will load this fixed amount of alerts as a maximum for a session. +// This could cause an edge case, where a noisy rule that alerts on every process event +// causes a session to only list and highlight up to 1000 alerts, even though there could +// be far greater than this amount. UX should be added to let the end user know this is +// happening and to revise their rule to be more specific. +export const ALERTS_PER_PAGE = 1000; + +// when showing the count of alerts in details panel tab, if the number +// exceeds ALERT_COUNT_THRESHOLD we put a + next to it, e.g 999+ +export const ALERT_COUNT_THRESHOLD = 999; + +// react-query caching keys +export const QUERY_KEY_PROCESS_EVENTS = 'sessionViewProcessEvents'; +export const QUERY_KEY_ALERTS = 'sessionViewAlerts'; + export const MOUSE_EVENT_PLACEHOLDER = { stopPropagation: () => undefined } as React.MouseEvent; export const DEBOUNCE_TIMEOUT = 500; diff --git a/x-pack/plugins/session_view/common/mocks/constants/session_view_process.mock.ts b/x-pack/plugins/session_view/common/mocks/constants/session_view_process.mock.ts index 83cd250d45691d..f9ace9fee7a756 100644 --- a/x-pack/plugins/session_view/common/mocks/constants/session_view_process.mock.ts +++ b/x-pack/plugins/session_view/common/mocks/constants/session_view_process.mock.ts @@ -920,6 +920,7 @@ export const childProcessMock: Process = { hasOutput: () => false, hasAlerts: () => false, getAlerts: () => [], + updateAlertsStatus: (_) => undefined, hasExec: () => false, getOutput: () => '', getDetails: () => @@ -998,6 +999,7 @@ export const processMock: Process = { hasOutput: () => false, hasAlerts: () => false, getAlerts: () => [], + updateAlertsStatus: (_) => undefined, hasExec: () => false, getOutput: () => '', getDetails: () => @@ -1173,6 +1175,7 @@ export const mockProcessMap = mockEvents.reduce( hasOutput: () => false, hasAlerts: () => false, getAlerts: () => [], + updateAlertsStatus: (_) => undefined, hasExec: () => false, getOutput: () => '', getDetails: () => event, diff --git a/x-pack/plugins/session_view/common/types/process_tree/index.ts b/x-pack/plugins/session_view/common/types/process_tree/index.ts index 746c1b2093661b..3475e8d4259081 100644 --- a/x-pack/plugins/session_view/common/types/process_tree/index.ts +++ b/x-pack/plugins/session_view/common/types/process_tree/index.ts @@ -5,6 +5,13 @@ * 2.0. */ +export interface AlertStatusEventEntityIdMap { + [alertUuid: string]: { + status: string; + processEntityId: string; + }; +} + export const enum EventKind { event = 'event', signal = 'signal', @@ -150,6 +157,7 @@ export interface Process { hasOutput(): boolean; hasAlerts(): boolean; getAlerts(): ProcessEvent[]; + updateAlertsStatus(updatedAlertsStatus: AlertStatusEventEntityIdMap): void; hasExec(): boolean; getOutput(): string; getDetails(): ProcessEvent; diff --git a/x-pack/plugins/session_view/public/components/process_tree/helpers.test.ts b/x-pack/plugins/session_view/public/components/process_tree/helpers.test.ts index 9092009a7d291c..39947da4714996 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/helpers.test.ts +++ b/x-pack/plugins/session_view/public/components/process_tree/helpers.test.ts @@ -4,12 +4,21 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { cloneDeep } from 'lodash'; import { - mockData, + mockEvents, + mockAlerts, mockProcessMap, } from '../../../common/mocks/constants/session_view_process.mock'; -import { Process, ProcessMap } from '../../../common/types/process_tree'; import { + AlertStatusEventEntityIdMap, + Process, + ProcessMap, + ProcessEvent, +} from '../../../common/types/process_tree'; +import { ALERT_STATUS } from '../../../common/constants'; +import { + updateAlertEventStatus, updateProcessMap, buildProcessTree, searchProcessTree, @@ -20,8 +29,6 @@ const SESSION_ENTITY_ID = '3d0192c6-7c54-5ee6-a110-3539a7cf42bc'; const SEARCH_QUERY = 'vi'; const SEARCH_RESULT_PROCESS_ID = '8e4daeb2-4a4e-56c4-980e-f0dcfdbc3727'; -const mockEvents = mockData[0].events; - describe('process tree hook helpers tests', () => { let processMap: ProcessMap; @@ -73,4 +80,42 @@ describe('process tree hook helpers tests', () => { // session leader should have autoExpand to be true expect(processMap[SESSION_ENTITY_ID].autoExpand).toBeTruthy(); }); + + it('updateAlertEventStatus works', () => { + let events: ProcessEvent[] = cloneDeep([...mockEvents, ...mockAlerts]); + const updatedAlertsStatus: AlertStatusEventEntityIdMap = { + [mockAlerts[0].kibana?.alert.uuid!]: { + status: ALERT_STATUS.CLOSED, + processEntityId: mockAlerts[0].process.entity_id, + }, + }; + + expect( + events.find( + (event) => + event.kibana?.alert.uuid && event.kibana?.alert.uuid === mockAlerts[0].kibana?.alert.uuid + )?.kibana?.alert.workflow_status + ).toEqual(ALERT_STATUS.OPEN); + expect( + events.find( + (event) => + event.kibana?.alert.uuid && event.kibana?.alert.uuid === mockAlerts[1].kibana?.alert.uuid + )?.kibana?.alert.workflow_status + ).toEqual(ALERT_STATUS.OPEN); + + events = updateAlertEventStatus(events, updatedAlertsStatus); + + expect( + events.find( + (event) => + event.kibana?.alert.uuid && event.kibana?.alert.uuid === mockAlerts[0].kibana?.alert.uuid + )?.kibana?.alert.workflow_status + ).toEqual(ALERT_STATUS.CLOSED); + expect( + events.find( + (event) => + event.kibana?.alert.uuid && event.kibana?.alert.uuid === mockAlerts[1].kibana?.alert.uuid + )?.kibana?.alert.workflow_status + ).toEqual(ALERT_STATUS.OPEN); + }); }); diff --git a/x-pack/plugins/session_view/public/components/process_tree/helpers.ts b/x-pack/plugins/session_view/public/components/process_tree/helpers.ts index d3d7af1c62eda9..df4a6cf70abec2 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/helpers.ts +++ b/x-pack/plugins/session_view/public/components/process_tree/helpers.ts @@ -4,9 +4,40 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { Process, ProcessEvent, ProcessMap } from '../../../common/types/process_tree'; +import { + AlertStatusEventEntityIdMap, + Process, + ProcessEvent, + ProcessMap, +} from '../../../common/types/process_tree'; import { ProcessImpl } from './hooks'; +// if given event is an alert, and it exist in updatedAlertsStatus, update the alert's status +// with the updated status value in updatedAlertsStatus Map +export const updateAlertEventStatus = ( + events: ProcessEvent[], + updatedAlertsStatus: AlertStatusEventEntityIdMap +) => + events.map((event) => { + // do nothing if event is not an alert + if (!event.kibana) { + return event; + } + + return { + ...event, + kibana: { + ...event.kibana, + alert: { + ...event.kibana.alert, + workflow_status: + updatedAlertsStatus[event.kibana.alert?.uuid]?.status ?? + event.kibana.alert?.workflow_status, + }, + }, + }; + }); + // given a page of new events, add these events to the appropriate process class model // create a new process if none are created and return the mutated processMap export const updateProcessMap = (processMap: ProcessMap, events: ProcessEvent[]) => { diff --git a/x-pack/plugins/session_view/public/components/process_tree/hooks.ts b/x-pack/plugins/session_view/public/components/process_tree/hooks.ts index dfd34a5d10094a..fb00344d5e280e 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/hooks.ts +++ b/x-pack/plugins/session_view/public/components/process_tree/hooks.ts @@ -8,6 +8,7 @@ import _ from 'lodash'; import memoizeOne from 'memoize-one'; import { useState, useEffect } from 'react'; import { + AlertStatusEventEntityIdMap, EventAction, EventKind, Process, @@ -15,13 +16,19 @@ import { ProcessMap, ProcessEventsPage, } from '../../../common/types/process_tree'; -import { processNewEvents, searchProcessTree, autoExpandProcessTree } from './helpers'; +import { + updateAlertEventStatus, + processNewEvents, + searchProcessTree, + autoExpandProcessTree, +} from './helpers'; import { sortProcesses } from '../../../common/utils/sort_processes'; interface UseProcessTreeDeps { sessionEntityId: string; data: ProcessEventsPage[]; searchQuery?: string; + updatedAlertsStatus: AlertStatusEventEntityIdMap; } export class ProcessImpl implements Process { @@ -103,6 +110,10 @@ export class ProcessImpl implements Process { return this.filterEventsByKind(this.events, EventKind.signal); } + updateAlertsStatus(updatedAlertsStatus: AlertStatusEventEntityIdMap) { + this.events = updateAlertEventStatus(this.events, updatedAlertsStatus); + } + hasExec() { return !!this.findEventByAction(this.events, EventAction.exec); } @@ -129,6 +140,7 @@ export class ProcessImpl implements Process { // only used to auto expand parts of the tree that could be of interest. isUserEntered() { const event = this.getDetails(); + const { pid, tty, @@ -181,7 +193,12 @@ export class ProcessImpl implements Process { }); } -export const useProcessTree = ({ sessionEntityId, data, searchQuery }: UseProcessTreeDeps) => { +export const useProcessTree = ({ + sessionEntityId, + data, + searchQuery, + updatedAlertsStatus, +}: UseProcessTreeDeps) => { // initialize map, as well as a placeholder for session leader process // we add a fake session leader event, sourced from wide event data. // this is because we might not always have a session leader event @@ -250,5 +267,13 @@ export const useProcessTree = ({ sessionEntityId, data, searchQuery }: UseProces sessionLeader.orphans = orphans; + // update alert status in processMap for alerts in updatedAlertsStatus + Object.keys(updatedAlertsStatus).forEach((alertUuid) => { + const process = processMap[updatedAlertsStatus[alertUuid].processEntityId]; + if (process) { + process.updateAlertsStatus(updatedAlertsStatus); + } + }); + return { sessionLeader: processMap[sessionEntityId], processMap, searchResults }; }; diff --git a/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx index bdaeb0cdce2b49..9fa7900d04b0dc 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx @@ -10,7 +10,7 @@ import { mockData } from '../../../common/mocks/constants/session_view_process.m import { Process } from '../../../common/types/process_tree'; import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; import { ProcessImpl } from './hooks'; -import { ProcessTree } from './index'; +import { ProcessTreeDeps, ProcessTree } from './index'; describe('ProcessTree component', () => { let render: () => ReturnType; @@ -18,6 +18,18 @@ describe('ProcessTree component', () => { let mockedContext: AppContextTestRender; const sessionLeader = mockData[0].events[0]; const sessionLeaderVerboseTest = mockData[0].events[3]; + const props: ProcessTreeDeps = { + sessionEntityId: sessionLeader.process.entity_id, + data: mockData, + isFetching: false, + fetchNextPage: jest.fn(), + hasNextPage: false, + fetchPreviousPage: jest.fn(), + hasPreviousPage: false, + onProcessSelected: jest.fn(), + updatedAlertsStatus: {}, + handleOnAlertDetailsClosed: jest.fn(), + }; beforeEach(() => { mockedContext = createAppRootMockRenderer(); @@ -25,18 +37,7 @@ describe('ProcessTree component', () => { describe('When ProcessTree is mounted', () => { it('should render given a valid sessionEntityId and data', () => { - renderResult = mockedContext.render( - true} - hasNextPage={false} - fetchPreviousPage={() => true} - hasPreviousPage={false} - onProcessSelected={jest.fn()} - /> - ); + renderResult = mockedContext.render(); expect(renderResult.queryByTestId('sessionView:sessionViewProcessTree')).toBeTruthy(); expect(renderResult.queryAllByTestId('sessionView:processTreeNode')).toBeTruthy(); }); @@ -47,17 +48,7 @@ describe('ProcessTree component', () => { expect(process?.id).toBe(jumpToEvent.process.entity_id); }); renderResult = mockedContext.render( - true} - hasNextPage={false} - fetchPreviousPage={() => true} - hasPreviousPage={false} - jumpToEvent={jumpToEvent} - onProcessSelected={onProcessSelected} - /> + ); expect(renderResult.queryByTestId('sessionView:sessionViewProcessTree')).toBeTruthy(); expect(renderResult.queryAllByTestId('sessionView:processTreeNode')).toBeTruthy(); @@ -70,16 +61,7 @@ describe('ProcessTree component', () => { expect(process?.id).toBe(sessionLeader.process.entity_id); }); renderResult = mockedContext.render( - true} - hasNextPage={false} - fetchPreviousPage={() => true} - hasPreviousPage={false} - onProcessSelected={onProcessSelected} - /> + ); expect(renderResult.queryByTestId('sessionView:sessionViewProcessTree')).toBeTruthy(); expect(renderResult.queryAllByTestId('sessionView:processTreeNode')).toBeTruthy(); @@ -88,20 +70,7 @@ describe('ProcessTree component', () => { }); it('When Verbose mode is OFF, it should not show all childrens', () => { - renderResult = mockedContext.render( - true} - hasNextPage={false} - fetchPreviousPage={() => true} - hasPreviousPage={false} - onProcessSelected={jest.fn()} - timeStampOn={true} - verboseModeOn={false} - /> - ); + renderResult = mockedContext.render(); expect(renderResult.queryByText('cat')).toBeFalsy(); const selectionArea = renderResult.queryAllByTestId('sessionView:processTreeNode'); @@ -112,20 +81,7 @@ describe('ProcessTree component', () => { }); it('When Verbose mode is ON, it should show all childrens', () => { - renderResult = mockedContext.render( - true} - hasNextPage={false} - fetchPreviousPage={() => true} - hasPreviousPage={false} - onProcessSelected={jest.fn()} - timeStampOn={true} - verboseModeOn={true} - /> - ); + renderResult = mockedContext.render(); expect(renderResult.queryByText('cat')).toBeTruthy(); const selectionArea = renderResult.queryAllByTestId('sessionView:processTreeNode'); @@ -139,18 +95,7 @@ describe('ProcessTree component', () => { const mockSelectedProcess = new ProcessImpl(mockData[0].events[0].process.entity_id); renderResult = mockedContext.render( - true} - hasNextPage={false} - fetchPreviousPage={() => true} - hasPreviousPage={false} - selectedProcess={mockSelectedProcess} - onProcessSelected={jest.fn()} - verboseModeOn={true} - /> + ); expect( @@ -162,19 +107,7 @@ describe('ProcessTree component', () => { // change the selected process const mockSelectedProcess2 = new ProcessImpl(mockData[0].events[1].process.entity_id); - renderResult.rerender( - true} - hasNextPage={false} - fetchPreviousPage={() => true} - hasPreviousPage={false} - selectedProcess={mockSelectedProcess2} - onProcessSelected={jest.fn()} - /> - ); + renderResult.rerender(); expect( renderResult diff --git a/x-pack/plugins/session_view/public/components/process_tree/index.tsx b/x-pack/plugins/session_view/public/components/process_tree/index.tsx index 06942498aa9675..4b489797c7e267 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree/index.tsx @@ -10,13 +10,18 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { ProcessTreeNode } from '../process_tree_node'; import { BackToInvestigatedAlert } from '../back_to_investigated_alert'; import { useProcessTree } from './hooks'; -import { Process, ProcessEventsPage, ProcessEvent } from '../../../common/types/process_tree'; +import { + AlertStatusEventEntityIdMap, + Process, + ProcessEventsPage, + ProcessEvent, +} from '../../../common/types/process_tree'; import { useScroll } from '../../hooks/use_scroll'; import { useStyles } from './styles'; type FetchFunction = () => void; -interface ProcessTreeDeps { +export interface ProcessTreeDeps { // process.entity_id to act as root node (typically a session (or entry session) leader). sessionEntityId: string; @@ -36,6 +41,11 @@ interface ProcessTreeDeps { selectedProcess?: Process | null; onProcessSelected: (process: Process | null) => void; setSearchResults?: (results: Process[]) => void; + + // a map for alerts with updated status and process.entity_id + updatedAlertsStatus: AlertStatusEventEntityIdMap; + loadAlertDetails?: (alertUuid: string, handleOnAlertDetailsClosed: () => void) => void; + handleOnAlertDetailsClosed: (alertUuid: string) => void; timeStampOn?: boolean; verboseModeOn?: boolean; } @@ -53,6 +63,9 @@ export const ProcessTree = ({ selectedProcess, onProcessSelected, setSearchResults, + updatedAlertsStatus, + loadAlertDetails, + handleOnAlertDetailsClosed, timeStampOn, verboseModeOn, }: ProcessTreeDeps) => { @@ -64,6 +77,7 @@ export const ProcessTree = ({ sessionEntityId, data, searchQuery, + updatedAlertsStatus, }); const scrollerRef = useRef(null); @@ -189,6 +203,8 @@ export const ProcessTree = ({ selectedProcessId={selectedProcess?.id} scrollerRef={scrollerRef} onChangeJumpToEventVisibility={onChangeJumpToEventVisibility} + loadAlertDetails={loadAlertDetails} + handleOnAlertDetailsClosed={handleOnAlertDetailsClosed} timeStampOn={timeStampOn} verboseModeOn={verboseModeOn} /> diff --git a/x-pack/plugins/session_view/public/components/process_tree_alert/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree_alert/index.test.tsx index 635ac09682eae9..2a56a0ae2be672 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_alert/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_alert/index.test.tsx @@ -8,17 +8,26 @@ import React from 'react'; import { mockAlerts } from '../../../common/mocks/constants/session_view_process.mock'; import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; -import { ProcessTreeAlert } from './index'; +import { ProcessTreeAlertDeps, ProcessTreeAlert } from './index'; const mockAlert = mockAlerts[0]; const TEST_ID = `sessionView:sessionViewAlertDetail-${mockAlert.kibana?.alert.uuid}`; const ALERT_RULE_NAME = mockAlert.kibana?.alert.rule.name; const ALERT_STATUS = mockAlert.kibana?.alert.workflow_status; +const EXPAND_BUTTON_TEST_ID = `sessionView:sessionViewAlertDetailExpand-${mockAlert.kibana?.alert.uuid}`; describe('ProcessTreeAlerts component', () => { let render: () => ReturnType; let renderResult: ReturnType; let mockedContext: AppContextTestRender; + const props: ProcessTreeAlertDeps = { + alert: mockAlert, + isInvestigated: false, + isSelected: false, + onClick: jest.fn(), + selectAlert: jest.fn(), + handleOnAlertDetailsClosed: jest.fn(), + }; beforeEach(() => { mockedContext = createAppRootMockRenderer(); @@ -26,15 +35,7 @@ describe('ProcessTreeAlerts component', () => { describe('When ProcessTreeAlert is mounted', () => { it('should render alert row correctly', async () => { - renderResult = mockedContext.render( - - ); + renderResult = mockedContext.render(); expect(renderResult.queryByTestId(TEST_ID)).toBeTruthy(); expect(renderResult.queryByText(ALERT_RULE_NAME!)).toBeTruthy(); @@ -42,21 +43,34 @@ describe('ProcessTreeAlerts component', () => { }); it('should execute onClick callback', async () => { - const mockFn = jest.fn(); - renderResult = mockedContext.render( - - ); + const onClick = jest.fn(); + renderResult = mockedContext.render(); const alertRow = renderResult.queryByTestId(TEST_ID); expect(alertRow).toBeTruthy(); alertRow?.click(); - expect(mockFn).toHaveBeenCalledTimes(1); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('should automatically call selectAlert when isInvestigated is true', async () => { + const selectAlert = jest.fn(); + renderResult = mockedContext.render( + + ); + + expect(selectAlert).toHaveBeenCalledTimes(1); + }); + + it('should execute loadAlertDetails callback when clicking on expand button', async () => { + const loadAlertDetails = jest.fn(); + renderResult = mockedContext.render( + + ); + + const expandButton = renderResult.queryByTestId(EXPAND_BUTTON_TEST_ID); + expect(expandButton).toBeTruthy(); + expandButton?.click(); + expect(loadAlertDetails).toHaveBeenCalledTimes(1); }); }); }); diff --git a/x-pack/plugins/session_view/public/components/process_tree_alert/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_alert/index.tsx index d0d4c842525132..5ec1c4a7693c34 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_alert/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_alert/index.tsx @@ -5,18 +5,20 @@ * 2.0. */ -import React, { useEffect } from 'react'; +import React, { useEffect, useCallback } from 'react'; import { EuiBadge, EuiIcon, EuiText, EuiButtonIcon } from '@elastic/eui'; import { ProcessEvent, ProcessEventAlert } from '../../../common/types/process_tree'; import { getBadgeColorFromAlertStatus } from './helpers'; import { useStyles } from './styles'; -interface ProcessTreeAlertDeps { +export interface ProcessTreeAlertDeps { alert: ProcessEvent; isInvestigated: boolean; isSelected: boolean; onClick: (alert: ProcessEventAlert | null) => void; selectAlert: (alertUuid: string) => void; + loadAlertDetails?: (alertUuid: string, handleOnAlertDetailsClosed: () => void) => void; + handleOnAlertDetailsClosed: (alertUuid: string, status?: string) => void; } export const ProcessTreeAlert = ({ @@ -25,16 +27,30 @@ export const ProcessTreeAlert = ({ isSelected, onClick, selectAlert, + loadAlertDetails, + handleOnAlertDetailsClosed, }: ProcessTreeAlertDeps) => { const styles = useStyles({ isInvestigated, isSelected }); const { uuid, rule, workflow_status: status } = alert.kibana?.alert || {}; useEffect(() => { - if (isInvestigated && isSelected && uuid) { + if (isInvestigated && uuid) { selectAlert(uuid); } - }, [isInvestigated, isSelected, uuid, selectAlert]); + }, [isInvestigated, uuid, selectAlert]); + + const handleExpandClick = useCallback(() => { + if (loadAlertDetails && uuid) { + loadAlertDetails(uuid, () => handleOnAlertDetailsClosed(uuid)); + } + }, [handleOnAlertDetailsClosed, loadAlertDetails, uuid]); + + const handleClick = useCallback(() => { + if (alert.kibana?.alert) { + onClick(alert.kibana.alert); + } + }, [alert.kibana?.alert, onClick]); if (!(alert.kibana && rule)) { return null; @@ -42,10 +58,6 @@ export const ProcessTreeAlert = ({ const { name } = rule; - const handleClick = () => { - onClick(alert.kibana?.alert ?? null); - }; - return ( - + {name} diff --git a/x-pack/plugins/session_view/public/components/process_tree_alerts/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.test.tsx index c4dbaf817cff2e..2333c71d36a510 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_alerts/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.test.tsx @@ -8,12 +8,17 @@ import React from 'react'; import { mockAlerts } from '../../../common/mocks/constants/session_view_process.mock'; import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; -import { ProcessTreeAlerts } from './index'; +import { ProcessTreeAlertsDeps, ProcessTreeAlerts } from './index'; describe('ProcessTreeAlerts component', () => { let render: () => ReturnType; let renderResult: ReturnType; let mockedContext: AppContextTestRender; + const props: ProcessTreeAlertsDeps = { + alerts: mockAlerts, + onAlertSelected: jest.fn(), + handleOnAlertDetailsClosed: jest.fn(), + }; beforeEach(() => { mockedContext = createAppRootMockRenderer(); @@ -21,17 +26,13 @@ describe('ProcessTreeAlerts component', () => { describe('When ProcessTreeAlerts is mounted', () => { it('should return null if no alerts', async () => { - renderResult = mockedContext.render( - - ); + renderResult = mockedContext.render(); expect(renderResult.queryByTestId('sessionView:sessionViewAlertDetails')).toBeNull(); }); it('should return an array of alert details', async () => { - renderResult = mockedContext.render( - - ); + renderResult = mockedContext.render(); expect(renderResult.queryByTestId('sessionView:sessionViewAlertDetails')).toBeTruthy(); mockAlerts.forEach((alert) => { @@ -49,7 +50,7 @@ describe('ProcessTreeAlerts component', () => { it('should execute onAlertSelected when clicking on an alert', async () => { const mockFn = jest.fn(); renderResult = mockedContext.render( - + ); expect(renderResult.queryByTestId('sessionView:sessionViewAlertDetails')).toBeTruthy(); diff --git a/x-pack/plugins/session_view/public/components/process_tree_alerts/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.tsx index dcca29dcf4f84b..c97ccfe253605b 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_alerts/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.tsx @@ -11,11 +11,13 @@ import { ProcessEvent, ProcessEventAlert } from '../../../common/types/process_t import { ProcessTreeAlert } from '../process_tree_alert'; import { MOUSE_EVENT_PLACEHOLDER } from '../../../common/constants'; -interface ProcessTreeAlertsDeps { +export interface ProcessTreeAlertsDeps { alerts: ProcessEvent[]; jumpToAlertID?: string; isProcessSelected?: boolean; onAlertSelected: (e: MouseEvent) => void; + loadAlertDetails?: (alertUuid: string, handleOnAlertDetailsClosed: () => void) => void; + handleOnAlertDetailsClosed: (alertUuid: string) => void; } export function ProcessTreeAlerts({ @@ -23,6 +25,8 @@ export function ProcessTreeAlerts({ jumpToAlertID, isProcessSelected = false, onAlertSelected, + loadAlertDetails, + handleOnAlertDetailsClosed, }: ProcessTreeAlertsDeps) { const [selectedAlert, setSelectedAlert] = useState(null); const styles = useStyles(); @@ -57,15 +61,18 @@ export function ProcessTreeAlerts({ } }, []); + const handleAlertClick = useCallback( + (alert: ProcessEventAlert | null) => { + onAlertSelected(MOUSE_EVENT_PLACEHOLDER); + setSelectedAlert(alert); + }, + [onAlertSelected] + ); + if (alerts.length === 0) { return null; } - const handleAlertClick = (alert: ProcessEventAlert | null) => { - onAlertSelected(MOUSE_EVENT_PLACEHOLDER); - setSelectedAlert(alert); - }; - return (
); })} diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx index 0791f21e81846d..2e82e822f0c82c 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx @@ -36,6 +36,7 @@ describe('ProcessTreeNode component', () => { }, } as unknown as RefObject, onChangeJumpToEventVisibility: jest.fn(), + handleOnAlertDetailsClosed: (_alertUuid: string) => {}, }; beforeEach(() => { diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx index bc2eb4706c73dc..b1c42dd95efb91 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx @@ -43,6 +43,8 @@ export interface ProcessDeps { verboseModeOn?: boolean; scrollerRef: RefObject; onChangeJumpToEventVisibility: (isVisible: boolean, isAbove: boolean) => void; + loadAlertDetails?: (alertUuid: string, handleOnAlertDetailsClosed: () => void) => void; + handleOnAlertDetailsClosed: (alertUuid: string) => void; } /** @@ -60,6 +62,8 @@ export function ProcessTreeNode({ verboseModeOn = true, scrollerRef, onChangeJumpToEventVisibility, + loadAlertDetails, + handleOnAlertDetailsClosed, }: ProcessDeps) { const textRef = useRef(null); @@ -123,18 +127,21 @@ export function ProcessTreeNode({ setAlertsExpanded(!alertsExpanded); }, [alertsExpanded]); - const onProcessClicked = (e: MouseEvent) => { - e.stopPropagation(); + const onProcessClicked = useCallback( + (e: MouseEvent) => { + e.stopPropagation(); - const selection = window.getSelection(); + const selection = window.getSelection(); - // do not select the command if the user was just selecting text for copy. - if (selection && selection.type === 'Range') { - return; - } + // do not select the command if the user was just selecting text for copy. + if (selection && selection.type === 'Range') { + return; + } - onProcessSelected?.(process); - }; + onProcessSelected?.(process); + }, + [onProcessSelected, process] + ); const processDetails = process.getDetails(); @@ -248,6 +255,8 @@ export function ProcessTreeNode({ jumpToAlertID={jumpToAlertID} isProcessSelected={selectedProcessId === process.id} onAlertSelected={onProcessClicked} + loadAlertDetails={loadAlertDetails} + handleOnAlertDetailsClosed={handleOnAlertDetailsClosed} /> )} @@ -267,6 +276,8 @@ export function ProcessTreeNode({ verboseModeOn={verboseModeOn} scrollerRef={scrollerRef} onChangeJumpToEventVisibility={onChangeJumpToEventVisibility} + loadAlertDetails={loadAlertDetails} + handleOnAlertDetailsClosed={handleOnAlertDetailsClosed} /> ); })} diff --git a/x-pack/plugins/session_view/public/components/session_view/hooks.ts b/x-pack/plugins/session_view/public/components/session_view/hooks.ts index 17574cfd28074a..a134a366c41685 100644 --- a/x-pack/plugins/session_view/public/components/session_view/hooks.ts +++ b/x-pack/plugins/session_view/public/components/session_view/hooks.ts @@ -5,12 +5,21 @@ * 2.0. */ import { useEffect, useState } from 'react'; -import { useInfiniteQuery } from 'react-query'; +import { useQuery, useInfiniteQuery } from 'react-query'; import { EuiSearchBarOnChangeArgs } from '@elastic/eui'; import { CoreStart } from 'kibana/public'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { ProcessEvent, ProcessEventResults } from '../../../common/types/process_tree'; -import { PROCESS_EVENTS_ROUTE, PROCESS_EVENTS_PER_PAGE } from '../../../common/constants'; +import { + AlertStatusEventEntityIdMap, + ProcessEvent, + ProcessEventResults, +} from '../../../common/types/process_tree'; +import { + PROCESS_EVENTS_ROUTE, + PROCESS_EVENTS_PER_PAGE, + ALERT_STATUS_ROUTE, + QUERY_KEY_ALERTS, +} from '../../../common/constants'; export const useFetchSessionViewProcessEvents = ( sessionEntityId: string, @@ -75,6 +84,46 @@ export const useFetchSessionViewProcessEvents = ( return query; }; +export const useFetchAlertStatus = ( + updatedAlertsStatus: AlertStatusEventEntityIdMap, + alertUuid: string +) => { + const { http } = useKibana().services; + const cachingKeys = [QUERY_KEY_ALERTS, alertUuid]; + const query = useQuery( + cachingKeys, + async () => { + if (!alertUuid) { + return updatedAlertsStatus; + } + + const res = await http.get(ALERT_STATUS_ROUTE, { + query: { + alertUuid, + }, + }); + + // TODO: add error handling + const events = res.events.map((event: any) => event._source as ProcessEvent); + + return { + ...updatedAlertsStatus, + [alertUuid]: { + status: events[0]?.kibana?.alert.workflow_status ?? '', + processEntityId: events[0]?.process?.entity_id ?? '', + }, + }; + }, + { + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchOnReconnect: false, + } + ); + + return query; +}; + export const useSearchQuery = () => { const [searchQuery, setSearchQuery] = useState(''); const onSearch = ({ query }: EuiSearchBarOnChangeArgs) => { diff --git a/x-pack/plugins/session_view/public/components/session_view/index.tsx b/x-pack/plugins/session_view/public/components/session_view/index.tsx index 4b8881a88d7b09..af4eb6114a0a26 100644 --- a/x-pack/plugins/session_view/public/components/session_view/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view/index.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useEffect } from 'react'; import { EuiEmptyPrompt, EuiButton, @@ -16,34 +16,40 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { SectionLoading } from '../../shared_imports'; import { ProcessTree } from '../process_tree'; -import { Process } from '../../../common/types/process_tree'; +import { AlertStatusEventEntityIdMap, Process } from '../../../common/types/process_tree'; import { DisplayOptionsState } from '../../../common/types/session_view'; import { SessionViewDeps } from '../../types'; import { SessionViewDetailPanel } from '../session_view_detail_panel'; import { SessionViewSearchBar } from '../session_view_search_bar'; import { SessionViewDisplayOptions } from '../session_view_display_options'; import { useStyles } from './styles'; -import { useFetchSessionViewProcessEvents } from './hooks'; +import { useFetchAlertStatus, useFetchSessionViewProcessEvents } from './hooks'; /** * The main wrapper component for the session view. */ -export const SessionView = ({ sessionEntityId, height, jumpToEvent }: SessionViewDeps) => { +export const SessionView = ({ + sessionEntityId, + height, + jumpToEvent, + loadAlertDetails, +}: SessionViewDeps) => { const [isDetailOpen, setIsDetailOpen] = useState(false); const [selectedProcess, setSelectedProcess] = useState(null); - - const styles = useStyles({ height }); - - const onProcessSelected = useCallback((process: Process | null) => { - setSelectedProcess(process); - }, []); - const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState(null); const [displayOptions, setDisplayOptions] = useState({ timestamp: true, verboseMode: true, }); + const [fetchAlertStatus, setFetchAlertStatus] = useState([]); + const [updatedAlertsStatus, setUpdatedAlertsStatus] = useState({}); + + const styles = useStyles({ height }); + + const onProcessSelected = useCallback((process: Process | null) => { + setSelectedProcess(process); + }, []); const { data, @@ -58,10 +64,25 @@ export const SessionView = ({ sessionEntityId, height, jumpToEvent }: SessionVie const hasData = data && data.pages.length > 0 && data.pages[0].events.length > 0; const renderIsLoading = isFetching && !data; const renderDetails = isDetailOpen && selectedProcess; + const { data: newUpdatedAlertsStatus } = useFetchAlertStatus( + updatedAlertsStatus, + fetchAlertStatus[0] ?? '' + ); + + useEffect(() => { + if (fetchAlertStatus) { + setUpdatedAlertsStatus({ ...newUpdatedAlertsStatus }); + } + }, [fetchAlertStatus, newUpdatedAlertsStatus]); + + const handleOnAlertDetailsClosed = useCallback((alertUuid: string) => { + setFetchAlertStatus([alertUuid]); + }, []); const toggleDetailPanel = useCallback(() => { setIsDetailOpen(!isDetailOpen); }, [isDetailOpen]); + const handleOptionChange = useCallback((checkedOptions: DisplayOptionsState) => { setDisplayOptions(checkedOptions); }, []); @@ -182,6 +203,9 @@ export const SessionView = ({ sessionEntityId, height, jumpToEvent }: SessionVie fetchNextPage={fetchNextPage} fetchPreviousPage={fetchPreviousPage} setSearchResults={setSearchResults} + updatedAlertsStatus={updatedAlertsStatus} + loadAlertDetails={loadAlertDetails} + handleOnAlertDetailsClosed={handleOnAlertDetailsClosed} timeStampOn={displayOptions.timestamp} verboseModeOn={displayOptions.verboseMode} /> diff --git a/x-pack/plugins/session_view/public/methods/index.tsx b/x-pack/plugins/session_view/public/methods/index.tsx index 1eecdcbb3e50ee..3654e296e74129 100644 --- a/x-pack/plugins/session_view/public/methods/index.tsx +++ b/x-pack/plugins/session_view/public/methods/index.tsx @@ -15,15 +15,11 @@ const queryClient = new QueryClient(); const SessionViewLazy = lazy(() => import('../components/session_view')); -export const getSessionViewLazy = ({ sessionEntityId, height, jumpToEvent }: SessionViewDeps) => { +export const getSessionViewLazy = (props: SessionViewDeps) => { return ( }> - + ); diff --git a/x-pack/plugins/session_view/public/types.ts b/x-pack/plugins/session_view/public/types.ts index d84623af7c0ede..3a7ef376bd4263 100644 --- a/x-pack/plugins/session_view/public/types.ts +++ b/x-pack/plugins/session_view/public/types.ts @@ -20,6 +20,12 @@ export interface SessionViewDeps { // if provided, the session view will jump to and select the provided event if it belongs to the session leader // session view will fetch a page worth of events starting from jumpToEvent as well as a page backwards. jumpToEvent?: ProcessEvent; + // Callback to open the alerts flyout + loadAlertDetails?: ( + alertUuid: string, + // Callback used when alert flyout panel is closed + handleOnAlertDetailsClosed: () => void + ) => void; } export interface EuiTabProps { diff --git a/x-pack/plugins/session_view/server/routes/alert_status_route.ts b/x-pack/plugins/session_view/server/routes/alert_status_route.ts new file mode 100644 index 00000000000000..70ce32ee720208 --- /dev/null +++ b/x-pack/plugins/session_view/server/routes/alert_status_route.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { schema } from '@kbn/config-schema'; +import type { ElasticsearchClient } from 'kibana/server'; +import { IRouter } from '../../../../../src/core/server'; +import { ALERT_STATUS_ROUTE, ALERTS_INDEX, ALERT_UUID_PROPERTY } from '../../common/constants'; +import { expandDottedObject } from '../../common/utils/expand_dotted_object'; + +export const registerAlertStatusRoute = (router: IRouter) => { + router.get( + { + path: ALERT_STATUS_ROUTE, + validate: { + query: schema.object({ + alertUuid: schema.string(), + }), + }, + }, + async (context, request, response) => { + const client = context.core.elasticsearch.client.asCurrentUser; + const { alertUuid } = request.query; + const body = await searchAlertByUuid(client, alertUuid); + + return response.ok({ body }); + } + ); +}; + +export const searchAlertByUuid = async (client: ElasticsearchClient, alertUuid: string) => { + const search = await client.search({ + index: [ALERTS_INDEX], + ignore_unavailable: true, // on a new installation the .siem-signals-default index might not be created yet. + body: { + query: { + match: { + [ALERT_UUID_PROPERTY]: alertUuid, + }, + }, + size: 1, + }, + }); + + const events = search.hits.hits.map((hit: any) => { + // TODO: re-eval if this is needed after updated ECS mappings are applied. + // the .siem-signals-default index flattens many properties. this util unflattens them. + hit._source = expandDottedObject(hit._source); + + return hit; + }); + + return { events }; +}; diff --git a/x-pack/plugins/session_view/server/routes/index.ts b/x-pack/plugins/session_view/server/routes/index.ts index 7b9cfb45f580b7..b8cb80dc1d1d47 100644 --- a/x-pack/plugins/session_view/server/routes/index.ts +++ b/x-pack/plugins/session_view/server/routes/index.ts @@ -6,9 +6,11 @@ */ import { IRouter } from '../../../../../src/core/server'; import { registerProcessEventsRoute } from './process_events_route'; +import { registerAlertStatusRoute } from './alert_status_route'; import { sessionEntryLeadersRoute } from './session_entry_leaders_route'; export const registerRoutes = (router: IRouter) => { registerProcessEventsRoute(router); + registerAlertStatusRoute(router); sessionEntryLeadersRoute(router); }; From 03ce76bb70dcc916f4ccffa017f6e06dc0a10549 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 22 Mar 2022 16:09:27 -0500 Subject: [PATCH 21/64] [ci] Add artifact build pipeline (#128311) * [ci] Add artifact build pipeline * create dependencies report --- .buildkite/pipelines/artifacts.yml | 6 ++++++ .buildkite/scripts/steps/artifacts/build.sh | 11 +++++++++++ 2 files changed, 17 insertions(+) create mode 100644 .buildkite/pipelines/artifacts.yml create mode 100644 .buildkite/scripts/steps/artifacts/build.sh diff --git a/.buildkite/pipelines/artifacts.yml b/.buildkite/pipelines/artifacts.yml new file mode 100644 index 00000000000000..9ec56ff44c63e8 --- /dev/null +++ b/.buildkite/pipelines/artifacts.yml @@ -0,0 +1,6 @@ +steps: + - command: .buildkite/scripts/steps/artifacts/build.sh + label: Build Kibana Artifacts + agents: + queue: c2-16 + timeout_in_minutes: 60 diff --git a/.buildkite/scripts/steps/artifacts/build.sh b/.buildkite/scripts/steps/artifacts/build.sh new file mode 100644 index 00000000000000..211bfddecd010f --- /dev/null +++ b/.buildkite/scripts/steps/artifacts/build.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -euo pipefail + +.buildkite/scripts/bootstrap.sh + +echo "--- Build Kibana Distribution" +node scripts/build --all-platforms --debug --skip-docker-cloud + +echo "--- Build dependencies report" +node scripts/licenses_csv_report --csv=target/dependencies_report.csv From 4cd7f879a8d6aa2bcb581ea802bf5fd2c625a2d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=BCrkan=20YALAMAN?= Date: Tue, 22 Mar 2022 22:09:55 +0100 Subject: [PATCH 22/64] Bind create engine button to the backend (#128253) --- .../__mocks__/engine_creation_logic.mock.ts | 87 +++++++++ .../engine_creation_logic.test.ts | 180 ++++++++---------- .../engine_creation/engine_creation_logic.ts | 19 +- .../server/routes/app_search/engines.test.ts | 62 ++++++ .../server/routes/app_search/engines.ts | 18 ++ 5 files changed, 258 insertions(+), 108 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_creation_logic.mock.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_creation_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_creation_logic.mock.ts new file mode 100644 index 00000000000000..b78b936de127bf --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_creation_logic.mock.ts @@ -0,0 +1,87 @@ +/* + * 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 { SearchIndexSelectableOption } from '../components/engine_creation/search_index_selectable'; + +export const DEFAULT_VALUES = { + ingestionMethod: '', + isLoading: false, + name: '', + rawName: '', + language: 'Universal', + isLoadingIndices: false, + indices: [], + indicesFormatted: [], + selectedIndex: '', + engineType: 'appSearch', + isSubmitDisabled: true, +}; + +export const mockElasticsearchIndices = [ + { + health: 'yellow', + status: 'open', + name: 'search-my-index-1', + uuid: 'ydlR_QQJTeyZP66tzQSmMQ', + total: { + docs: { + count: 0, + deleted: 0, + }, + store: { + size_in_bytes: '225b', + }, + }, + }, + { + health: 'green', + status: 'open', + name: 'search-my-index-2', + uuid: '4dlR_QQJTe2ZP6qtzQSmMQ', + total: { + docs: { + count: 100, + deleted: 0, + }, + store: { + size_in_bytes: '225b', + }, + }, + aliases: ['search-index-123'], + }, +]; + +export const mockSearchIndexOptions: SearchIndexSelectableOption[] = [ + { + label: 'search-my-index-1', + health: 'yellow', + status: 'open', + total: { + docs: { + count: 0, + deleted: 0, + }, + store: { + size_in_bytes: '225b', + }, + }, + }, + { + label: 'search-my-index-2', + health: 'green', + status: 'open', + total: { + docs: { + count: 100, + deleted: 0, + }, + store: { + size_in_bytes: '225b', + }, + }, + }, +]; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.test.ts index 2232d471893b60..ec60bf5ae8a8e1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.test.ts @@ -12,10 +12,15 @@ import { mockFlashMessageHelpers, } from '../../../__mocks__/kea_logic'; +import { + DEFAULT_VALUES, + mockElasticsearchIndices, + mockSearchIndexOptions, +} from '../../__mocks__/engine_creation_logic.mock'; + import { nextTick } from '@kbn/test-jest-helpers'; import { EngineCreationLogic } from './engine_creation_logic'; -import { SearchIndexSelectableOption } from './search_index_selectable'; describe('EngineCreationLogic', () => { const { mount } = new LogicMounter(EngineCreationLogic); @@ -23,85 +28,6 @@ describe('EngineCreationLogic', () => { const { navigateToUrl } = mockKibanaValues; const { flashSuccessToast, flashAPIErrors } = mockFlashMessageHelpers; - const DEFAULT_VALUES = { - ingestionMethod: '', - isLoading: false, - name: '', - rawName: '', - language: 'Universal', - isLoadingIndices: false, - indices: [], - indicesFormatted: [], - selectedIndex: '', - engineType: 'appSearch', - isSubmitDisabled: true, - }; - - const mockElasticsearchIndices = [ - { - health: 'yellow', - status: 'open', - name: 'search-my-index-1', - uuid: 'ydlR_QQJTeyZP66tzQSmMQ', - total: { - docs: { - count: 0, - deleted: 0, - }, - store: { - size_in_bytes: '225b', - }, - }, - }, - { - health: 'green', - status: 'open', - name: 'search-my-index-2', - uuid: '4dlR_QQJTe2ZP6qtzQSmMQ', - total: { - docs: { - count: 100, - deleted: 0, - }, - store: { - size_in_bytes: '225b', - }, - }, - aliases: ['search-index-123'], - }, - ]; - - const mockSearchIndexOptions: SearchIndexSelectableOption[] = [ - { - label: 'search-my-index-1', - health: 'yellow', - status: 'open', - total: { - docs: { - count: 0, - deleted: 0, - }, - store: { - size_in_bytes: '225b', - }, - }, - }, - { - label: 'search-my-index-2', - health: 'green', - status: 'open', - total: { - docs: { - count: 100, - deleted: 0, - }, - store: { - size_in_bytes: '225b', - }, - }, - }, - ]; - it('has expected default values', () => { mount(); expect(EngineCreationLogic.values).toEqual(DEFAULT_VALUES); @@ -333,36 +259,82 @@ describe('EngineCreationLogic', () => { }); describe('submitEngine', () => { - beforeAll(() => { - mount({ language: 'English', rawName: 'test' }); - }); + describe('Indexed engine', () => { + beforeAll(() => { + mount({ language: 'English', rawName: 'test' }); + }); - afterAll(() => { - jest.clearAllMocks(); - }); + afterAll(() => { + jest.clearAllMocks(); + }); - it('POSTS to /internal/app_search/engines', () => { - const body = JSON.stringify({ - name: EngineCreationLogic.values.name, - language: EngineCreationLogic.values.language, + it('POSTS to /internal/app_search/engines', () => { + const body = JSON.stringify({ + name: EngineCreationLogic.values.name, + language: EngineCreationLogic.values.language, + }); + EngineCreationLogic.actions.submitEngine(); + expect(http.post).toHaveBeenCalledWith('/internal/app_search/engines', { body }); }); - EngineCreationLogic.actions.submitEngine(); - expect(http.post).toHaveBeenCalledWith('/internal/app_search/engines', { body }); - }); - it('calls onEngineCreationSuccess on valid submission', async () => { - jest.spyOn(EngineCreationLogic.actions, 'onEngineCreationSuccess'); - http.post.mockReturnValueOnce(Promise.resolve({})); - EngineCreationLogic.actions.submitEngine(); - await nextTick(); - expect(EngineCreationLogic.actions.onEngineCreationSuccess).toHaveBeenCalledTimes(1); + it('calls onEngineCreationSuccess on valid submission', async () => { + jest.spyOn(EngineCreationLogic.actions, 'onEngineCreationSuccess'); + http.post.mockReturnValueOnce(Promise.resolve({})); + EngineCreationLogic.actions.submitEngine(); + await nextTick(); + expect(EngineCreationLogic.actions.onEngineCreationSuccess).toHaveBeenCalledTimes(1); + }); + + it('calls flashAPIErrors on API Error', async () => { + http.post.mockReturnValueOnce(Promise.reject()); + EngineCreationLogic.actions.submitEngine(); + await nextTick(); + expect(flashAPIErrors).toHaveBeenCalledTimes(1); + }); }); - it('calls flashAPIErrors on API Error', async () => { - http.post.mockReturnValueOnce(Promise.reject()); - EngineCreationLogic.actions.submitEngine(); - await nextTick(); - expect(flashAPIErrors).toHaveBeenCalledTimes(1); + describe('Elasticsearch index based engine', () => { + beforeEach(() => { + mount({ + engineType: 'elasticsearch', + name: 'engine-name', + selectedIndex: 'search-selected-index', + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('POSTS to /internal/app_search/elasticsearch/engines', () => { + const body = JSON.stringify({ + name: EngineCreationLogic.values.name, + search_index: { + type: 'elasticsearch', + index_name: EngineCreationLogic.values.selectedIndex, + }, + }); + EngineCreationLogic.actions.submitEngine(); + + expect(http.post).toHaveBeenCalledWith('/internal/app_search/elasticsearch/engines', { + body, + }); + }); + + it('calls onEngineCreationSuccess on valid submission', async () => { + jest.spyOn(EngineCreationLogic.actions, 'onEngineCreationSuccess'); + http.post.mockReturnValueOnce(Promise.resolve({})); + EngineCreationLogic.actions.submitEngine(); + await nextTick(); + expect(EngineCreationLogic.actions.onEngineCreationSuccess).toHaveBeenCalledTimes(1); + }); + + it('calls flashAPIErrors on API Error', async () => { + http.post.mockReturnValueOnce(Promise.reject()); + EngineCreationLogic.actions.submitEngine(); + await nextTick(); + expect(flashAPIErrors).toHaveBeenCalledTimes(1); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.ts index f972993b32ca49..2bc7f1f977129a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.ts @@ -130,12 +130,23 @@ export const EngineCreationLogic = kea ({ submitEngine: async () => { const { http } = HttpLogic.values; - const { name, language } = values; - - const body = JSON.stringify({ name, language }); + const { name, language, engineType, selectedIndex } = values; try { - await http.post('/internal/app_search/engines', { body }); + if (engineType === 'appSearch') { + const body = JSON.stringify({ name, language }); + + await http.post('/internal/app_search/engines', { body }); + } else { + const body = JSON.stringify({ + name, + search_index: { + type: 'elasticsearch', + index_name: selectedIndex, + }, + }); + await http.post('/internal/app_search/elasticsearch/engines', { body }); + } actions.onEngineCreationSuccess(); } catch (e) { flashAPIErrors(e); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts index 7c8a611cebb3ea..cd1221ec52b806 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts @@ -176,6 +176,68 @@ describe('engine routes', () => { }); }); + describe('POST /internal/app_search/elasticsearch/engines', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/internal/app_search/elasticsearch/engines', + }); + + registerEnginesRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + mockRouter.callRoute({ + body: { + name: 'some-elasticindexed-engine', + search_index: { type: 'elasticsearch', index_name: 'search-elastic-index' }, + }, + }); + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines', + }); + }); + + describe('validates', () => { + describe('indexed engines', () => { + it('correctly', () => { + const request = { + body: { + name: 'some-engine', + search_index: { type: 'elasticsearch', index_name: 'search-elastic-index' }, + }, + }; + mockRouter.shouldValidate(request); + }); + + it('missing name', () => { + const request = { + body: { + search_index: { type: 'elasticsearch', index_name: 'search-elastic-index' }, + }, + }; + mockRouter.shouldThrow(request); + }); + + it('missing index_name', () => { + const request = { + name: 'some-engine', + body: { + search_index: { type: 'elasticsearch' }, + }, + }; + mockRouter.shouldThrow(request); + }); + }); + }); + }); + describe('GET /internal/app_search/engines/{name}', () => { let mockRouter: MockRouter; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts index a53379ef44c67c..99314d6da8112a 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts @@ -55,6 +55,24 @@ export function registerEnginesRoutes({ }) ); + router.post( + { + path: '/internal/app_search/elasticsearch/engines', + validate: { + body: schema.object({ + name: schema.string(), + search_index: schema.object({ + type: schema.string(), + index_name: schema.string(), + }), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines', + }) + ); + // Single engine endpoints router.get( { From a28b25a9b9d14110ba91e467c4efacd26aec0a23 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 22 Mar 2022 14:55:37 -0700 Subject: [PATCH 23/64] [reporting/upgrade/tests] rewrite waitForJobToFinish to handle errors (#128309) --- .../lib/config/schema.ts | 5 ++- .../apps/reporting/reporting_smoke_tests.ts | 8 ++-- x-pack/test/upgrade/reporting_services.ts | 39 ++++++++++--------- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index cf1afbb810c713..17c3af046f92f0 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -114,6 +114,7 @@ export const schema = Joi.object() try: Joi.number().default(120000), waitFor: Joi.number().default(20000), esRequestTimeout: Joi.number().default(30000), + kibanaReportCompletion: Joi.number().default(60_000), kibanaStabilize: Joi.number().default(15000), navigateStatusPageCheck: Joi.number().default(250), @@ -166,7 +167,9 @@ export const schema = Joi.object() mochaReporter: Joi.object() .keys({ - captureLogOutput: Joi.boolean().default(!!process.env.CI), + captureLogOutput: Joi.boolean().default( + !!process.env.CI && !process.env.DISABLE_CI_LOG_OUTPUT_CAPTURE + ), sendToCiStats: Joi.boolean().default(!!process.env.CI), }) .default(), diff --git a/x-pack/test/upgrade/apps/reporting/reporting_smoke_tests.ts b/x-pack/test/upgrade/apps/reporting/reporting_smoke_tests.ts index e7769f2761f3f6..14136b23abfd53 100644 --- a/x-pack/test/upgrade/apps/reporting/reporting_smoke_tests.ts +++ b/x-pack/test/upgrade/apps/reporting/reporting_smoke_tests.ts @@ -89,11 +89,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const postUrl = await find.byXPath(`//button[descendant::*[text()='Copy POST URL']]`); await postUrl.click(); const url = await browser.getClipboardValue(); - await reportingAPI.expectAllJobsToFinishSuccessfully( - await Promise.all([ - reportingAPI.postJob(parse(url).pathname + '?' + parse(url).query), - ]) - ); + await reportingAPI.expectAllJobsToFinishSuccessfully([ + await reportingAPI.postJob(parse(url).pathname + '?' + parse(url).query), + ]); usage = (await usageAPI.getUsageStats()) as UsageStats; reportingAPI.expectCompletedReportCount(usage, completedReportCount + 1); }); diff --git a/x-pack/test/upgrade/reporting_services.ts b/x-pack/test/upgrade/reporting_services.ts index 13186cb9b2a755..2de3b72bc9a47c 100644 --- a/x-pack/test/upgrade/reporting_services.ts +++ b/x-pack/test/upgrade/reporting_services.ts @@ -47,33 +47,34 @@ export function ReportingAPIProvider({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esSupertest = getService('esSupertest'); const retry = getService('retry'); + const config = getService('config'); return { - async waitForJobToFinish(downloadReportPath: string) { - log.debug(`Waiting for job to finish: ${downloadReportPath}`); - const JOB_IS_PENDING_CODE = 503; - - const statusCode = await new Promise((resolve) => { - const intervalId = setInterval(async () => { - const response = (await supertest + async waitForJobToFinish(downloadReportPath: string, options?: { timeout?: number }) { + await retry.waitForWithTimeout( + `job ${downloadReportPath} finished`, + options?.timeout ?? config.get('timeouts.kibanaReportCompletion'), + async () => { + const response = await supertest .get(downloadReportPath) .responseType('blob') - .set('kbn-xsrf', 'xxx')) as any; - if (response.statusCode === 503) { + .set('kbn-xsrf', 'xxx'); + + if (response.status === 503) { log.debug(`Report at path ${downloadReportPath} is pending`); - } else if (response.statusCode === 200) { - log.debug(`Report at path ${downloadReportPath} is complete`); - } else { - log.debug(`Report at path ${downloadReportPath} returned code ${response.statusCode}`); + return false; } - if (response.statusCode !== JOB_IS_PENDING_CODE) { - clearInterval(intervalId); - resolve(response.statusCode); + + log.debug(`Report at path ${downloadReportPath} returned code ${response.status}`); + + if (response.status === 200) { + log.debug(`Report at path ${downloadReportPath} is complete`); + return true; } - }, 1500); - }); - expect(statusCode).to.be(200); + throw new Error(`unexpected status code ${response.status}`); + } + ); }, async expectAllJobsToFinishSuccessfully(jobPaths: string[]) { From 2e33dd4084951c6d692b7933387cbebda607b5ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ece=20=C3=96zalp?= Date: Tue, 22 Mar 2022 18:05:31 -0400 Subject: [PATCH 24/64] updates CTI card table title size (#128273) Co-authored-by: Ece Ozalp --- .../components/overview_cti_links/threat_intel_panel_view.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx index 0f80185750261c..2709c193caffdd 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx @@ -25,7 +25,7 @@ const columns: Array> = [ render: shortenCountIntoString, sortable: true, truncateText: true, - width: '20%', + width: '70px', align: 'right', }, { From ae91c5bb7d5322dc72ce7df2e9768618c05ef3c0 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 22 Mar 2022 15:28:49 -0700 Subject: [PATCH 25/64] [type-summarizer] enable @kbn/analytics, @kbn/apm-config-loader and @kbn/apm-utils (#128206) --- packages/kbn-analytics/BUILD.bazel | 3 +++ packages/kbn-analytics/tsconfig.json | 1 + packages/kbn-apm-config-loader/BUILD.bazel | 1 + packages/kbn-apm-config-loader/tsconfig.json | 1 + packages/kbn-apm-utils/BUILD.bazel | 1 + packages/kbn-apm-utils/tsconfig.json | 1 + packages/kbn-type-summarizer/src/lib/bazel_cli_config.ts | 3 +++ 7 files changed, 11 insertions(+) diff --git a/packages/kbn-analytics/BUILD.bazel b/packages/kbn-analytics/BUILD.bazel index d144ab186a6a11..e2cc4b1f58f243 100644 --- a/packages/kbn-analytics/BUILD.bazel +++ b/packages/kbn-analytics/BUILD.bazel @@ -23,11 +23,13 @@ NPM_MODULE_EXTRA_FILES = [ ] RUNTIME_DEPS = [ + "@npm//moment", "@npm//moment-timezone", "@npm//tslib", ] TYPES_DEPS = [ + "@npm//moment", "@npm//@types/moment-timezone", "@npm//@types/node", ] @@ -70,6 +72,7 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, + declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", diff --git a/packages/kbn-analytics/tsconfig.json b/packages/kbn-analytics/tsconfig.json index de4301e2a2ac05..afdacfb1d1ae8a 100644 --- a/packages/kbn-analytics/tsconfig.json +++ b/packages/kbn-analytics/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, + "declarationMap": true, "emitDeclarationOnly": true, "isolatedModules": true, "outDir": "./target_types", diff --git a/packages/kbn-apm-config-loader/BUILD.bazel b/packages/kbn-apm-config-loader/BUILD.bazel index bcdbefb132aa6d..e5542391a3c37d 100644 --- a/packages/kbn-apm-config-loader/BUILD.bazel +++ b/packages/kbn-apm-config-loader/BUILD.bazel @@ -65,6 +65,7 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, + declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", diff --git a/packages/kbn-apm-config-loader/tsconfig.json b/packages/kbn-apm-config-loader/tsconfig.json index 7d2597d318b310..35e9c12eb90eac 100644 --- a/packages/kbn-apm-config-loader/tsconfig.json +++ b/packages/kbn-apm-config-loader/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, + "declarationMap": true, "emitDeclarationOnly": true, "outDir": "./target_types", "rootDir": "./src", diff --git a/packages/kbn-apm-utils/BUILD.bazel b/packages/kbn-apm-utils/BUILD.bazel index 9ca9009bb7186b..a2505e0556b9bd 100644 --- a/packages/kbn-apm-utils/BUILD.bazel +++ b/packages/kbn-apm-utils/BUILD.bazel @@ -50,6 +50,7 @@ ts_project( srcs = SRCS, deps = TYPES_DEPS, declaration = True, + declaration_map = True, emit_declaration_only = True, out_dir = "target_types", root_dir = "src", diff --git a/packages/kbn-apm-utils/tsconfig.json b/packages/kbn-apm-utils/tsconfig.json index 9c8c443436ce5c..f4c8a0de0e603a 100644 --- a/packages/kbn-apm-utils/tsconfig.json +++ b/packages/kbn-apm-utils/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, + "declarationMap": true, "emitDeclarationOnly": true, "outDir": "target_types", "rootDir": "src", diff --git a/packages/kbn-type-summarizer/src/lib/bazel_cli_config.ts b/packages/kbn-type-summarizer/src/lib/bazel_cli_config.ts index 160e33174d9f72..6f02160a1cb3fa 100644 --- a/packages/kbn-type-summarizer/src/lib/bazel_cli_config.ts +++ b/packages/kbn-type-summarizer/src/lib/bazel_cli_config.ts @@ -19,6 +19,9 @@ const TYPE_SUMMARIZER_PACKAGES = [ '@kbn/mapbox-gl', '@kbn/ace', '@kbn/alerts', + '@kbn/analytics', + '@kbn/apm-config-loader', + '@kbn/apm-utils', ]; type TypeSummarizerType = 'api-extractor' | 'type-summarizer'; From 88d64dc3bb77861ef6c6f0db08f0a2dd72f1c6ca Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 22 Mar 2022 18:56:41 -0400 Subject: [PATCH 26/64] Add "phase 2" dev docs for sharing saved objects (#128037) --- .../sharing-saved-objects-dev-flowchart.png | Bin 176397 -> 0 bytes ...ng-saved-objects-phase-1-dev-flowchart.png | Bin 0 -> 175119 bytes ...ng-saved-objects-phase-2-dev-flowchart.png | Bin 0 -> 142909 bytes .../images/sharing-saved-objects-step-6.png | Bin 0 -> 77857 bytes .../images/sharing-saved-objects-step-7.png | Bin 0 -> 28445 bytes .../advanced/sharing-saved-objects.asciidoc | 180 +++++++++++++++--- .../core/saved-objects-service.asciidoc | 1 + 7 files changed, 158 insertions(+), 23 deletions(-) delete mode 100644 docs/developer/advanced/images/sharing-saved-objects-dev-flowchart.png create mode 100644 docs/developer/advanced/images/sharing-saved-objects-phase-1-dev-flowchart.png create mode 100644 docs/developer/advanced/images/sharing-saved-objects-phase-2-dev-flowchart.png create mode 100644 docs/developer/advanced/images/sharing-saved-objects-step-6.png create mode 100644 docs/developer/advanced/images/sharing-saved-objects-step-7.png diff --git a/docs/developer/advanced/images/sharing-saved-objects-dev-flowchart.png b/docs/developer/advanced/images/sharing-saved-objects-dev-flowchart.png deleted file mode 100644 index bc829059988db1fc4393b94430e272fc777a8e8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 176397 zcmZ5oWk4Lsw#I_HySuwvaCZn0Tn2Y{cXx-NA!u-ScXyXSaF?KOviI)Y>|=g(Pj_`y zb=8sYeCG@iN(zz)u(+@wARq|RQer9~AYgSMAmHTCkiaYDH>GO81E`aVqzFjWB>oZb zi;}6PG(cV+gcf)X4FU#=3IhJS3Gf3FW&r~6*Et9XCGZIB<_qXQcfWxBeHX0m3;5sX z;N-s>a;`t206WpLP}6kQl$YZ*wzFY0G_f->WpuZ(|J?wD-<=nDYGdkbNbGK7ZR^DA zEA~nW!inj&_Wb5+c9?zP?UL){cHy*n5hM0Ss-z5o4k|8;qV)G+ zml#aJ!G4n0^v{dHCf&qeOf-a`BmGIgKvRSL_uzmDN>S%p^z1YDM7jyXBjHb>vmfOs@5DB@%3y0#! z!jR>wE41Quhs`kE+)QNh2yteMgS;~5rLsm5Y-x0NGWkQ07mK74xmk3wa}W|J@_nvv zX$S>;>BSyM&IOv0^4cEkZ(@6h%dBbNYUUSU)25bl7u#yDNaVc zyr9h5bTm04aHbya&n-no1art8-$a)GxjPV5qt<9jr_oI(i>bz_2agenA^xjCJZ5k~ znTU#`IqUwkXK=&oyX}%~cBeh4i{+BU1|-6?aHDepjmE#l2bv&&TN-c?!EkcAQg=0U zs-@ZF3w|7>QK#_Bu~avopMRGbw4Joydtgvrv(bVrH5}05I1uT6cDFQBAP(0YX;T&X zr+7sQ8$j%$XS^Qf`7zs!qK~7JuGPCTw5t?C;;L6;KF03Otms?4^|UXPv-l0jv@-@b z%D1_eYB0QQ+VOijd(!X^ zg#6u1MiMMeSMS^OoZUHXuHq+)mBxNXyx@N>n-LCZ7-bzAl|sMUmUqw#Py3Z^-6y3& z>lMvVy8AHw4pX?ncOIlLh1_1M4$qw&x6O!G=_B|*RV=BudJOuuDm*`dJ0!so`_6X~P`9xqpT6b4tu2($8ne-t)SSma~3;jBswkq{vT zMS^h1ddqjC7fp)VofnA2OEjikiI?!gf9(OxhXBo`alsU8?ThfVw_mDMSFcog#7$=R z!GS)>TZc7lW>+(ATO{Rfh;qrLNRx>Ta^n9O6ZC+`XyFP=fQ5#V@@^kM>NK#+_vK^6 zDV*=@BdIWc1eF6tC+$DG^aZ-#48ka8{CvFyjidaNWL7KFUU}8UkXf|x%VzlR#{F9V z?mg09)_Q$;v%^cdOHbGPc_z$U@2i5f9LZ+fO1>6bbFrL-yuo7+eVsV4)xseV9UM{>H5qM0U1jWHs^M+ zlcw+Iqo>|c_2*dG!o(886>)J$vu$mA^R>nHib;bz{vJU4VY}HGLr+ME$kkMuJ$?7N zXXS57}*IwZ5d0OX;joNs#JpEIa!oU z_>^h*IB(GiRby~xna%As=}Nd6G~5Vh_nuHWgcUK!VP)`id*n_Hq4%%-ZH^U=j$gqT zOWi51H6=hS??I#7jVPKEsPo2Qd~buybMuDs@xzTUMIDS}HD8}v_PA6EJ|tLiMIod{ zML1Tk2+vt-+MEBTnqXcj|ECVx(?b{qkDo8sfEUAJI9Kqct-LfG5TT=+Q6oJ#OEJJe zYA;mB5qbH+axEoea|7t_2?Zkc$RrFF4j2IETXMO9sMQV3xa)dbG2N;{LUro4ubd@J zI_6a_M7;Exe3GpWf?r7vhK8E|L>Keg`-VC_?+(P+v@->+}S`&7RodfGn6p* z^-3iKwp6o#d(&oSeVk1Vu+Uti@JkYbuM)#7t%qsP7LDfQ5htDs*0F zs~i+{h_}|xe`q?g14O~bII(hpm=q(#aVN62Bx)vfyh+zm!H3jXq9{DPMZ?wY>KfAv zJ@_nko@F)F5~+Z^_eb!m;;{m?s3mbUFIpvn#Pw!kZ$#-NN)FWyuLoP5^x8?~KlM}z zCES&DEJ2{oZ0Oq!mPn46wGKC92~tM+Qvb-!;l$;>mm96lrh zR@a*O^HB2pGm%!pEK_ep4+=EA5K7`d77J(=O@GVlV$}_6s@>+P5;ujbL(lp}S08|d z?y^(`0jEq&e1X$Oxy3~K)_iGk{H{0>WBTr-C%w)4!=~K|DjOC9>)qzhJDN?@A?DEg zRk{RF>ibNsXV-%Q@K-Hb!=cDA0hQCkQ(@KlZ~wz~zwweSgb_V>dK*w)J6`OXzbX)% zj|_;34`vAX?0^vpdPW2N-_AaI`qoza8F-T#%?|w!BvY-o-36=5(Zm(x9;;pSRE_hn zrNBs^wl7OYCoiogJU3;!P;lhY>E-3+H8~9Wo!-{+l0)i$K%ngqlyDT*F)iQeJD+9& z-)P^9j_2N_MMTmte;85MGu~F$69DfvoWxGAL!SeKqgzGgKblS4Nrr4}a$n!?I6>8z z>2)xX@?9fikFN^R&F?Fdp4iMMy@6XCws>XyZfrA7xAS$}%(0!^WCf{zq=qz5`10;5 zygpV~mnmY6_i~JB#;N}S#k^R(5EgWIv&zRy)MSNfZyYcjPg2&puuZgD)(POEhw(-C((Ci_~Vd9?;2%nm9f;H<$Whe8*0Ns`O7kfDPSW zni0Se$Ln&u{Z+Z}a>R{{*<;hYT=PLW90cfjgTfHSt8`kd1=z?xxsD8%{0mfg3lk4= z)M1)eciiKaQoPjq`6a2`)4^;MqjErdvskjfH94SlKEHXHq{x}2l1nE-gur6dD!5et zIe~0R;z1Fokl=X%X|gt#6Af=ZYiz=&^}*rIJkqb}B&A}A0ceMlJ3;w<@$(EEg6A`R z?ev-gGb`o5*nmP3sz$UFbEi=xByz=y_-%S<$4{DWScw6!O2V;-9;Y= zWMbR?IPUx74L;0h<@+J-$HXr>rL7LWvmqpVnSfR8(j^#ImXMjjRIyiv)pv6AO}V z+nb~k&z)|U#y9Y}oo|mMQ9V9ibk(@;h>*L=1iQxI?M;Lt#PeIsK0fk`w10&{BHB$8 zk75yLc0WCggAD8$W9q`MU~H)Qrcx|INb+u}fecT`@Xr_l?&1ETiCS=nR!dbl3=A*~ zO}cHahAqfz>gAsbr7JVd8DX$+)haF3z_JSvf9vpj7hJ5+3c3@yzdzrKYfYgm z^!*W8s@n-R`&HhUBg*O7ip$P$pd?1K))qXq>T?(=Dq3S={N3?FIT>&_d(OikQYk;k z$!GC69T+S%{2Aj0JjM!V6W1+@HhceKoy_J50y+#Fm9gu!COhM~68Ys$Q@BQ{@m2hl zx_rx##8PtHaYykeVqgrvu)cGjR;@pDAmCM_$3FRfz8sKbHkEIk!Ah;(NI#m!6bD2w zVcceknv}u@+N;qxY@wQxaDNJv6f|@{4Xc#^vIPH^LhK3T(hTpX+ixx-Vq(j6W=M$V zYhgYG4f02sU;{Tl(ioUve@upep^}iqrKMH+$`RlT_~1fOTz9`257nGpqO2h9oOe7^ z)2}Xpg}@b>&lFHz3!C2<&VpN(SV#yDF-p+>i!&h3Cqt$hskob#GOE_=fXyUX@95g->p%!fUtUfXsL+dAWJXZmV|zkIVjO)g`;b`-2&y zZvPQn?FfD?9OxaY6NoL(Kx2x6_Wd_NW#`$$e0O4ZNpj0{G_hzXJn=X6{}-tW6DuTX zJ%>F$UKy_=lY0CZ#W42lUM?dKTn~hmX69&1W7K86lMc6&{%q;(({CN1jD9XtwgJ_N zw0-i7e7dH%yUnUy-es*Y>$ck;K|%6Sgp*ZFGw)8hE23m5&2G#-G*-JzyKUACDF9c2P=7T0z?0Lyn{ep{xaAm9gFl zbsQH}*`Ps8CA^FrV!pjrA6C>dp*a!$M)OZU0xG`!3%Z%VOJ=>|qsTCW*Zt`MBz9YB z3G}+Rx3`=aL?*0~v5cdPiiMpQ-fa~FnAKU39?}`V`8&Iofe9EF7niZ1FdA443?8nh z_P^qTW$6CTz=<_VW4Ac`+CoLu)YsRq6F`6dWJ}&b-onoC1v3qTi{0tNy{MyA22^~r z%9>0aNTNn;nwqt33x#azxHSqz-=faHY&{TJ;0L%{0gzub^xK2Q=2_v~uQuzU{=Fht z;@cTfZ&CL}XMc)1&~gM|O9+ox!-H4#{TUR0ffgeGlK~VW%AQI8&sNdzw z?fuBCQK4yQG|2ap;b;9RvJ$Hm)Zh(b%D5mB$v<&fzYhe^4V$pKAQB1k1LF;_W^jr+ z6!&2n02xT?KMrb_8f>XbR}^T^9PUrG+}+*#XWFE==QOfbf^RDB6#iK@VtnW##XQ2h zlcgb`Gwf6TT;9(KTr1;en4gdTEXuY$^n8UDS{9d`YqQ5ua3I+i6HabQ?LQnn41NGA zh0Jz$05}YONVCJU3}A22X;$kWicJ`vG@i{&!I%v%Nc31KK}?vSn-nCQ*IyijzOwAM zv+sr@{fqb$^x^H-Yj1DgK06~g)i$@O_XSadLC7;8iibw$_jy7;KR<5!1cg-K{p*_B?HpgFSO(*?74yF+ zE*L4mbSyPrA&0kwb>tNI2EVB=@8`27Y;JpEu`%cpBJ~J-FmlsadiEVBF%nx~NOyU2 zji`S|Z2E`uucKE!0%0;VuYP|4^hcGc`rTfA6FuB8z*nzi+~cDv5<<=JfKy2j3EE ziiV%aDYTENjY|X^&Kg^hh5f9;(zFjmp6M_BLxu$#iX)N)V#DzT;0azO9mjX^C`^Q! z4-qh2j4@8C9)e)=q=KHe&`q|_+kBYIuE{61$Na?Q?et)E5ror?wukR_?d z6$zvvhB0UaOH#exq&NMxiCLpc1cByCDwAD0JKf89JKskF9yEsS8Nc0J<`M{b_NMbi zODG#^buf)v{+2&`h|(FL%%ru%(`&I-=ywrrw7T>Gu^fB?!Y1iX@}t9uKBo~niHCwu z;zy!9K5pknYS{g$m}DB=m<}_O`Ye>pPw46;-##XiODok&k+5tz4diw_FZ`m7{4Ni@ zL7m|%x07Lc!+F(abY07usMXe^M_b` zI6&Y}ZZ1|7GqxdX^i`w)$%y~F^1@I{w?D7XDKw+u_`bg*%8xM@9WTW^MkweV-6t7K zqQ9y01(LtW(*e{i(XJE3!vnz9|8k`2(isUi#n5o{ZW-%Abg0T!Lvgcj&_U=@)eas_V(3K$8tm7B3qze)udRj%!9E|g29@^Yw* z&hkc_p4FLMO38gtF?&FtDiv$?*yUco*&q1EQNEqYg2sx)1cOE5bq@s2s@7|rXd$o% z7-to6>A34Lbtb6~;v%wq`etTSIvC!pfLkRe@Moy1p&pg3x8KJ)XSR1YpAG6k9)n&Z1-o?t>aGT|WNulut# zi8`E86?}e@{E1W!xchT~aCn?qyrhWBb;z>{9J6$n0*Fk`O9MSqvi#%RDX(iuF zzcsNsz+oEIOW7|IH=MhTTq6IIepq$9N+w3p)iq|V7oJ7C6KTWOPyRfC8wk@!C-X;j zQgq57QXt<@zP{4L?X_MC0*wz234_yGqfHH90@uDe(f;?^O`6GPRqs*71$q4km!QIe z&3AX+uUcJRUUr8f;8`~bc;5a*X0utL73%@*b4*CLCnDn)dYX*Qi;Jb<)_uU1xV4u=VJTfn8J!(A_jy3hA+BBWm5MbKGo>67qe;Q#sP z^71IX_iKtZw4PS0wunLF1ZXJ9Zx_J#nPyDd0QXnftNG;Fp(7 zaZVf+D=&eu$VANoXW0Yrqz@RMk%^fXdLpjo?U8)}Ku&o`aM^8&9Wv%Pyv#e)TW+DM zQp)$Nx>&IQZn2Bxv+j8+?`r>|FddKrCbGB+by^$&yFfoJ`TBGh$WjJ0~E{GeS(BIeo+9qG3Hf?og)7E+Z zTyPXvebFbJscFg1V5kO6cPXT6cFIlp?(}}h7V_IP`EC+A2F$x;HF`HL+Tq&R(g4W_ z784FTAl=*SQ0U>YGba9HG^GaU0@X^DK!0P=RRgQ`(#@pt&1Q!=bMw8RMxL^(nKv+G z_xp%XVu2)=OZtAmv{Hdo3JFEYL|$ZT%!$!z;5VtGbIVaI25t0;nE&`2WGs{~WS@XY zeVd^%bbe5q)vy<$-fl}zM1Urtf!QO!19|ym2vOD#K&>Z-t8XDY5H zFbjF(T?q}Ho0}W@Wi~n3rCPTQz}`vTG=B|b`RCFz;?dJ+R0o&q1e49-=5Hk$nCX@a z&3|&09e#TqU<516K=|;kDD;Zn_3{KKNc4hJw)Bm#bWzerNaU)P;?nleZN^jA3Z8(M zecDCfFN9CbNNkXo5T&;lebZvU)2qbhI1u9HxX&YqN$T!#VT-TT4pPkL+SA7;XprG` z-fpjuh%K4M$Wj7{)o2s?*_EkiDvL;3rMOp@MOG^H9yS-&@RnK)f^j5i&6bV2r9t=c zqI+RMoz=1h_v`oDqq!i&33fbco#yY-WaF&4N$lF0idK0{`hr@!T~s4jwWKZ4(i02~ zHSis&KwKnk9Y3%bE7sz9cl-8GeyWW;+r=#twvRNP2o_1W{nZa;)2LuU>W}dsO2tpi2v#*MxnWb1eIz% zfjXX1{=GH+(!2I{GpzfGH;jjB-ISjYTs39~=*EumZMNVh44(XL1MrfEEMfg`q{zNK?r}ne&REK@2*WLN?B$qDJ2srE2%wT7X-TF~c-{lEBbZEohu7#fs=?K^lT zB0@b;*+MC0Xe=QrZ9+q45$45=p>b$|Mqef{Aqp`Vjbp4k+wqrLA_n<4c#p%(*k0%P zy?q1%T^q4QLrPo|QkwxXA0PnV78Hb*hf|TMSXR>_4-Dv6l0QzxrQv7AJ);+OElwT& zV2i51C@xiKR$L|_JN8^6V3U8q-~f2l=I``l5>c8K+g`3QmN=2&TLLOQ-VK;nST&hs zEOs4uCdM?R>*;dMLET45=FZdgL~jwiC%5>?Hkr~e7r^3pdG=t;kMt}3EIndJ{QMj76HwP1< z6PX+n_FZpJR?dicrar25nELfr^YH)%UBo3sB9YL*5V>2hIMM+*31&;3Bjf`HWrW!3 zN4yM~MuG8as{~@3kTD7TIbOk+6eHv`#d}y2FxWZ%+MlA*9rf6G zwsU<+o!<&WQ~J}iwJ6~W0BMai6{2ShYvt5nvBAQDRCOw{;`> z)LjmUcq~7?_V77f$2Hllmzif@EkyqkInp41ABiepXdi?qEN54S(?S{qm~S*(te!=U z9ZP(P(XTq z;^$TQn{)v5`W$?Kwl2VBk7wla?e%#AwxqHK$aEt53kOox8S;3D->-PnPa4k80@u?7 zW#0AodMZPlvN@_(XwIbSrIx7D!r%pvibwk6jnJaKE4j-hfyna7CV?j|G#X9zI9eK1 zdamDOyR?`&gP&xo#~r4w1d^rWro$ZPL4*hg)@Kx-DGtbBI9x7&bhyqWpx5q+@&)^e ze;>vjcP7-j#U19r{~zC2fC`!^sU;GIMMnlKuqdoB+Moj)r1v+h!`6r@fs_-H8xo=v z*dDa<#wqyKU28fPeS;O(GNTpG*;pa^mDv&?ZQ0V4NU4cD*AxA!39&-E)Io}j22IXA zLC=BV?ERCk2+8~eSJ#Lq?3MQi*B<^LQ97e;tJsqL>IEF}<(ggBj-Tv1WSZGjwxDU} zss0x}n`3|IBB>o%#VcDz)z*6V(i^rRut$E zQp`tULz;EYw`IC*oS5{Q{)hy;*w>>F(Cng@n9uu#l5x0b5E2Xq+>Gba=1zx`HnW&% zwCbb%>89?CsM?uTn%ZkXADsyeb-%j0+@1aaWF zvSj$INh$rMF(puCB^CGJRbzKb=6D(rC59^{3WsT3nXa2BH)2r7R@!6|qOX={trWXXj08o9je57kK zej0&z@gUX-CT`>=!s{7LX8+i&Cx?H7P^H5k6@ljKDiwxVg|2yJfJ6HYNb0i0s#+s) z8+3!f<8vbe<2tA~^*}OVl~(5!nfWr5+92#gETt&Rm0ImgwUcbGOnBx%(39)zdjGrT z8O&5KI#{p>Ml$m6u|7IllR-s1z_1OFsPVkb7OOczKr$=XbV00`tJCNeu8EXTJ>gZv zP-BXO|0}oN)R_$V!Upio<7mB@P4Kyu%P491`r|B5MbmiDq#+O`#h#m$zlQmPh+?Q? z+K2`LDz7F=k7P_lyQxXgf6!?{Y{h1hDh-kyBx#)^+NW zlHWP8o+Y-B2h`QOr8d4PzD8rF6~#etIUS4-Hr`+E^f7T-*i=G`A^&ED4950TA~MD9O85gBPDJbP5ENZ5P2hj7;DCL+{AJ5w^j67+1^}vL?FW$%=ZFvGriraVEp6L_! z<>Dm67<+kwids4xkRlSByJ=eb^L^-7bsm!4JZZa2Kb? z4Kb-%zWaSOrn)`l6N|8hHy}t0Emdz~5j|SNy*~{eMzjY=Oe>~>?>XPCEDe9WE zKL#O`2W5X{A0(T~PFWKuU>}_t&+p;*Oe&xOQ9uyjUYrrbKvg{~Nby@Y9Z~GKoPR2r zj2?#^*tv$g8{42SwB&f0e5C_IGJO03I}6dER!;hdGT=(&lwsb zd$tAEumkF=8jU}due}UY?B`mnK22lC?L0Pc;%}}4O7rFxuS5|9Hf1Ke7?hc->@=6R z2LjC|vT#B)iu>pWpJbmkWz8*x*R;mY7ipWe!dxag^`wS;jh4n@b{}pdSX`uz4*=z! zGN>ZVYB_0QOjoJHkhC!10q%tv#YvxLjKmXUQbkJ`(s6f^>uc_sE5{A1&Z_`#QJ4&o zmEZbL9J-Q9KU_E;VN_oz+no;}yEmd9PpCc*c*3=~Y<2p`LzV#34Iw%oWx+{= ztDQqrcJ(MMk)`%Y8^7a-qh-y1m@Lp0ukCmLxr_9^U$7>V0U?!z?@KcEk_ zz**AQw?=`S8S_Ztu-f6_IZ31%n-}VJZeO+gLI=a=Md1x*FbN>Q>FJQ^{Yb254k#o` z`XuKE!S1AaOQIl|Nje})W*5*q!xesye~9u$RWrYU;<67($iL`DJUCWIgMZog^(jK$ zR&|cxX0tq)2-I>>tql`WPXs-t+16HK4^*o4_6G<;+X#of zbF`L&V7^Y@;mlPSv&rm~&g&16p~(02dAjoClV(eC1U*MC@1(lt{KFbmy>(HZ$p$Fl zbD3InNlO97Pz4mfUyar}qc8RpDyI)vUV=8K8lJYt@M}v&tx{b`Z!>FCZJt!AP{G)G zy;<7VdP=O>_&Hh!7*MhML}Rn`N|=zX=W{AS^osc7Dvpf0+->_l>cdFoqG;SrS0!Y# zzF7m=p&H*;&zrV7V7zn#R;_tP-luNN67TtttB_6fa|PoScK;@qbx(K(!EjE~uTL8xcE zq&3L47{DsWL@h6>Y9ONJ%})AcBBVXYV|@zL1EoIhnkzwdJ$TT;6M79&Vya(JcSV(Z zd|(4IsVQ=jVPs+e3-Gme4=3k~%?-$@?Vg~XcU?xeG9XJG3ldS$@6z$z3GeNfCkP>x zBE}JklpGFT>Wp5gN2Pw3AdugOZz*o%)NOSRJxyF*h1;9qg>mp9FM-HCO6-*!J}n%2 z8&2khLDX%It6L)48O<3g&0a#lvK170qo;{ZOh8%(0%jZ{##7AB4<^z169oHaog=9553 z2S!5L4R4Rdq{}S(?NgObKv#=n(5 zZ%A1$WBqEbBCq=Zhsh2DgH0K$y_zfFl^LxE-=yAbZ&)Z`GyUabcO^`IQ4D>Asfq)- zl?H8PHXqLO3P|7y1k%j4N~8y*iRw6`30EmJkt8suDR;W|Wl4n3j~oX5?N=EQET>ff z{0G>b;euZP!U&Vx-$0SOIHMuEJu$-Y=`ctr=re9pIFoYw_RT?J_yxE=zXv|g-d#An z-3307u4gHA(iEHa(n!lUQZbKQHS_>oplmEy#rB;~FrA z${g0z>UNJ2p+KEL$X&ndN!mEszHkCpM7r?b`cdcZ=uY)wt2b~UpJjiwJ7BLEOr9Bm z*^E@Z-a=esV;cJ_i(T{{HbtrOBF!FF#H6F6<2NSzi`xljGf7p@yO2Jp!1ripv3X%< zFTbO;O!I`fqn#gWlwT`2i%6)SOlL-;dvBVJu5mh1NFG(jCF{b$U@-Qb{RY+AzCfY0 z1%yMlCyH0+UYR`f$jqJv&|+j@&fYb?<2>*%bagZ=WNALQQ|8$sfft#CM;w;(N>jJ* zf-s8WWL{EI^bNJ@f@g3|*12klGPG60erk_6TeYT9(jI(voBYi67-Wc-H+Isi^%lyV z-X6{5SkLoCsG^+WwB$0msP-v7tTUa0zQL^5Mv}lJli9nU@f`}rb3Kn!;Ut$3X^5An zG9({Kjg|kwQs+e@>aRe~uJv+qd^4z#UneETR0{*1rkO&G&Y7I#-*Na^x6c$RmD`n=K7Ft(rTLj?<$W_VL1_7CBY@2T#40Id~auHclDr=Oeq)|giJAPA8 z@yuGG(r|?eq)Qg5_HQp9%(x=X;ZD2#QjGnXV!+yw?~>j5LMZnNdb*y@gS{(o6YFVq zx}ivduOMgu`R-={@cMK#-%Ni#LANW7y+!OryYz2Erf9hj&&_!}UsGL9-HfjsTUr5l zi|=?@SHI5B+OAfh^73VN$|TsHrXv01v!>b}Se|XN8u!9_gm!ygp_bdMh^lm8H3Qon zk6>GyE@Eq2#xff9{REfONxsj#Xx+?IY4ORudxU04-taP94vh=R`F(tk#u0@4%xMjy zZ9Vz^c48Ne$1<7D39owi9hqfTM4Xrw#Q#Y}1RZA3m^5_l=)Y~+;efOJtnmluk zQH!){6JDq=${_mF`I;}{>0|hNCK>n~9=lPTQe+AE4>?BX9aLG4*Jcj5L#$xsh=d44 zpAbJZRO_yJ`@57c75dJ3hF08rS5uvDwIOjefiTS!jU@JRn8Knd<@o4vyY zjVu=92U?_~{iTtvj9rJW=z(;c!jSA%qs4kHZjV!RTb(vg)b-whD1+$-{hg?MeQbHV zi)zm-PVq=Ai6L78aF;f(HCxfzF?GhU5AWkc*EVDFdGo`YEwAI7rB+i9HJ4i-+z*4? zIdukJr!l8=Ca`u=+avB@fJU7k z16~7fK6B+Se%d0|%A5~Jobp*6R%yS1q9IFeeU215}R*87Gp**m(Q z!c$If9In&hJ;1pG+}9QUaHsuAL%Jq{Wt;&apKJ6qCOytHTn$^3@?P+!`EN?a8WqtkPUs&srKjNJadW}u*DNXcC0g-sq~*_Z=Gghb<(Im_s)+!65;sV zUMBD$UF6Ynh%O;#j2oIp;vIfpv}+gyCYSowDi>A%lW?{dhQPi*z6yF?=I~)X&*qCd z*7pu6vP}%+6tg*s+5j0j34z0k=WzlUpNvlH&>N#;TWalPW}`k^0_bk8uLGN!+;p-! zC%F=#xq|_VnqJiZ&RuIwLkEePjqcsYEvz+$dTR6c+w`u+!N>V0Y#?dn+q~LFABn)yK zQAsxefqi6<$8A1?Qa(Xv18%5V>8kGZc3ACH<2wOaU$F%HDtN?n0O58i5yVBYzvPTg ztpcL{UQWgIm9s5>Pg2pKe7S2?_G*>_{-fT@p;7dsWv~Nr;eAs&vQ?bilpNUUn%KeP zjjc~~&`M3;kcdrHMxRIs?a0ypbLY=PINTNIRIk2lutX>@TUg{523fa);f>BqdJdieK(k2#Mvd>yT z098edY-RF$SI_+v^rpbi3I&M@OxKoTd`{+TRAVsNv(g}}lWS(MG=O@)<#0S4i@Dcb#WAaS*h;nMXFI@_WabT;S&8-v z1u^ait$}3qxmKiY0_HNWkwT{xa0OP(ojRO6_Na>JCm?}W_cMaKr!mFui+)fkRq7ONKryUJ5DqkyUM!SRv8n(K?M-X$#v zt2_618)u6L%jVAxwjmulK{i3pKB(t{M&-Mz(t_PBdAttje2uounvy1rz&p`bWZ(r! z%S|^a6q@&5Z>KcRyY@u-+Kq#3*?DY#0n> znhcsKyQBsz>j6Q-VNQl_C_|T^YW6xLoP9NeM0N%VT{1H9A75e3YEnAkg&`|M-ycp9 z#peLef@D6y>VhbJMWC%867aik?_ZHMl0&DT#jV$q%ZqzF_M-pXJiZ@p2PDSikS>__ zt~^fpUg^(wdS2UR+I_`6G_+xo@l_DF9i=`*88*nTh)Gv3% zMc|fxIe49NTdY4nDi_??$t59c^?qzoH@Tl*wB4x3q>xDwoi!N&*M*xJ$;6OlR@1Xh ztJ=US9=ZS)Ap2v);K1N9|xGHz_7x+cK?yh}=Ho%-M>hJmq|jgZjc0@r4y z&;uTEpxUx@C^Zi8VJAKeiOA?^i%z*5&^@|$lllGaU?NMo-dwpzI_bb;!T9xwK>6S? zT+}6(zr~ua*ihaQSZnjU{^9B20*SCI{PNj9)6#cR4IeL{JDLIVV71N7Zv4fxL|PIr z(Z<@nL|DYa&Pb`%`$%}AnLlw-;FcnBpC++y_U9^Bk-Sari^pk{_0auJw{^%n!8G9# zTbSAOuBOoYV^o|{K?QHv2Os32IDB?-gsjfkMFKGPUnBcrd)=Ek&Aw-_p|=-?Rd!po zghIY^L>pcR?Aq)xi9s~WTgQ-NOjjW&3ztGry z?_$36;r700>eY*&TEZw23I!h6)rWaKZBx=(%(jwQxn3B^6A9jmXC1gq?@l15bM8=G zhL!C-3tU=`HI|$g)sQjU-8=Im%Tt~>AFsl-xmgd2wWKL09i;Q}HQDsvMigGYHtQFi z!xx^lJN^pvj%fSx9-ojdWJXz{h)a`ye4al;$8e?Bv`MlWpolGiA9e5w7zcYKy&O3N zr9#|M+g zpGU*QCaxD%HrQ`>hmd_T3zmCxzFXJ@imdVz=ND|0#5TNc?VGJ+Wh3iSCQ~^)5pkPd zbL(!Y2k+)e8>9HQHwVIzB;k`D?DEH;y@;}}kf z7HE>YYYD19#P={g{gy$0&}nd)%u&V2S0Ny}|p4%?W$&6q8909~k$G z05j$I#*Q`2Q6l29kq5Te5;54p6L^d1F>}NLcx)bwp0WV$)p<-tjZt$Rs9VJ8P2a1# zIO@;&v`-JmMpB!(IiIcTF31r5D>r?Y%t0j)-&LS50u4+SHF$!-Gm+IQbX4)7>VoYt zmfL+16rwi1b?h@JVXxFeEr&edy(DFPu2C_aSZ4xKLQztT|ReHDgu4u_Rd%-N(FQG1YK50APa!66%-dXKL-!C@Ih<96}1{ha?QN1#gVmr9>* zQ{UVHIg%f}K6C3zoBN3bKb$v8ooC2ds1XJ;t-SmmXzIJi1=8?Ny~Up~dKcoGn&E&5$A? zP)Dz^MzP3#vf9`vWi4Uja3_DnHx+b7q@!W63%kXMY&cmo8vn^6Mdwk4RxsWEMLo-{ zMqdujeCX+9-a+$y3ihS#$<97wO>Z(+&_cn>&CX6gI_eO77!xikjn=M8z3LuS3cwV_ zBO+ELbH{6_aWVbNxE|$s^NWZiwG3s=dvS9lWyZCXW64t08ic%_7aD=~0+;rWl8MiI zg^Ey_C-;7(N406NDOStAr$3v;pH&jQWJ~iAX=zlNje39DU1`*51L*a>e*5Si{D_IR z_DQzZWWpBFPOl4a|HD);b)Qa2ESuy4BGSJ5zP~NpO{P3^Wvks`Bb}h$>mfteH>zoq zWyxKUHZ~5#IJ8QuZV=Gst*W3G2tG!1H?l{l+w>P)r5)>`Q=au7yfi z$CLZ}s(J;%69y|LO{fTiws_|7lhEN{1XdxIyQgO_O8C1|3Et=rvHWDK8d79`;d*hl z@21x>d7~y}uo1(YRjUvotQd_p2(%pi<`LM!*H|b!tfP5d6KHD``gIs%dOE3IWD=C5 zta@U{nM%5>M4bdsDKYQ1ViTwhftX>>*%r=Ixx9GiPZj24_^FJ?Y+*S9UW?{~kEaYI z%0{8Al>PST`8igVK=08N;IK2A7WxVfuh~1{A(6i~9hPysZ z?s8mjWP8cdNYHB8sGb#PGD;V=Q z!>a0NW!937ZcZwTd7ZUi7du=&=0@gTi4PWw@j0wR?O4pqhbWnXl&Hpgx>QF0y9(OXX3rm)5Z8*v1z49}LYOaUef^QRigEBq6z1=BQ%+I{=fh)5Li$L;2P&rU3G#AO z5+r>ID2<-D5=D5vP+neMLn}(3oxdyK>(#{!gOu9regK>KZNkK3PR;Zm5ZSar1rpo* z7745#UZccMu<{M-AW=C_+|ZsmNPn{O_FxhDkZ?K(qF7*_xWHjDgkKCIbvu}0{QbWG z;%@mRkcb!*WT8kGawiS*5RXk+!>&BULrkV}34~rQgIEj=?y*Owgx;;Q5jr19{plqJ zxS3X=8ocKBeeP%Yeo|3_tNOj6Y$2Q*w=!sz1j^HS>@FK#rN1xHZ{6&+^RPwVpr3vG z7T`_4swn$YNnEn9=ba}m<(tw#D|(ero+59)PQD!XeV&u{v_u+{Za0cnW7F1T9v?xy z_Lm!VRPE?j6}_@0wF2k;lr&m>lkxO{bRz4ucEow0((@1t0$FKsa)R9TRy|Jzj|}OM@{Rd4=|aLZ!*b5=?`hZE%Q_--jB~MxxGV zNR<*twyAVVC}(8V2tH^XtRV_ZTnjn{C`w7PIQQy& zFGp7P9-yRSSEwV0{NKc8cih*#eZWaCE^$MgMiR&*zhHIy(>d%_S5pVhmdG2wHSG;X zyQJ0x2q!20PI9~zKF1_|axcJO)}TWp*dP88GnwZ+_+44F#^4?-3*d6MRsIpdc4hs? z<;OMK#^m~Y1D|C<1DvZ3u1&OE=Q$m1ZnFT32OwjYE@ahN`Lvnib>%w8S*p@pChcf1 z8+}hh-YeR#5%&Y%Zlac>B-?NKEhTfJyHmo4PumwH=v^u)J?`1JeWt!P&d|v1=;S&2 zRPNED;3#YL@JH0A^iCW->%riRDT2e{o2dkiuQxBezkf-nxhbWM;Y3PpC8H|kjSx5SZ@_bCo?FiZzs^BokUk8vK6JIH- zJ1y{A5Q$)+eZ&(-7|3aqO4C`S-JcuMn zb$oprj_owIZQDj0G`8)=wr$&H<21ING`7)L@BE*&-cNAOIx}Zx@85l2*LI6O4fYXo z2`++#x-L`g`S)zQ?(WTJl^O9{Sc-dmA_Z=5O5G~m*Q58vR9-eI4NOXuCFFI)5Y+&N z(N|Jt_F2FP^0AD8p;zvAn>0ahl824{u(O=|-;t3Kou{|DY%fAV-?O+f-%oc^FFX3C zTF05zHv4VP5A6?u4XVa&6d^ir?3O+$`J_(*eh?n3MalmvLK@N@7GGG8wgbGbuC9rT zHMyjP1-=*#$p_v>OZU%9fRh(k*+QdFGlPizo{^wTyb*L!it9PqO_+!;r4wA=oHB{5gNQZRjiYCtvACR?+%@z*)Zji|NYsjHv{KwyP z@D*<#H&qg}hh+Bn6PD)}&T!rqzQ0hn>R|?bC0*T7DKU87K9E(ua6%zZ$f1cZdu90g zZkf&+$|B5#t8m-#^ZaIEHcvil>YQ`Gn~Qh%dEAJ1?JIg(? zHyIMO{6&13FyX(mcN#s*wQmetc7`x^yL%zGS7ABzcYaj}JbJAU_F}SY+m$(V4X#V; zC?l5JEdu}LAxrv|v84=I26rbkdzZOLms|Es58Gl3m zIRj8~dlBUhNE_|WRI;h`5kQ}h1mFnLIjk{#zdqdFfhvAFt0`Qo-QUQzZC^Z~6+Z%W zXyw=XL*N@s#?VyEftomp0?WMVJ6dxboAoxTz^0`eZsI=1#EY1;6bYPPANDkj^YR5} z&2C$eAR8+9r%s2a5urJokX;3Zaa<9MtvQZk04mf8wkeZK{N8_QsF6PYMLLAPmQp5V zJWS|gQGdhl;yjA1vJ>@x@fLv~m;?6=9X4$rt7OiCfLM2LCU; zwr$MD=zZO!QK;N49tjl;?q*buTXw95UUe%|A0ol}H5_N!F7+5T>!%+^FU%H`vkF;l zILDRwb70S%EIzL;U9V?VM_{2sjzA}gKpE!i`Mz_J41=8aOkz7vwYTFA0-zBb zK5GMm4UD=ykzgV%>mf10kZJx1Oq?>?Dy zJ4h`p7jOf6TraimKk&A`x{qT}wO3v~yrfz6Q)xAIdZhZxyvQ*>erlK~5BR>C`lps2`{!C{w4*ZW{w5D5ZL&QJ ziNsmg<~6NPxGMN%>O`mmP2bp$`EYm5Ha6p?`;87b6Q$IfS-HPHR!LkOz!8TRN zRpt+QUJ1iA+iqYkRjG4r6BsH)6$c$bZ=Ntqq6Y(YDZH427V8y+0Nds243GBK*8(5w zu|$d+VR{9m+}@y~v;*f{O4!8T+&J@@>Rj3-iZgCX&T-H@R4sOS86 zg^Np4r&bVS=y}P@5YByX2f&wxdC-Xfn|Mw3mSj9p-aIPJCg^wf5(mUlq<_+>v?ufu zVb%Q^QP^c{KzDZJVzs8o1Wh)wbV*<2@7;rg#TSVf;dcU|koMkBOK0UG+pC`*=SZ|v zaXI1zMxe!+D~%pzh$d)2TIskDte+1w7ct6GLGGcd@@9wI-LDGgShZ#FR+(CQMJSqI zplh&}JydzaewP3EGuSkh&Eub+&F?W!@1}&;f7vu6$QEn3XaiJU2fSa9u$lYb9YQ#y zq&n3!3sGFId4wBPJ%CN~-lg~N$w~KK7tSd8$2xa-dHyaI07t*i>GRPLWmPYhMm1&{ zzsB;di#K7N_!)!C@uS1V^8Q0?s+xj4z`MYZyJk|<$k_vee&{X2LnOh5X6H?#xzfmJ zXD=JZgOyTBnWP=Eky7f>MJ9n?MtGU&QG&rjI+M?jr2k2r&)a>@8~@oE>D^Qhc$)<_ zq1bsyG?$`Mc=cFd(P_by9dN~JodRN#K}%LS(l=rJj%(T08TkzUy?@=}W8lk*xlzK! z98Jw3*|!r2Ka8;nH&&0Q4|;*Y>8EXMdO_6019?j7tXi^Y;Tyl9!BhTTDlf+GO(EMY zRp-+NaRqMIlX8+M@+5TMueJEepK|KE=QGfFHH=AoE+^}tx`VlCPH(Ak^lo}=BaeP? z5o}lyjTB{y*^(u=Db*^*3*(c<$zpHFMJi8l6<;mxSgc-m5Uei zs{MKvY{;$S?_y_PhSDeVIHgL zc(8P9E)33wfO93e#EpoNOxve;bMw<+9C6I?y*iIgPT0Nwz3D1LTr?q?)N3kB5Srkq zcC-orW~W6!qXk}GwBL))!iQP-;g{LTQsI%oh-ScFSKF-f+y$dl zBHzJFfRha6*EA2KJqM+VxI$w~<;a#mZEw0gkB}n<_-2%0d zte-@~(|YYrB_1r40TdaBl6>RnjiUclvtjb^x1oEX-EI#?6`!};LB9qzKza1LJyVoe zZ{DKBG#(OrE`0W9hPJQN@TwGuKABJwiAL(3bQ3vJE(w>C|NR5+Z#r z&TYXiy{8pkEoX?6u@RKv*|4ukZ!$ngjXnuUf5#YP)3} z#Hp~6$s1kB?7i70xi90j;#$`_5}jwBZrz;3@T1s29o}SMx~4Pw6qM;l)7wDtwF1GL z&_m{8ibk7XW;nE(Kq&6M5LJlMH(B>o_L1h}7Mmc2fOpF`y$zI+0z?g_28=5pZtr>h zN5}TLlh?yJMfBur7 zrHz_~JG3o&?)}Lb7r(J}PP&-C-1$3_VQLjX@@7fXE?&(gy|uv9>2 z@N+qpHCvT6Z3H`HoL2RPU9T_d<-w$rsirc*;g1=^%^n&cjF4{@fj99#!qeN4JD&U^ z9B_W~@w-WoZQQ(A>ue%|HtNqiPAeIW{VvKm^m2wCvIKlq69kx@8CMrt>y?t4!6|vm zE4Jbn?T_A3LE4-FX{(?}(`(FT{%U6{&HrHGIH6B$BA+z%XEGC12<*C4x`;TPm<_2T`ZnT%$ndlRx zB}=E8mhuqU3kgbkFh;@he*Mz-^{M;(w}0H?nTVHE`PwGDYeVbqvx2J%YWI|q6TUWDcl^Wt^ zZ~09dO(PS#cf(#KSxD=;<$N(qsq4G9at4RVC`wXg#!42uWnh6=*gF2IrepyteX*xb z4kgdkN*z%wp@5Ydk9Lby$a0N#maEI}T-sQNX!?J56HIu}=Yf#$p=;Yff)@kMdA@MO zUpwYJg-|#!MpsDe5f17etFu*rrdcI~HgqG+3#$)I;|TE*TGr#g9WkV@NSf3*-N#WP znP!V=5ZQc^B@(mEi%LI8J1~%XY1vqfd@$>E?F-Zx%H#3YhOi?M^j$<$FNL?ZE@J@_ zingEk75iC3m!&F z##3XejJ7UtER;LHr4t9hQdGa~x8!Vat`F;XdpA^!kfZ$N?O|*^IHp)1ra>TBoe1W9 zJL&Fw+-ePVg}y(YWM=_rKbq({GN_CLC}W9%Nw3CW78jmJg1ik%o0WTqhV@gyE8;#P z6J7^LqSSbN@0&l+|AA+7p)HmR!|80A1?^xhzgz>H3!J2rc?q)NlLMSnvmIzi$CT%z znkwUI;S?~WO+?Y8ygd*tbLsc5Q8*UlD}|%+f06Mt`HqNorro5r0jqIq@(j?V^Am8ms+ zdttY|GW_G+>2Dj~w0+KZ64P0;g}l~zIf~;dOfypGiXZXItiz7(D1M<2_+}b4k4&Ab zvztsWN8qztEz3><)%F&(OL<;(y*IZdWlhIX*y*p)-H&vNdt`e*#a2&Oc~5ycPml*;8{FVXl6=Dp`RvFDu}%yMc|bhA&>3uG@B+aQ-Kod2?3v+V}! z?{BA2e0rT`IIoZHD~s1cCgh*l0t7;FpK6noZ~e<)UkwRUb` z0Ikh93i{Omnr|utB$zsy$25_%Rcm?1K`u#^G`-?jc$j8*)fk~5|;=0rc_`F+VaMp2UvM>EbS83 z=Ze55DX^II5NHwFD@PAR$@?DBv;;EG6Y^PKP++NZ2x)X0Ok5D*-ipx=0QKiwqp94z zABvVAew%CxZ9WanlcSQT_d{_K*1}xr%z<5YSNVrZgcbyLO@Ow}H{P%mdTq2IiaoKC z@f2nNILo4PwE2rHb2%7+uHe0aN+#*=Xk!`J=ia77BA>C|{$^fU6_i-}uy{+>YA1z6 z1XHT>VLS$v#2pZv?4%3!te!BdTSWCm;+Qnh8+>(+C%hckdi+~{Rhu{s~4g&iP1ud=!PYJ-wOz7}ghGrhI!1&ML~K32I>ZU%c>-xI+90yo~J zTxhVnC#f<)4x2ow*{Cg+{!xvCE|1y5wc*NntSfY7&AxmIp+BxBGa#q))#eZzVTH2n z6E9vh=m#OK)-FF@qH~4EX=CyDo?!kB!>ad0!&CD@y29~ro<2=i##7VvWR|38cfHXR zo>h7rK#M&Ql@XrJjxNMxnbkO4t~@dMUay@e6@pu4PH@>1$Fch554H3&+Mo#@ zD(WZHuk+=p;3zV~l40lf=UZT@xg-wf*LD#SAxOV#&d}^xJuqV{ZMg?*LhJ=Q0ZtN{ zD^aUT&*H;TM-se&G*H7iT<#5J%q9)FgZNkIA~vs?g-%Br1fM!1xJsc_1|`2R0Keq9 zoCR=gd4fYf62N?KqOL(#UIdp@nV?7~6!1(p%`L}f2SRjhE*h1R)Jdh8(bMV-^eGNA z^rHCe2NX1}-`iAXRa#B8F`F511@F7atQ5_U<`i$(Ar+dJ6y;+<9~|fDnho?a8S_(< zLWLg~3`)Z;B2tW{gTH?H9Jx!GH=VCA?q8fH?TuS}QqVpZ-3N9bE6kTKBU3V0(3UN4V)I@KGOX8DwuIWS!qV2IJc z%=iS)vkw}*eGbcJZU+cJhiWi&z}W-xW5Vml!67y*7MGhJ8&26R^jLqr)=8B%&{WF} z4e7Pl5gaXtSo@H*m-zdZQE7VRDje_6~Hhm6S%5&c{*TK290b;a)v80-B)ej3zs zjpjB9A@@bo_={FLUJ|@4m=bR>F0}F8XrrLYEbC!qU06#~g(O&L`Ys4nm2^F?gvU^_ zLCy%;x?B7f`fjwqhg!L-d9@I$f9M`G(=yiWXc3mp#%^wWeF9qGb?vI)qcb4MzgRH` zlYrOHrnATDUP&GmpXL`{Pw3&7HZkXzUxC`+VM+pf@#_I!LQi}l6k;CJ-$Im9<+`1% zw?}0k^!o0nI%3zpXplVzjYxGYr5sQ4tv}kGOq(+Ahw<#we>Wf$4IZkk%Qx0gGz(G8 z9X@3h!5zC^t|MMxKn_5T!5vW!(iCwcc|{6^rimXrHZxc+%ZnNfNoqR$?XV5wzRlr+ zMaPHS20G9<=2ci3ab;y?mebhh$$;S`{R5~^MK55CoB$*yHA_BV1cQvGcgW47#kz>=B+J3D|^5Crh*a*O*aY4L)VnMVlG# zXLPv*#ELQCUIWXo6bW1%miJ|Wu|~TXq2Px2nsB!2e6C(pT?%ABnfRYj6Jm=xdc&i; z&DL=5+ojJlF@-*FiZs{v@hENQjpXRjCYT^4K?!eY zSTSaCm6O9VGUnTaRGM{?8FmHAn$2n1ZFFtXw>Mo@nnuZ|%&ud_A9{@I`+ZVQ$1|=r z-s*R4l}83Qd$h8f(vA{Y@!83}PS%r?eylyIybDd8Z*G_2j#U5pqUTDB`bHN+%b_<6 zM23IjHIj$m%?dm(Ux($g*HhmnRhV#Lux=^MI}Gf)u)JuPWT>Up>SVL0-=3}wXR(M3 zfzULQ_K;4UO9@#L;WR2#L7&Ssp&R*)<$>FdOgK=6PyN%tnGCii+wJ#p9LLkUU_NCu zdD=0%T>Ur>Ql!w)a_)n)-r6!+tk|A)_p0V$Y~wNfQAARismE)VMT)>ws(9f!dBWRH zH}}~vPpNDQ+E5fm>Tw!aR+|kv;(m_wAK5o6EAsX@xC-qyModN>3-l323c6w|2G zf`H=4k|Sq_JQ7X^wXbg6m?}<@|4%8SHJi$2hDYrs3*1(=N~gT+T{&s2rI7F<$e?2l zGq<{0Pml8((w1K*9c3(nhF&iD7$!GS>_{<` zNSagN7Id$JM%+UL>UGeBFfi!!6nQi5v{tXpcr)`GoRLkl*5A5)h~@tMl#dhohP?d( zlSBb3F3vtp1{Kqn*4-o8hTMUJc=EN@33o$X%jlV~gTgKyzjLL=|GN`#>u;85_;o>0 zBMtrC*!!0D&5a6T1!eQo?aDQEOwz3>%OuI`-q!4Ks)+{} za){52M2+Ah93Cf=dg*F?A<5FRLW(nCo--M-iIHiv-){z)GG;

1Xc^t%XJ%$aFLmHLU0zS70I$9lgUc4t_=ti!2z|BmKbIgP@Z__4CT z7_Zc7mH07)n?avjJ)rhJut)ZaekZ~MkmAiN{z{vEbDK1%eLch z^;sbhE`+bX?TznEbP!rCRwZaS7i2cJH0#IW8?RiLu|bf^}S|g;u2jg@=oR8&Jf{#4>$Rc+mtA->2Wq2tub$* zJfMU}oHna{W^DC{VW-hYe0%zS*j2#FbrTSK=-w<}ktc#p>qj|&AU1-0@2OZQrHz|8 zX1`b|NaoQt^ZWj8Y1^<*}icBN9@d2yaQ zx9fAYE4h9W`EIR7UtKnjM<7N?voEvRPCiAoTTy{miJ^qYcCjO2y#FD3ec4{Fh%%9u$+`TT41{sT8$CpPn#ePY;OAF-G(0e-ex1LSE-NYzCagegLt@g)lg=L+7Q#9xPy>Rk~)si(1)hi0+}SvApM0sRy{OT+);8F zpD^e9x(~0Cny$J+VseF4QQeK_&qu^5dj1bbAF#yyNAq=PQKLVb#6PXDmg~>rg8W6{ zvt%WH&RXjgqSjuBX18Jo1SnIYjny4lN=+^rBy)d=?BCin?jaQ3IR0mu?PQ}>o;nC)PR-DBiTGs<}X4$tqiMyFZ1d8p-V8)67J3`nC~iHW9Z7M_n6-f5KHH|qhPp44B*-Qd53H^Jv2L8sp!p{ zHhj9?dMapVP;P`-vee1S`7MmZAyXSpeGo!g9fP${$N?)Z*i3IW#Ud1^I&n4Q(q z+h)JHocX*OSYE&g5&T}$=Ap$y&gDNCi=Ne%C+NG!Fnu{1P*nX9VhLWXlj3CKyiuyB zO`BL!PTG6EgebU^#c`A)(DsKK&!<@a(uTdg&r2at3;98X?u}X@o*%Rz|7!bTrw|U3 zt}#d__JAkDz1Q8W5C1rf9QiZi=%B@PNlg!J4qfxTUids%w`5k{B|mdUQ$-@zYcH>5<}W`Hcgl%cdkil@p%fcPT(2!(JI`XRCunBlN1yNN1hJbpZJMdPYur}2p7Qo&53o|R zfhuf7x?)Z15W8=gPi87vS`Zb6A8uWGbxoyHfZG=Vxi+ob5q&{t+tpmH3f~%kzWeE3 z=(PMNv>OghqtHYn@fh^<}hW(9; z_Qn_DI=4k-Sr$`TLt2rVFjrrO0gLJHU)e&!S8d;Kt*5rDD!g0nO2uA0>)h@#S$sl~ z`{B^wq2askW<4>-2l)9`Lw$tUxJ)--t-r zEi&Z+-x9_mto1{~!}n%m_xRbzk1A_Ro@r1dLu|5Uh-s5>7I-%Ba94fAnwMt|#^ynd zhK7@Ae|7ydFV|`6pMbZ_tc>@D0~cnDWUA{%=U%9s1Pl{qwJrm< zkB^|i)i5Zs!yVa{6bACqFc7q{e5hTLsyt4klsgvdj1-A!-C8?&Ux0M2?{--&n62Y( z#JxyOJxDfPSJlQ~wZqsN5l}ZHNI}9_=>zkU5@09x;z=^^uOE0X>O+1p3vaa1e~WOM zy}N4KahJa|^dL-KYd+&$s<9HQ?ab$NAZ@i&`Ln(wxafDOL$C?~sf30Yy%E{X@W35o zs-4sE^whNBR3(S`aeXyU6PmPc6Yi9UFLAZQb3fX58VQ%I6`Ma3PjgFH_RB67fbz8B%72`z&?~I<^WJm8cS4m` zOhOK$U=Dm__8;VeMM5OZ4{~QE!8-XhR;8AXCGUSRT(9@#I!$OBwWL~LYvlc8AYXT zfk8R<)9EqETdSA>K&m$bZ70BRW9rYPgi=wZUZ=cJ3Z;*@F-2`55R~6_<0;!oTYnk(hB(7EV;eM<=1#7@J!aQ ztfLwGyU4x}Q@<&>>^65GptZz7Yw)#;n!mRN0u@;^@uSRP@q#7j;1reiBymgV(~-iL`c7cxIiC_?MHmSY)OHxZ1g5( z&9O@_lWO$lq^95dO;&E4q*>dQfP&?M`JLk`rU~Yta3RjKKac9RFnaNm{7NBCUp3se z^`#rS38o1heY(J|@6C_DMMm7dWjN%yM8$($+!-?kyS`2)K085_&~WD$wHiUb3m z+!;Z1K(lTr8eJ}>HK`{=Wn@^8rnUC)=qrT-0LfNa3>V{tW-u8ofY2jql=Nzw`Mbs6 z4vrR!x&D=B=^OCE9>DRT=;Z&qK7Ou93Z1<@i_>=RsSn?<6-7mj>3MU26TcM222`gJyw&wTEIl(|RnCP_74VI9=@S zJtQb9v}KqpFSk*GnVJxBsw)U(_5jMD+iSGtf$xPrE~jHT6Rj#3y{3UpZvl-;7&dBy#MVtf1mUlzS%|Rh*KeBUj~5A}Ony#=xJoO55srd~PvrYU@d@WRuxN;AB&oDQtP0qaX9OL|#}i zN+=Kcl#E7-CHt|_?j=^mi|-*bKz9Q^sC-vM>_k)>4&dXwExtW~>Fi>GvPKK$r-Zno zJb?}J4EcZ_VEBm43dL&cd)F3f_4<8zLF|IH2d#xt1R^9eBZEZzlZ*cca(A2;iV&tn zfTHd|hhl)D(rmvv8&57-6~sWu9DhRWY$o7czoGUiP!`dq{eyli9LodzBTkue`7CdEMXUAx#jIee_2_QSYYgwd$P z77;#JF7_?;Iz`e6;ZJ;ojiHw9g^jg5CtYf#p_CfsB|Ja>!TwY_^()BeaXQ6UUkIm9 zB=i0um-o@prd)+DL!%!+v`#sgbGgut!(sb3r15xiVvJ0FB>rg^4^+WA{2GH@TtjOK z->KQSAjbc7y{W<``lO-UFr)Rr_kmg9$x}2=&s|ymC+7V9;5FDJtnj0Vph)daClZ2) z5y|ZM9w_J>;g8soUsxq2kaLhCA+0z}(t!}L&-k-yevIXHelsbT??mDJqv`s+`vEz) zXs`j)qI8yaUg?wc+O5$Yzs@C3=Sz%ZsA}=p^F06j68#6quVWGTL>z{Vp1BD$#TzTY zVvSS4wUorc0yAKm%ZUqr7-^!Vz>v46u%0@&xtTk`7e>v1HrI>_Ln%jjG!?3Z%m?oX z$WyV8CDWj1X<1w|{MCvn@xSpm+#ds}1nM@G|4=!0hcntXWCp97A zu#SBd3E#pv>$DXC1cvenJuF3~1k0S@e72_7Y4f=_JZIauR+BRiV4DnGro&OmC}$S_ zkl;SSi}L*HcQ{nd$V8)X-SddaS0C~fo^wnce*W(#y6Xu*W(wVavP9;pjdiMHFx>d&uoyjbw zoS)q=6&_;7l?e#k)}UO^UH%}3L?(dPxB#`w`50+qNz^&YeJgwl{tmtnF0dF?&9*8h ztUXx48Y{JJR!19`!T^Xl-W@<B_;L%`7;Xy%xW%DSwFF_a@dLfTqAlC_`mTfwX#i^?!f*GD?z%t%LAe|an z)4DhqP|BiqPG8yaM^f zl{zi?QpJ=_C6rGAuh(>I#EYfWoB3kp79u26YCc~RAq;90;wSe!GId`YaZ(hp@Dex% zV}z8uZ(8D0biCEVAV$wxrmfLM*+n9cJMT+9odjU&{8nlO@a^Oi`8_Kn z&P0KQfVyQ8QA-Vlo&TE!Ip+P~8Lb|%;B2GFTcl@wO^78JA#QzeG^fV$a`Ubsq}vC- z@NfbN8aWejVyXT9wqSU+;CPVV!#iyrk259H`P(o0de%u@vdnUL+bUPY2V~Ue_kEoY zbc=mMyL_Hl!!@E0;Uf5ZVDQ&hh@}7@$C{4$Otr349|1Sksh(R4jAO5zeiCicjJE?QzM@8 zBtPbE6)_>*2^yW6sU;vNx4%n&?x-6tYM->mPj}=|h)W1ZYX#rK!NSfc>X8KGonj+q zww5$vR5`N%(DOtnMi&IqUI{F#DJ13bbhhfc0&-?)b57>Y>H7W^1Cidlz*~$7X%w#r z0h{_(;IK>6m~yc)Qh>cjxZLsgdz}{F_#ErR4P`;hRd2X&j0M^J`tcjd`X?pxq-C-> zv5SARHZtFsYGvEuGujOpK2#@Do)Re47Z@D^Ueneo1a#Zo)x6}MzHyQQk8OIxj*bpo z8JVu%jwi3cq_rwJSKvZHh^!z6YIG)xYl}9;)+`q;JKvBCz7yOgEBRa+zV&;7A_uf` zq<96G&RG3&I|$k8Wd9(DmSj9)@@d1uyZCgNN?F1v#h7|o1^*ZL;*IY1IG?+b5_^*jg0EL)X&cPHVB>&aLwToW3v4%)p+?F3bYX zfj5h71ci&}5@im|hm?ZVX8vKM*;k?Mc)!m6vIw(ACAS56A(XKkps>|=gsOE))!zi*D&uwyfAu3p|Cnrk( zT|6GXQB-otAjg{xM#bWkRd7L4G=8-WtJrQG{a zYZH^R4r{ty<*F+r+6VF%w57!yl3~Xk-Fq&-uQWmCO1`21lyVM~5Wl}%N`<)G(NsRV z)R)q!cN1Mxl&J`T=o!!novq$2fn`?$c2K1oZdXdUt`NiXZKlXdT6xQ>zAD=))n+=~ zu0gD(lRIL@HpIjU)RCEHugT-EWwPHhJ55PjYSzP)c}pn5%3C>7Ku|+4@x;F!L8c>1 zmq{u%#2LFI2SV0VOWHMvLL>%1t%ey`;yjR?$dPHx8i-mWHCCrMQt*A1p&C0Y?w`yR z>HJmQhp8c#rp0dnBW(hV>1BUA-I~xbcmriXCK2QzkqJ@g?}Jvs+%DG(@OS$F8CpMQ zgL@o>@9IteGQOpUAs_r+rT!~)0C4CVzfk>-R{thw0v-Dt;HNeZ_7MwhmW;toINIT` z+vHyUz1IY$0{;)ZLbIuK8N=QX4mcW6bY?`&NZSo-KcX+TJEo7Eh zyGMXH>U$xbSq?&vs4;0y8CH5CUX|?arS8va`Aup|dcfld;%BMZ7gF>xmAKh&D?NBF zRXATO;>%vFAD**op~hOL)jE1_T!D9>?``dU4DR+i@)~m+<&{R2jTWz!>Z(mTx5(<|o;*-XCwJj--wmOAZikFP5sj&p0E0yI2mH!Ni zR4td5IkdS|3Q_4Ccx(lRma}(5O{X^je{G1Oq+w`O%1q<@vVRC01FM8wGAiMqY&q0n zAhww`KO6;RgGrZ2uF(iedqf?;z}0@|{`KX-oy7~mo|m=4w2%yYmCT;eBrEfwS`zI$ zptY3`dXeQFjs1o9%T6~e@<9HNZgP!kkQJAO-38J;SZuh$Mri_xw?YQoQuK%^l@YQ3 zjEGK2jzYyGV(s>C2}Kz+I;o!wpXJvxKc(}*@HFjz<=uGS{0NDY9l5`aGmwh?mNCyq zC#v1T#qjRT%R##k%UUvSs3Hm<2s`5}PR8wefepPHHe~{sMcp;AkzpL1z;=NfiwT{` zg1jI_LYo}X-;Eze0YJ*#;loHQ3LO2y#=szc?XrRIl_X@x_X*L*f{xJflc&Ss7roPl zsU+(Oy8!Az8NiooSVL!{^gym$1#ipOZ(O6-ojFFIh;jm1NmU+a4fGcB)ah z%@%!TP}0h?4eHRMkBsrlIaFVi#Vbpb2uN1~(m9zfrGJc}9Y-0GSM1f|)?KW{4)4;` zQE_!D)QU34bqo@mGG(M|I$M<<_>>q@1`CwVC7Ugm%y>O-sL@ZvypZRjVcAapb^G~v zQ={0oDIE?kYAo4r?}wR9;`4)7;iV^GA-t0XCSmmylMt9=uCH?G)u|#FVtm&O5rwT3 z?gTflgn&hFQn325QIK0=T*#2Uo=C|BAwS}!0G5RuPLJQWGl{A5M}wIFWNbv|9ew7_ zZEX+?*Tfp$H=km%t`dy=TJtO z)eHjJ_VG?iz06kl>09}oXW_6deYXFLG@rXETnR1^w1)nIJb?iY0(za5UWDudJsir$ z5Y_8jrp98BF;cUS1#+YF*QXCkp~xR%V()(=UCC>qZQ~%A zi`-2bqASf6tH`lC&Y|1(3g`scJvK|2j-T)tw1X>1E33+XI0k~?6HSAYi67~uT^;Js zkF1@istJ?hoshJvMs4mNr@krKOb)o6ZiJXAWDc2NLXHOhEH*)1P6hNJQLzO4wO#E= zcx4?{w^o%K#Yh~s)}xdQHGW)L%+8>Pqgi4lfj%N)!-#5};Vg@YTsSo!#x-z9SA}** z4b-}&F4?~(7WM_ZRmboPD}rGNJX^~GCE#Ycbk}t)Vxe7dggxMoN?iYL0Qu8$EVqXy zyv)Q~#4)zM<>`LbU(gFRl@b|qS3dT77y0H8l`MUO2oy8(`*=_%(prf3N5M5;5JyRk#Va(y zOhSM?uHT^j&cR-nNjwIY-OE0vLX3R97s2ZGsgqqJ|K03*vr9}PSHIUNXgOGANwDiH zUgw)5WI{;3zJ;FxWae_-Pq;O$+7=qq%;d#JWB76PRRiLfj^oXlxgqB?idzAW zb_VaYS23M@K`i?>1zS&u$8nJbqKlOWB-%YT)s-tz}HCf0@_`5v#htikZQ+vDMUHLkUKz2E&Tg5HgA+z+l(I4y%#x z5u;d{V4_%08`eJwA2E+;#9rXH`mF>Q=mxN+OwektPF*ZQB2N(Dx&64EAByo|%&)T*h4$n554*Dkjg z6n1cp&##BpTX`(IT$y!~Xo=huB;0u-g0!8K3VT)Gx_Iti$Y14>BH@0I^h!B*puh}RcpWOm_u_s=plm)8TF%700JtZ^^P7}Cc%#Xh7Si%@-} zD%tCeVGj4rq7O@lbfV_UKa0j#0?0PbTWdE$Khxi!iGn_Cm$Nlv#C{VwtIuH`RZznn z#+asFK^z@GW+jL39GKGh&MN}f4kBPy5@dZc_%^W|0^Y}RF81`_>Ae(fRxhtPYSZK9 z&SbFgv%{XK2IXomX$r%*V;x94CMv`QV!#ioCR~GM7bLlhLj4wlzoO;fm#531pNQ&5s^vO}4C&4)%Asakdw>(t z63IVy-@(cnmvyyhkpFRCe7L{AvUu*?`tMHw#aLggr**iiWKh&Ie7GC!0={`46igqP4KW;8s*=pq%;MC6h$BxCXOcCDKrY%!$5_su-_LI z({7-v+3NIPCeo1oCEJ=0WInH!yvIb6#B~~%gK*^dy#_>4tnu2L#x)eglbNtVEw9PG z5CQq5C}tc@uju~H8lUek6~Os<%?^J=JG>qkIjWYkYxO!R>oBh#cMq1Dwb*|3JXrwA zl`3GB+vnK}gB7U^h!Uq`?%z8`nrG|rdRm?_R;GqIL3OVaq>`SC@ID64;$o2$o?A)q z5g{%{0pW?pqZ3?W`YXBCT{Wu47|&P8 z%adJ+Sd8&o`Rns*mL^MT)7OXfEs_r5hz8lD$-x1v2p_k*$C90eOu>`bGrGHwo?pjB z(_5bV`&P({r+*hEcQ=k&Q{9l0KoH&8l*&pJ7Xb|S?bFjSnH5wNXzm@RAfMbYQoUvs z1mTG?;3_7Yk=KN%>R_AefmRw1U>?YzRTm;K4=~P5@t1&p*TiOb@M51Mpu6FCz9FOq zs1j!r3`UIo`Z7FxL(&fT7G?qJ0}h8Xbm|pMc4F(PBqP}I^rQ?V)s^dQ6nCY;!fqg3 zMnqBrPGhxh`n-(pIy(^yI)Jhzffqyg4ne;B%hYj36Est#sFE%G zVT=2lH{em#cUMCW8$bc^9llh#gtD;tL$>GBrd%{^c~TN~nnj1+ZC@llVCcah{CvUZ zddZ~uVNeWLji1wcsYh!WXtY!(Is?Ou>3vv8#w4W+WK?{5OzJ)j>gb@Di<)|$mM#GJ zeSsN2om|LoJ@^w$>KjwupY+;I1u@5(4A*%zo!)(JBD~84_?J2(;q%zxG$k@(`mt7@ ze*J!l$GU+{tNaJ?CX>HIVz0`&>%nv|wztGto*x>n(3V!cMDhxoA@@;kDPWp4e!2z= z;>p>DbRJFYH}5pQ1JF0psGV*1_P#tnubPXViXuTVhWgeYGVENNhRjfy(7dO#Wl#9Y zA`b-K5QWW1v@@EP^sO&KimAy@cps>Z({XDIyMsLRg_1w7=)dD3fd5r`B9^HYQXS;4U6-jvTiKP*eGhcmVU{$VbEz zLTFRdH4F%c%b{fE+=aeWnBu4wB4_UveAQRW{HmV~0d^QTfRXlY0Z%t>^d0`LN9)sH^x%3l{c(Yw=(lOgm!S6f`;#G{Dh7HiE4 zrQ)a_RCR~^e8Ij2ovyd*e+x=A2nm5!4QIBJHyf{4vm+(8=@jamd#f*N+}HW2VV$p*43 z{8B<5im^}POve3(u!4~A!bho%@J+j=!sGnVtDTs5nq{9l0y@?xEHGfgaKCVPK3;|@ zQ8{O4;{VLQw?jkiN)8pZf;yfuooknY*3P=WP{oORyrBWA-=Sjzye8tOB%t>seL=AG zFi+nzyt&P8+JW=OyFc!WIIHo1LArl~EabAr0(7_rvTq>5o^tiKUK+)3)U?XksW&`{ zzmD;Izj)i`zeVB-0^}{GUods*^78V1cfKd_pA@?^KA6G~s_IxR&;lwasrqgbxJF`& zs8D{sKE4xuInOC!lQL1`LZ@&`$`qJj#r2$_1y&}!DrXW&XTS2h7Nr$q?*|h^bOt28 zR&lP7b$N8JZbj@8s1eDI*0462_R58Wbx`9 zgxpKVuX6K|WM-&lM_8NNV&%ey`xOIAcVH9%kH_)T3E(Uj+~3%nAuS@*fea-GBFqsr zYE~3LdeRISZ>+6l-E_bs@Ljbw_{{n0x($_IuJ7Ov&5_}NRg#n5nvRy2RixOJz&}>0 zoLECbhVoeePJ?VDh8!js**A!y%oqt_WDsFfi2__0MW6{w&2p{`_YEuxU%>Y#;gSfU zJ0e;k6GC;}p6Psr4#~p6TC-;cdktPpFSp?XI~NdT^MMJHfFvs5&o&cqK$ZY zGc<55m(ZEE^3)mz$|^vocEfPneF=>1|M8Whbsb~#r)*}sM=nT_Jm#rbnE-DDP52~B z_Twio6Z+$_v$a+kfDcNFjkzdD@r?xK^I)0t!TC;)60vYZhq=*VU2!_M)k38RjYGqT z7JHg4uz9pnFe&7tlUZF42xZRW5u=onF*|P6dv0N8Ds`q;N{_xJY`v2_o}%di5*8Vg2u=>X5b%p}jIa4X}dYpkXMUj&!yJ6H8>d=Ae`jRpW250^rey5_5 z)0Uy&&`_*E*`Y-m)ntIz?HHG$oVEcyY%niZKj3yYwbtt1y}9_npYOva@LRFF4h&CI z8%^`2jLk17e>s*?re@Tp-RlLm^uxVQ7%T*7AXppz#E@TEmm^r}d86&RQVf~cLIoDH z+Qo~Y(uY@~;&r5Yk?WT5qs4@Jh3sim9|nqQJN2hvYDL<4NENs{_#wP z!n6Q~R+Mf+5H6n~1cBf~HZE9pXT9$R3@=#=*;3PG@9azWO`lUgFtbLI7gA;eFN{Od zFBb-6PqSAXlTVEpM~j4EH;ZW%`aBYAdW^{}eGyoiCOcuW7vE@Ed4dNpIK?p%>gj4~ z$|HNUmfn7sflREO#mM#gQN>wHb+uMHtPTjM89ptLuVpKj7D#DysQ9ut&2 z=RCJ;dJEa@UO&+|k-qy%8O7djPu*4^b-G~pZ)i{?U*@d};@~-#)vqyztG8N=)hZr< zCTl|l9v}J1n!AsV4AzY&6D;LDL&poUb}}Z(0gNo-`7&4`6N4N0kNiWAw|l+b%5b4| z{V(~F%-^Q&D%WQ?u*8&#O$=q+0eDjHr7H1T$5AAwt9yGs;=%M|x(vz~!*A8?A1uFN zgLIx?OH*b=tco3yxm*tv<2?Zws)`)~z{#)yU-2-2@-u+Mb1sZL?pK5ikdn;gv|aee zC-^u)`lzb?Ds1ItQTD3XJ(zz9*{Q^$@G^3GRmSv6bkHe`VP)IAA4&#nG4y9qzH9;- z5%FAh-}L+Q0F(>X7j_2>3kBtsJD706sbk#q@AeKgJx~w9LU4!!C)5}V7#(3V1VUFX zA+Pg-i}3+J9~syQ`LVHKm;Kq=7yBPS_F!N=`T*VZj<_GrP17J)OY>25^a56B=n)?`8U~yBTn3>%g$=*9S3>$b13*jgD ziQK2-7L!(G<6IBG_p^aMLGWdSv5UJfSkKJ#betT5fjtZhT)EX;ndUot@)!r2IBtYi zyw=p-XA9N@Tk1pdi{lOwx3O%=KryH1m77A0kyxRmUC#9Uo-K|2U#{!dd+Qyp+l02( z7g5EyUS-X$Hsw4G>9o==$Kna->zhr>xnps;jA#NQuI~ea?p+GIcpNAwEJyuL=i}UE zuKl}p*LV3q-hw=mC;2`G3Q>Ba=qDt@)0}*89>#Dc+le%PawcirUAp@>I}NX z`-|^TQ(-8vC;_1&d61%Rovn)KpS;^79^@5u(P&soJlfHUMPUyyX;N8;zaCh}EORJD z6k&WRto7Zd3X@rn$Kt5Tg`ZB~rThVT6fiJIkg_1I)WZ>L5iEfE9=!ub9{q8?$zy%` z>hmSEe#Ed{;UR-oy^KDk<+)Qf?7dQVM4bq}gMb6?3}jU}nk%PlQQy}~Wn$$aXf{X- zgZ)5K<3h?t+CWk>F(X`Ow8THvl`G5WLd@>!@mYeG-$H56-4RC2Ai+O5j2U0k+_*Xt zV>Q}m(|rXeh~Lw>l(_5Ib3t>x)jetSyN-^|hz;Y%V{~v=DERF^o!p4S48SfeEKRC> zB&m58Q&TA9`swyBC>MYU^>}+SjC&e{L^yO{U++r#sjU^34Ci>cQGpCs`Oga70 zk=MurS#!t&oP13`_CRU7V}Tgfz1_@a+){rb{Pb2oFocXVt0bN-WF&?x#oD(MMG#bi zLKPxuAt5p52vJueuLdT}@^P5Ip)?Fo6ePe@;}=r_r_WEYh!hN83mF-C&d~-S;&UO3 zilS5#LaeuW=jVIBhy$ZG7e_m3X4enH)NWU&?CnJD=Rn;qvEN;IcX%&z>2)Ti-|m^O zzQ*YCc9k^V(hllpbGtpX1B3{6c1%1v;GE5-d`VECQBcw}Ss5V!*J|-|Q;wv=fAKaR zh)8@+sunh(m8Gk_an`}WUA3M+us+HB-sMv!?QW9Z$Bwxa7jcRY5cQzpP0aUkpU0f3 zo;5F*$Zw+IV$G|Z)5~|ZMV>cpj{7S%f9|#mO0?sgf31lAAYL~{vl=_jx(PG~mxZbW zK}|SzGXl2;%Ltrtjuss$Ze$?rp+M1#8fDnW{0aUzoph+?>(iZGk zW)ePSNqDcgBzjF@2JHr8^pI$RRV;3M487N2Xw^yC zo5wMyoMM&g%r=h=4&akjjjvf14w~lC!wTw}L;sTQSW-NmUy#2%?0u%yA4`Zzd+lCG zxdkK?S#kku;$!^CswSJ{yIkbU7THnspMxJD za!BvXlO9WIY&W(3Zq5?zZ1pQhUto-n)eYW;)On6`3&$=3zT4A)vFtvOtG89G0U2$P z@n9?lqaL5ddbIS$Df5jW2^s8tuwVd1Evy17 zz$~LGb>k)TfszOj4uqfV4acVOdcMB~cxg|;8Sx13I~rvZ1&7rXku+YNE@47Y#uXmL zX1$GYvx;diV6%?hhW#QPaf3bAc}#fij&q|u+qsTc+<3Px}TfFuXK?YG_5)WLLNB$X(M z?<*xAli6#wBKG!kBo8A4ya;GSsILwgii{$qHy@YD0L)AsxWhW(BKff-7o z{wQ1)>A86m0dM?SUO|uFz^npl2$~}_ObtaYW|j{ZLD_Au?0V@bf2zwX?>QxW3IOfm z=Wdx_;#U*oFzSANq(W3opb3KM%mdq=TV7r+N|`-eT%OA^@sb}@rob*QYt?P}nrs_D zSqog0S$_~v*GvOMJsh`%DPgu{re`KWV^@E}Xm8*`Nnr28!O|^I*DSC1Ctv462Vge# z6L>|0I2gbbt(7X!U))bVVU8*$=Fq}0?opWE>-j%@BqPd`i3vrc^-CkS=65TPW#FQe z6q1@n*o7`gl}1@uX{w3q_f!}=4!k}ra5{!_*Ld?%yjxGI)QhPltKCgUI4(sd;K~|! zA#~?4NwH)?4T+vaw3sWCg5`$veW9=WXFw-kCP@jf)O_gw4cHL}2>t~mK63$kXM5e} zfemv-QIx75Q0LrzAHNb1pS(y1ojjZ^)&?gs>dOPPqU^uD;b~tqQP5g+RFWxMjy2=M zv4WSB@SV4XM>LZWI;!bX?O**)=vh2{e+z{p_yJm6@mp*DpI{Ypxy~<&h71w6H5k9P zt^9UPtFIl5r6Pra;fh<#%%Jf~GkE>+(q}zXHbVWErWVd&403FhiJXA>TL7bk?H4_v z#iaGYtSk(UL(u_5gGWZ_<8~mMGxE6XPRi12bt6#0OVvuo4f$t-kiaHKfjy(YzTD{_ zPGm!NB6g0Bj+V&_8eG65Lwv*I71voU7M(xAhy6^3mmH>VQs8t>M8T!2h5g5K@ADUz zdku58IJ<99FE3-2&W8DjZe)Bq-J)IH-ElR>gT@>&AL;hUu3%lE$Ouwa0mDM%_|5H` z`->=@d6F92wH8tZ1%+4CKyJT~e*&4G&jT!IvlZp<@apR70{BR?R-=tvll=}$;-LIF z3YfXm_mC1;Ydp7?rSJ-!W}#5@5(-3GG{HI$bfk?2d4tB@b46DesYUk_GBMy0XjY~; zmM=>MK+5Kc;v6pfQkL!#BwaC!SvpKmiDQ6d^Vbq-!hc?%P`)H2(E{FH6*rsRFkQCq zR&WbtrMByBDs3JQB-bRWetuw6GgPw4K9@U#Vj+`Zd62)5iD;&Z$VdX%OCftrg+N9$ ze^XUb#5>Bs;3;67kbne?8+L%YkDxBE;@?{ntU>_>k)bo=R(S2o; zP&7Ss^63lle6B2k@F1fF3{Zq&H}M=>zact9N`gKtY|0G+CJp*~^aP3=?)P+q<0ts~ z2a1wn06j<}5pB+DM!v^)aP?|HD5ylMq;F915K# z#W6XMTOR9^r)2+U3j*H)m>@W9*Ybcx3_LGlNv8xJK|K_^`F}#{`zv=7&lkvulpCxT zyKcCHlH$-=@bxP4(u4o+Gc-Q>0!HCVn_YqBbD56Dyih&hBZyYLz?Z7LlK=TNgF;w7 z_uGi{jEuqVZvXW3^y6!Kva!0rkyN{K;LHDaTNnT~%3Gq+VFv>2+@0N^Q7xCy1Q~pe zJdYQc^(Ewny&ey!^__r1^`BLI|8|V*3s5U20c-FBfN}vCXV^UP`oi)UO^!S|Y%bTC zcNe4PiiUnG^G$YmyaNzo(i!b41K;YO?`7h9Kz`4x7sL&=2LKOXO#vB(P3S4wQhEyv z1}(N1!$suo_no=-7x2xnm;4EcZYl$vEYDeCSD*ksczt<_CliC$@6Z45>p}3Huhfsr z&CT_c(=wG2Hif)L*`PVO*y?XtG07TdxKX(JX)P>(u zXOt|t`r1@AXP~_Gt&8ve|Np*0T#&6+NKm6P1GC%;1uTyCzkeKn0)b}60V^oNn?|cj zi?ju6(bdmo#oBvsOSWhTXR{S1Iu#;optyc`1%n>&A4@pt^&k6o_lg|N*_mQkC zgjc)9Sc=DSA0>Dj{u}GR<;?%y4vTrUlaL02>~jg2fm{QsWj`v(Vnh5&xO!yBkl%&=IO zOgmd7D#O88un_w9`vbrlfdDZC|MyLyKv_llEC6-zxequb=pk%}&1HqezI@}bbgnJq z$$VwQZl=ZDA(hF{uX0Q2Ka2gJZwBtbdRY2h0C{yWs5-~-q0dCC*}Xhd6iZH;&aYXX zu-)Z_43j}GenMXK|28l1&q<6?QCLd5mjMPDu;oQeN|UnTYWw1ho?ZUrw*4U7;+^Wz zL@%){Cv!{x28;$^?J-^P z`zvYx_r->zhD@X`cBHq^i>an}(~HS3!TtAj5J3Rh`_c5EWn{a&-mcI?ZUa~FONa37 z|1Pn+P{;7_uwDV})eI@h=0KS(HKPO}!QAp}diPL>L-LJay`lopf6m)w&{CSSTBLD4 zUcjJHqHqmlMf|R7=V_N=*ZMwALGq3KeHdK7{R&V8QK>bRqt$ES@_c>f{sw7mW=3o{ zf>^e>ZE{f0&J`S~K4ys9%RQomVm*O$;Y+RXOPKo#BP-UV^eZrK{mCS?4uek33O|BM zH;4)X=yl{j{ji;0^p2VqaDU6YGE-1o&-A8B#tZ#ty|YNq&o@Qz0M$}mKup*9Y)x)5 zM{F8tkOPq%g>-;Ez2%SNiEx62YR;O7Ti_BP%`&X==zg`%=00atqE@a|G)kU>?D2SQ zZZY2mN$KF6?X3H#2+(TCcqmX$pDXqF6%bZlC8CH|a$T>y_9om=k)XH@fdFxi?TbXn z^R>n00yLE;^*R>=X$a%{UBT3P2e?T}j6Eiqm|FQ}z@8rPa;`60uL@OZm%=}Cq-|Yb zfUrzt%#t#KnJkFJe;O%8@XDU?S;5jswgQ0td8w^j)l`vdp#d1f2M1fqu^U!@q$643)pa9JIGR@cS8!QrW+Qtse{3NNY1AfS9wEg;h5OhHN77ICI2g zuqT7Q92MIg^^ryvYHqPB0)|=c<*8@F7AtwoW@N0Tki`VXP#eb|@E12yu~wR#GWc7( zvA69`TK~>7>L0&g__29|Kyef@Qs>`+nI>RMfx(ievy(h8`kCgpFg9P+$>L3gmIbO}vLhXQlVDAv9yZj{l52xE-}z$=G{ozV7!+ zalBY*Pjo!9O>5Emn+-tutOjbi2K-Zg;AOv~AH-{>fH-aD0HSg=)*(p1O1S z^a)`oEvHEtEj=G17UtL8{@DpWM9DYoex6$#QC6=sskOB=XJ-ABJRrC&VEH;8OjVDT zSxdyMmHP_*CaL}YZfl4B5s#NiB^MKF&+i&{DGj>h;m7ex4A61LNw3H&hl2>A3p>|N z6ld^>oA6sy{!}mHhIc#6R-)F7dh%e2aHAA}Wq&vKF_p^=EQ8N$z}R?rU9Z{0*R9T0 zQUduje}B?w;>`AH*ebNkriwV>HRMI4)J~V(2x9s?p}V$U)URtpxE3Fj!+?eokrSg$xK6Z@S01$X@7LPVjW5g>B`iU`ij}`T__VI~G33k@vBxunNc^HTfH*z%&^{Ru^MWz>B3!6IXadn- z*-u5~<87_2K;lsVarwt(bp4%z0HN9Jd_$)0n}`Kb8%(nmeA(9Y#&^1sr#aoH7=va4 z`Wqp(?e!PdTB|m``CzRAT+AtCXz$1IxtGyA$rd?`bFTF4(bRD-{{BNPbsgx`Q96Dh z^pL|s3=W+#`*!uw&UGs_|C3CE4x7m>iyQDjZjL7gfT`3tDr@MaD!0&EHPq$&vWGU; z7kEP?F`2|%*Wm^hQ_tpl3Q!{Pm*15GSGq>{(JR5oe3FED2xhC8#ixb4>QQ^fWyZVm zryha(LkXqArlUP_B?u6ksNm11h=dl4XI3sf)=?Ch{kww-g)zQ=8 z)H-w72l&1G3;+Jn%b0`=v5xq%-;R z2Jf5>LV=o0iy3Qa6V*JZg*jot$8S9mQuRaFd9%a2%KfJ=O$0q>XCl}K-NDGxylEM7 zaC-dS;&5n`yIc23v~oONzM?rN+;y@G4R*} zqKx7bc=@WI75|W-A198@>uUpn>w+_vxxE=4*QGaKJ3v&bNyNTwTbR$&*-0%k`9LH7XaMj~%?I{r>a4e< zz0Pr%RM*Q?k9A(5U+V>F=nJznJ+h3a}cxgA)NzQoI!q3Bo@OT7KZzbpSm8R4vAJdR1Cb#vKl2YxYGzN zZ}ld2(N&j#rBl+O{%hmPuSNsZuwtcMKPIf_VkpkL;UQBHub%-q^K}1YWdg^jd)UJjxDb+74!Y=N$>sbYX9vVrO!rs(XyIe#^T|) zbXdA=-xI5@^~v<{mTDbJt6RPLMe-whANCwWyMKbdC;PW}T#mc8e70UIeh+|Urj1F^|4H^p zqkLAQ0pD~dr7=Zw);|-m>q%cTYMhF7UN2ReBr$oMj^jt8Ar_To``7cdT{z( zN$l}tK1Gf2YPUXZrtoE4CD&L^cxl-(sS^Jw*kQq^8nVcU+75};56zF4iNUAgrnp9( z;Z*Avbkr}|gp%1E+zu;2M#UWQ`~`qA3uD&fPO6QZ-8Ysn| z|Ll-rwN6j(S}Lc>5`lwb21bkZ`pUVLehwQxiJ=%?fs8+w z@%sIr`SJwj&eV+A{iEDmlyhy+O@f;fLhWvNpw0e7Xx2=ND46h^XRb6mgx|4zI$_*i zZmewS6<9bp{IH~AIRpUFWs4ctK*{@_z8a82b@9E&Ps~qh(E!6n6b_v_4-j@!np6_o ze_ef=sZpc5{C%|r4U#JZkOWcmAi1oY;dzHfsr1;d;Yg!23cf39_|O4Osu^?FcXRDD zUS0Qf2D=azYON_`Y?%>8RSpZk>Kzr+yCaeVX5^uF_CHvYr)L(t@%R2nai9*Ng@pv!h9?byrreOpMEg&aQf<4?jrIfpX&Px_qsN{Jtl--Uv#@Kz08|( zL;@D0KGj&l=A?O81VaY=;I;l9(nb*%YK)oV)sE_K%;OqwudTcRu3cMiH>KEhhO=mo z1y-)4F|!Zt?nk2(T;AE=th~Mp1=2t^JsV}G)udR<#*ja}7SmM9Vr{4dIWXuqVcmUD z4km}Wn`ozwI)6GzQ;)N3qJ=tfvCq%{;`GeKo1y#Xd(g5yCH~8__nQ@4fFs?19>fPI z#2@N(j>h4iH=bweE!FXOyh6w-#EPXSOZY}A;&>Anayn5>=e9MsZ`(E5Z2R~QIco%* zZ00lr4TppvRf;H*q3VieW2a0cWpWE6OsBzSmg`Je@01IaaQ)_{J3D;=0&l)*xcX$a zXK<5ew_0S8N>eP`?>&?X_rqjfIb{z21WUD?E3?nhGLLmSuiKO8c)Tw3gfDwIVo|6W z@71FK@=^EIa-AExwO(tAbg7-Q&2n)_uY|=y4*0C4TGX<|TZMj;Jmas{tcKO4OAoA3 zE!jA6g`5}OJLNzN%;fF0Hm|fp&PoOJ1i&IMkZzINYIh`MCSL}-9Di{M)_0E3eX1~u z7_|2@l5N}?EeZmLVPT4=P_`S%U;-FQZ1orMxluWlu&R zo7x_~_nfg-sg<=%LivaZ`!IHv_0!nb;oC3obVQw?tEcr5NgJ4WI)SV=v=S|E!aJAwu;Py556#*cyvc(%?HHi!&{+IFz9M=M_&& z58`w}ml=PLO%qz!)*TcEy#B!6YCzeiM$X!PZzceRvC5a>7d56ODfRfHe^tcUcKnJ$ zj@eQpC2wG|$ zfvs=iwu@9oA>hL2@pzD2I+l&w((A^$tUAhJB!S~Ike)_0)2Wukv;Rrp2a2o?B-D;G zaIa>OCC+}NZ1@|RC%q?x9U60aJ-fJ`e9AOz{9H^vSF7)R0||^tQMeY{r<48?{6jBp zkdaizW0#9;N64J{5213UNo57goTp51A38^4sHtCAoDe@MXGg8MxYFfVN*aJ!+ddKT zJ2Yl~aY}@v9k|+sOasjq>#k6T?5E8O9H0ETIg-|XT^xbJ*~;YdfWXi7S-`q4218%G z3FFHBvK!<@Iq6De_tmz#*u~0vb&KH6qXC6KkNiYysdR33%75P0FvJ^kG2wI*r7TU z6PKs~#1Q+ee#o8F4q32jXJVR<4knnV!z$?*nt^j__%ggmE9tTM=j@y(zu(34-cdp_ zB8ay6S7B60E>{NqYFRN_oMSq0Vf(#pzFL3+g=6LdS|{DXKo4FoR4ssN6D@RjecUP4 zO7$F3Ey1ij&9&g`|E-c^1;_JykL2UADM=;!p?cw^>hOnqQRlZc&(lJK7zhuC$m|is ztR^u5WT$N&+w2w{#shET>*;>thN>k|rgk`5f=M81X&|&1Z+#Q?jz%+)2F-d{8&btI z8W1@nS+13F%Ox{)0&P%OeAtfbEVK5QAyG=BBKd)O4^?j|Q%N~`i}<3lP2J{~Dwe@e zOr`E2uK%Pqn<&0aLZ$#gqe;e0I-VZY?DCQh93t_6RC~@Q|FdT^t+l%G;ll5@i`ebK zOl|H^9Ja5vtsiBE6f&P-%h4$;mkuohI6@x>X%QfXAOf|iLTQVxaVtV>Z40YgJ`n5GaRj^Bm2L6L-fKegs;b|8tl z9ECW_G!%mGh_l5L@Tbo+B|bU$an9sfC}Un)(wK~Si!2{n5S$-h*b3yW=j)cc+S7oOG8#ydK=4Z%Q~5w zb6SRVu65CEwwkzXySX(*dpoh*fFlE#U8@!K@biH)zom>I#>ldxI-D?v*?&9|FFj_B zz-HH?2-B5?c=1a*X=l4MRI#w91OwO#kPr$&iIb=RYG#9_GV`BRqyn1`%hLr%5&$g# z=B5ZjP7Z+Q77;x_0$`J6s$%*f$OkXG15_7X4|QCVs-p%!}51pvHgBL0zx z;9yuwz?AzsPtep-{S#=859QF(&kgM9Xd^0^P^Q08ocG1!C()9G$dhy4mwrTKow1AHnDv(c04XrQm6mtd1 zWE#>sEHJ)MIwWI$hC)MOtJMBE-9)z7*~%NlWZ7t=lRp

)@J;N6BrJ#6j?PitHL= zsL`5DbZX)!siIJZbl^Ksv(2%CtuJ%?i|9x40NJqWnY>)nVw>)Av1!vYpIX%INA9_@ zxy*(yTV4@=>dPxzcOF0|t$OV_7vxWhgC3t(kOzvX5=ZuG-9Vyfa1^#3u6in=Xl!t0 z0yEkGpJFtfF9`b9V%XturG~@Y+%jgZ(RLn7XRS@s>5s<+{g22i@th9>KSwQ#&C92* z6??&gBJgH1Zl7BN?{+f(vkQ8JNO}g!5(kg|-pCwp?`Um|NE0Nw$68n`2L{H6~XrpN*Dc9P5mI$g)k} zV@SBLx?eb;wXya{WwU_|^PeF;B>i%pj*HF(*Yh=@>*4Ribe+W_A3rt9CD5;$>cI{F z zfVa~!>isD)F!A9d2te;aKJe?%2mgW(>b>TzXJ%y@<-{*onw?n>j$exnmw%i{yvu0C z*Cmd?m_kRxDes~GJrGyh<(bG6_M0MBS9y!>MHs{6{X2{YIHt zJ>i2j4Iyt!R2T-Mh>h|`j)P>#D(5>$BQSwhL3Hg^wJ$?4nOhZ`KrEmaER^a>kXtNMs!(GCXA%vDoaw?q(j~ptw?@9?4tWo!sqz*%j|n| zefVwPh#_2gCie)?@*RY#Xr&@i9{3s*fOAVbAo{_tATUQ5Ko-Ky0NPy&xBo1#10QCp zw!Ke=J`AN4f|(8Z&8ft>(|#*R}923Gl_GVk#ohf(4&lv4R1 z5OZFE0>8YtBT+Rj7}=}tLWv5-;cY>-`KMm?u61)w0S-qy@c3WIohtRe$$JCViWjqD zI${>2Fe|qB%v!BCB!%Ge=@c~mg}Qt{{!C4k+z)*SpSU20*}va>QK=Kf)%TPF1}FY` zG2BaZP#MqR$?Li8$Ao|}isEy*Z4|vmi#>&yGL_viUAmo^C_bG}3}B5JzK<#aW1NZr z^JE_wK1Q4d@a+R&0E^9PfvGu=!PYl3-oZdetIE5gW1^1ha!~}Y1tZsob$_Ghou&ql#6F?B?_zYb?t+LI0^W|#_MqV8ST7*lwy*A z<>tnx~mKFMJGj}`!z(=$p`v+1J8 z*5*1@B-)&1+l~z#Rfacc*E);) z!agVZ%X77%!l`w*Txqz+UIK9fHl zuPiqf^J!RPTA~vx!eg2I3JezYd1&0y_Gtdulki7LEjAs(;SsVhf#u^hL=X) z@v3*iJRl@IsIl7q%2BZ-+5W<#9P@XRt!DjGMU-!~kN z{{{GA2Gtq;SYNV9{(_;1H>CEFwnP$c@oju9rDIve)6N4m#VkxVN|*&1S810te}s~T z%!tJ}ge*4aJ`EF>EKE9%N-mXLEDoBYcI1k$_3g33QvXi(E^=+_%URj@-kQT0zk6lt z9nambD_+aW&h%1(Eshgnmf%tglS`Qwi{4Qi>^3{xs4ZLzd}Xifr2^R)r5clQ(p>7% z#7PAKs-22qs#PM;=PyqJ2aUnyI^~J%4BFXY)}^yWmJPLBqpsUh9dSO?;0h3f3J@1q zW*ph+Enorc!*j8PeTaaJsEzfXL1fEB={{LplLzGCo!35V_Ib}_zAPzZi`hz=S(({r zcit7co3kQnR_f7sk&;o*hm1wkisQS~mr|or`P6fR7L~T9SQQ$rS(j_KvLyN^o0RD9 zn|qOT+Z(U`F_&pPr(!8g_I@g*s&YifYn>gSq&eN4>OvUlO|rn9i(meECTUnx^-<%4 z{T~f(^eyv5hLJ9}_p@WUElm$zHpp2-Gu&;|_%Y#C>+JX{kT1j#uyLz&s|}l4iacMk zwo;qF|EvZtQ*X+zaXgwHt!~{oDP`RoWKDJX1~hI9G+R-E1m2!&;~Dk!@HnhKrLdU9 z&g;3$?T%+MZuLhcCF!|Ic|Bi9G`+n(nVc-uU%uR}Yrk8;?{p&z)bXr)J)HzPdh0FU zhp~pMC4#^~9FUIQT%UUJ2Ylr$xjM5c{ zb45gH1DE$_Vw18iZocL$Y4F0Rf2Sao^8lH4CFp5~T3U{Oa*C+=1S_ zvtttykzjiuK^d?3)?xmqZtPV_`fG9GC+)6VnIuLNJP{=5NEe+FwaOBb>M-9$1`Cm= z(Z9CjI}-Rxd-AS3A7or*cB^*F({Q7Ab?#nQP5Sb1_k0&s@Zy~R%An`P-?qNApV-T_ zS+Y&f(?ZtP;a{$xD-~-|(kNF)_%4>GHAt^69}dM=D>F<&W|=)q?Xc`y;ZQ8~!1NY6 zXaIwGc?y~Q1sSV#i4rs7B#A23*@QOb?QQiV283yOa zho&kHa5?l)$~yh%BmIQ^@tLK>76=PeIsOvAp!nci(9S6~q)2GtbfQy)Ja}CL@%Fno zu~N&A2^&{vA}!e(Xp;%K$R@$z5VGWsIB^B(Qwrnb2Xv===y%-5Q_-r3abtYIr87x0 ztQ80ZP#FX|DrMxFy-wU9Gp~nX(pEB-JvqGk&X2J}vGv>^*l98vBiM=;Rpwr5KcCl8 z>Sg)33Ptz_pyZvrm|k#Ro?x%1-X`XuUdUYlX7TkFsx-QP>b3{BAoYQHn>b)XI#ud} zDJCF(%+_B8P^kvPFzG0MoUX7q?tcYz+DttZhPU?qClb8hoK7EFNUbRrbtQM@NJ4+6 zt1KEVI0TYD!n(`&so z#c@2B$BDp1mJBclW&+T_g!>x$iM@4z;U@b+bH&Xjo(zwuk_d%ED>t4dHh*|oS_wr%e%K~7A8ES5{- zsIi>?JebB=X>Q0&(}7wBC{3`~{S~EAD@z6pV&#Gmabx#=k1vcQO!_%8A4E3@(CvxS z7_jk1ZbWboY^rlVQq`@0Hr6TP`~7J@&@~^Hx+X_gsH=Q!VNJfNcTr6O*cq`j=<~>8W4=&5USZ$~Cpv z*Ajiz8ORn6qjwNyh;>)*x!SFdOkL~xA(gT<Sx&TsyR%`)(|AcW^ZW#|890 zr9u3?7grmny946K``g!dSQ&yj%oJsRoBce+H0q zo2?cI_(=5dO~lGxyPd7BrMsa5#kjff;)mJtQ$d8w&eCho^BRQrW|ITZE^^{x3`gsf zTo&xsvuq_6aQ3HX3EtYfRY&mpNuOAPtrx0BOcpv&nA=HEf1JT=fV^m1Y1O`t|=zOln1{zeDxT8PUug)-!dzBD$G^9 z$V!H%eyMhnO<*FQYG#UMdWXZ^T3_?+gOz$SGUKlA9s zTnryM>%)XS^eY&(bwkWjSXE3dX6oWmiGX5!Z*>$fBbP&+x0&@yE&>xef3pQZdu&*93fwxsH3^HwF>Fd)hlu;ta=YHKusS0Qo<~)RhshOb+TZ+cS&Nog* z79C)eDpW}4u@+blejTk<}uHhMUK2*kEtf;^k4>s!aG_1}2 zXM)Utjg5iLfvp9W;RkaP@Irzjo>pD%QA`oA%6c&}6-v=<8`IpXUhy|msDG?0qfieS zqtH+7#=g84Z<CFcExx|Cdj*Fs92fl`>lLqg$qdm& zpuctZ^b<}HSDuHD7n$N^d5E(6Nz4ixr3w1vpppwp@CnU~E3cCYyUL|BS{5?9HRl=K z`d%!L3oj95l3nIp`5aj!^O}?`l`?Y^YaN0NU=H|vFhxp93D)N#_L7#~N^xeSTjC2y zuvfm71yj6Cpjuzy5>L2 zAg|>0=eL7}y_ZTdyHRXP{ZSl{Uo;SvlHoGIj=kOErmw*dX=5|kr=6a{CdT8}#Uk^5 z?(K&bX?^VuE+;QcUiDLVO8qc=e{;wrmp1L4@Ihm+uAYU*dEY;r^ghUhjs5!ruG*{J zha{>ve64;8EY2gPXt|a7D?&EyKE|#KGI>_7qkW}fKOL3khEEQI36zBl&nLq?RJ_!{U&uWEna{f7L@we)K8;#82DeHl=xSlj z4AR%ARt}zGi3@J25P=z3W1Y~4(zj~s`TB@KO_*_ud~ z)3xU|K8Fuob2y{hg12r7gqK3mnJ;S}#^sYtmIf{2qzH$B5{V(3l$?eOv}T)TfUna2 zNR@TnS}7D?iSK7E@}_*J01d*ybbd(X)nK#_?i+M<$ z&rfl$Y0FyYTUg8OGVG=w)od$FbUH$DnN75pyATnSt~(V13^yU6_si?EyB~`+?@eD> z`CaU67$S65=t)nSN2l&{Mo05@@$WD2ajcEg5B#mfZ&*RT_Raj{v+hM=b8J$C>?V^V z=KU+#jo1G^KgJ9J(FU#;L4!4RdqRX~6CzHrq2=Bu9rf|v=jZsp671Ew>m-dj6UH>C zn|RiuG@hz{y+i48zu4c~sV0k(T11dWN|}pn#vWWK>>s3^or~}?Z4=<%ZoYJXZF$nT z_gEa?i;q(gBlrttA3*N3!0!(~OmHwVus2J4R1$X?IRRJ6a4fcXfgigs^eX_OY zZIs6}h6Z+cM+%nv{oyLxX0H=hjk(ACw>Q;c#&`%})Pm=p1{C_dTKQn+aCGbZ`IMb2s(0MnH*QA~@YssYV z`=C|^D|%Dnd@!vgpveZ-qeD)#m;EOD8q{^Tu+{BOitL2)1k3)q{j9hX*f?%Nk0L~V zc()&un;^*0LeP}G1TJHnLb_}nNO+qXPRtr&L~cn$poDK|K6sa8I9UqU=D4-%JdOJmYUHKWF{ovuR2_IRWDvH zwl)fOASXF=`!_IYLTiO_)ur=QKlp_3;gUKoqg$tola!xB*sPuKPqY zQNSLTZCt=U>SyrDDbqBKLZcHq^=R~RDQE1`&fx$6eMhTlW&}tnV~N{X_t1t7By|vN zIDOPxDry8)DuVOZT&`CeG`uo%^?=mT8A*pqTxRbry;sec_}q2B7rhg^sX1eov&{hH zqMOBHHNP^p=1IjYYlRE}i}@H{Cq^O9J<5K%zGYU$h?pM?gcU$w$QIwDekATU04W+wmE^1`4|sNhR) z$Iqn7oiJl=8k)4M?*b3e)wJN8@I^N)8AwTO0W^nrCxRpqP>K*oaI)G>4sl!lvASKTdPPFl z=V?--P3g&@{qK7ZRBCm)2j$j`>ZtH_T?C_Jgj7r{;}8(e0hnK2ufFVzNK&H@ZG1 zU!DvX#554h^#oI$=BAxBkb6`IZCiq$lwYZZmPo zai%4GXH{)aW7ZZvkL}9UmJ-EYI;6>$e@N=%1sKN1eb#nw&b`QG$>n$b346MN$>X%_ z+TU}pIp;!zrWipU3%QEYW7fR+W>FcKhAlPD>0CD2%`XoSF{Wp!p(|*GCZ1YzWQVJF zbAN?fVvex1y;5mfNx3oNI)g5~1v9*Q&5_b&(NtDkFtz6I759g+hxe2)-xj!97zaXs z`Nrs5UMj1f%7Ln0QR*c79W`0E+AoZK@xkx4dcwe?V(^q1W917f&riL$M*ruri4yge zP0RbcWnqqR-6XY$GCu{w=~O{8rp^OslW@qwc`g?<$qJX9t{rgds^GA*`}WECwdL3i zMU^6GYHrL;krMIDB{8*Di-y{HYL%q>`S+wLJfyqA*i)S=+y>0yZSNM|tCwji%~j~p zi)9cPYNe#iS}+AlqQ-hH-E**1*lske9d}fHw0*pJY8)l@+py21xyTL--nO_73f>*} zluv22HO&g&I(cqINf|l3w$!v-QuKXVFlvwOKLqqZueDWpm5gAa{%uqcpaqA2XTZDMezAR(FX2e`8N|4Nd{o!EpX$NNhcN3a3 z4oZb=5t~qTc2OTTAvu^r;NssGefvWIZFdT(;J`F1{(f! z_osRZRioo`6_rt!g3Jnlh~+@}l-9P~b_>SA*LLCNdyQuE;ORN6lF)6K3AN_Rn&T2- z4J3b7>YtA(E9wy)S=jxG3$B;nN0)9*XC+M%RZ0TB9s|5WL>EU##&B17iFrp$#OKk)kDt zWiP(Uemq`&OekZPSeLF=Ot`5&J-*+Ma~OJw$JslISo{|zGWj8;v1^B!R)Fx*tmxEA z@($$mqstCzWu~=94Yx_Yo5migMRHXv@4GU7w+TKWHNJ1jJgU9h>%E9&^5IYIuk?Ez z@;kYX(vS0uIMiBx_VOVN=UD*gHu~Wh_T4NxllR3&P}Ta+H~AJ4`ULE~chE%AcvZ%; zw-+-DUoNtjQaWBu#as1D33@eR{s6gKm6SAr@8U6Geu(VS@qxA8EYd7$Xwi&43O6`zM2zGBPfm@tCk+%WX)E zlV(Hw8GA5T31JKyvkiwoiOB^R?WlsEMG_Og{GpuW~FE2 ze4OY!E}K*h?ZUzbjzllF$F!P~`F4q_U@BdLt8N|gPooc<&r2rsGt7H4Sb7|R$no?^ zgndK$8lg*s11lCzc~XxVa~(F*uBPApZ(6!ZuNia14(U}==ZNdLV?L50?Z0M&a#+ zbdyUF!*K4|BDx-q$lW`_t!*l}1+}nM<7a3rVop08O~Gy7Z*;gLFZedh=(e1P>WPm; z>vjTDdGeIw zR9pD7D02P^bbTKNw37)XvAp4Sy?mMKk~w&U>Voo=hyK6xpU;wWr)wHA=rZhhdxt!! z(P>e}zv>I{-o|~qc;~c`)sen)nP19m-ob6*9Gr;nZTa>WQ%KDG!oOcuY!GZtxapfX z!VQv}b-&kSr-dLSY`NS;I;=gGx&R<&k#*FJ!j6MYhucmK<2>&0Oqu1Yo3Mihw-pXF z$ZD5C$j(JnW;BIO2mHK&>$EbYLviW>-U)cxG9o{9v9=(DNm)+98ne|XXr8JJSgp1R z{w~e;4i0%&7{eRkLYy74ORP!O7BzZ&lHLldf^G~!^qN1`X-utM!Yv+yOQktl?7Jpr zG$vwm`ALx8wz9zFOq@B1%H+(zYVkU|M?Tp3S~hi6HUBt0*q5hgefZ|KUUf0_q4nz7 zG{O7vn=0A>0Uj|JUpY}6i!$?wIjlqLL-<>vE`~4De~?Yv&EfGKIolgDV40Ya+$T|E z$F9S5)d+idH%KIr0il0^VkXn+n3u+b%K!F7?d1o{FAw&thV zU`p@1Pq4Q=tfpA#11IZlU2GnVX|EZ2on7W|y7+QPO&$3B+E7lPDpb`~=I1)ge+Vmy zdJk`Qz4#>9CYmEgCNSO)b>;C#4&p^v-l8+^+&heaN~e>^H{0{tE+p@We@vN=Kiuir z?v>uF`J>bbu!ZVERkwi&#)bd`4R6eCbyvNncT&%!cqqH&9?=EW*=k$4JI*J`uvLtm zS?}mBd^-c;%;dGSnJarxQ8`>=0zxDNOq=qLBL?r<`TpIB*aHmH*KP*GrZ;<4O(8KR zFJ5FJ0vwvU?$rHULNt}834rKbCTOnQ}6@S@@K$f4P07V-TXs){^IkV%EP+a7ED?^T_G0J5 zHAU)hB%CvyZX?-eN|P5{bIyP+Z@U0XUbO+)%p{g|OvTMDQzSZr5$(QDPtaQAHM z+Z(0J%{*T$>3n_|W^)?Sl>H2af%U$vNNYSRn7nP+P*8Z~7O7kH?{)LveFqpTByQB2 z78BUD068!zP6X(p#^^eeIWZE@h_locCciy)wv<+={Nv^yQG}{s5xG~Lr#-^$FMe_H zHDgmCpCNqq+{wq6BgykDS1Qbg;^YYO{{=8U$UEgH5Mvk349}082@635(y2g*!mZoB zZFY4Li^;D~_nVKE97E!sZ+9WNVi{=31l>vL&ktv-+}jZz?D;JP`94`r%=hM-#5D$M z3~#^L_6I*m@@9w#WzxQRFQ!3($?w+F;>Vz(SpUPE>|FQ$O{BoG&-PmsVEUHENh}SKBeeL)cR)3u}*O${pA`kulJ0Dsy z6l+~&aSGPyJ-V%7^)7_1OzM6y*G_VzwNoY73%%6igY>RgcAoRk37eJ=m#=XEeOlZs zt1ioql1RuLuJq<1O<0g|BlO7N{Raai@B5llla(0_Mi_$O(}Sr;Ln5O}}7TDM%&;n2pwpk;(R=KFm9IR0{^AFv}48b86} z_Id~9w$VIrBfSV=^U9Y`U7I>lP8=~OWHYT%?N8+@W6#o~V=#i{^18iy7YB>?Ykcs7 z*!OVXxB!=u^5Z-UZ_?~$YJS2R%4%24l~-x|26k-_7?a?txbh98Fz|i~Yf=e78N?ZU$Bda>#j7!B5gJ#THT!<fu*# zxK}4DA+rLwhulYo%Zi#g?F!n)2#S2c6g(scW=$3|EDgL=)y#h9n8{SEAHQrC3H4dP z`OF{fx-rv_quKS8eIZgy)K99LW*6!kr%X3@7`}85zs^##pZY6R{_rY6?uVR!_ks+r zCBjVilG=^OhQ~R?QAT6eV;-}!j%QuIFqIW)jF>pcI1D73ILwd!E@_SGFAbtHABxi1 zoobf$6yli^o{h2|1~$be`x47lI%C>lSCnfzk{P^v^aDn2eBN|KDZ9hZTa4P*qewd_n;(0yXv|;gB~gjK_^fEPSLZ{%oiEK=?TjV3B}sDme7{o8 zkwbDxxnEp26FuUgsVpO``LVZ+%a4?M+(YepUU)iO+w(B25_MZ=;?r-E)DePuvaa?W z-Uq|{lK?Lspb zOS!swY-HbPcZP8^msZzJ(W0D=_-Q0~*NUt3-u$$~_oK&_REW;xt_XRP6Xsr9PPAsE zM96ouPe9Dt*JSIg00yeGrGan-P)R(oVWT&h(OWACe*@%tOyGQ+T&9rDns_CgoUdR| zA6Z6`3)|k*cJDE5^OT%-xxIF@UwoS9Vl!Gpc(FyaP%H2@X>KlH)~kCJ7dSauZI^ZZ zpvnA3eL;?s#0pFlNh(ZK4nx9RoJ3_18qKfRfjCJ})hXy~-?>fynO3wJLRp|w2gye< ziDm9M)Cm zoRblYHg>FT-%pMcC*}Lg83N@7_0Bb!zGuY@&$|fC)cRzv_Iv+fEvRo@&w4xF=gx&o zv*#H%$0j%&otu%>hHGo=4Kmt|c=Y_~%due8t*bI&TCTCEEvQ~C-QcWuK3r4+XnLiw zG$H(qAHf&7t7P$x+zD|AgBnA{e^4h+2-Dk2=zsgkk4};&LZaP}9@I`mXZjZVKD}`NzHd zywNzCDAh8}$+Roc`rDz{VAvAX(gTE=ktWY$ow)wr?OLAyU5#~_&;*2)B%}QVi}D4> ze>STSIcs@YM^i3W=k&W>sEgG?eRzygD-k??@2<)J&PyG@;(0om73x7SvrO`YUYaV? z(9YGFRB?C((B`h`@bmS-uXXp#M4!NI=G6kv3+%gR-v+xV2NW3VQ!nFm>wzPf6=jSzT=^8?8h~ClYlqLAevfX-{5eH)s6%I*+V<*nQw48RyBt{nT>D11LTHJqXGmJs(Z#+&TDDL9m-Ao>gV zaxg!?^oamo~g& z|7Q`IRHkDOk%Y^Ir733EBE}NJZY<~lPX#dbAMx|+B-`}KzivJkq!-m%wzzbHV=BMxV}y3@s5=6?QXs>VciSo8X%&5iTp zM?YD~jlog zrCyp1>eKtNqS0$B#nQ|aE32Ngw5L2hT(S}W2J}Y8K`Di*%;eM&5cR6xD8FS%?~VNy z+Z@ubPX3@k1t&E8#fo--vSc>EKFSYknmVeXA;L1gUL3tlK*achXreTb3frL7>sG-p zwTtwyyB5%qsJjJFJNQ5BC%uhDx7O{PyTLvTgTAP8st1 zf4fT%(t8<};X6^@V9wH6xsIo+Qvc2Vaodis_Y4&bOEbS^csw{~@4Wz{!aS}vFyhvD z%`5Oa?`JW1zRKS}j;|Zv?HA(-yN<92T36Y%bLq^zwrUrQ`05O3&$f%nMi|G;j$qg9 z4VbX%4?ejlZV@>?qve~7pg1vBe;ijRFB2@b+8arF4``e}h)F5L-U9u^{tJm`1}n9S z14H_WvbR$3T1DmLiQc5jB0UJ#Y+ZtmE2syPZXqV*k6`w2!HMERplvh1QP<_VS#$Vp zavGzv2wx3fOgfE?jq_ECshHK$MF-+6PX@P_Oc2}Ye+AQjZ!R7rT}K2K?�ULH6h< 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�H}sr@FA#7#f_@i4kjPx~v-d2f60`qgN$>L) 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

xbJ?oGfm& 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 GJorCOcHmI} literal 0 HcmV?d00001 diff --git a/docs/developer/advanced/images/sharing-saved-objects-step-6.png b/docs/developer/advanced/images/sharing-saved-objects-step-6.png new file mode 100644 index 0000000000000000000000000000000000000000..b0fe40d926e27db7af7f53dc44a4a19f606f69b1 GIT binary patch literal 77857 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 M5S0}v7XtbGAMT&3761SM literal 0 HcmV?d00001 diff --git a/docs/developer/advanced/sharing-saved-objects.asciidoc b/docs/developer/advanced/sharing-saved-objects.asciidoc index 5dd93adf191236..59bab557240892 100644 --- a/docs/developer/advanced/sharing-saved-objects.asciidoc +++ b/docs/developer/advanced/sharing-saved-objects.asciidoc @@ -1,8 +1,8 @@ [[sharing-saved-objects]] -== Sharing Saved Objects +== Sharing saved objects -This guide describes the Sharing Saved Objects effort, and the breaking changes that plugin developers need to be aware of for the planned -8.0 release of {kib}. +This guide describes the "Sharing saved objects" effort, and the breaking changes that plugin developers need to be aware of for the planned +8.0 release of {kib}. It also describes how developers can take advantage of this feature. [[sharing-saved-objects-overview]] === Overview @@ -28,6 +28,12 @@ Ideally, most types of objects in {kib} will eventually be _shareable_; however, <> as a stepping stone for plugin developers to fully support this feature. +Implementing a shareable saved object type is done in two phases: + +- **Phase 1**: Convert an existing isolated object type into a share-capable one. Keep reading! +- **Phase 2**: Switch an existing share-capable object type into a shareable one, _or_ create a new shareable object type. Jump to the + <>! + [[sharing-saved-objects-breaking-changes]] === Breaking changes @@ -49,21 +55,21 @@ change the IDs of any existing objects that are not in the Default space. Changi TIP: External plugins can also convert their objects, but <>. -[[sharing-saved-objects-dev-flowchart]] -=== Developer Flowchart +[[sharing-saved-objects-phase-1]] +=== Phase 1 developer flowchart If you're still reading this page, you're probably developing a {kib} plugin that registers an object type, and you want to know what steps you need to take to prepare for the 8.0 release and mitigate any breaking changes! Depending on how you are using saved objects, you may need to take up to 5 steps, which are detailed in separate sections below. Refer to this flowchart: -image::images/sharing-saved-objects-dev-flowchart.png["Sharing Saved Objects developer flowchart"] +image::images/sharing-saved-objects-phase-1-dev-flowchart.png["Sharing Saved Objects phase 1 - developer flowchart"] TIP: There is a proof-of-concept (POC) pull request to demonstrate these changes. It first adds a simple test plugin that allows users to create and view notes. Then, it goes through the steps of the flowchart to convert the isolated "note" objects to become share-capable. As you read this guide, you can https://github.com/elastic/kibana/pull/107256[follow along in the POC] to see exactly how to take these steps. [[sharing-saved-objects-q1]] -=== Question 1 +==== Question 1 > *Do these objects contain links to other objects?* @@ -71,7 +77,7 @@ If your objects store _any_ links to other objects (with an object type/ID), you continue functioning after the 8.0 upgrade. [[sharing-saved-objects-step-1]] -=== Step 1 +==== Step 1 ⚠️ This step *must* be completed no later than the 7.16 release. ⚠️ @@ -117,7 +123,7 @@ migrations: { NOTE: Reminder, don't forget to add unit tests and integration tests! [[sharing-saved-objects-q2]] -=== Question 2 +==== Question 2 > *Are there any "deep links" to these objects?* @@ -130,7 +136,7 @@ Note that some URLs may contain <>! [[sharing-saved-objects-step-3]] -=== Step 3 +==== Step 3 ⚠️ This step will preferably be completed in the 7.16 release; it *must* be completed no later than the 8.0 release. ⚠️ @@ -206,7 +212,7 @@ TIP: See an example of this in https://github.com/elastic/kibana/pull/107256#use ] ``` -3. Update your Plugin class implementation to depend on the Core HTTP service and Spaces plugin API: +3. Update your Plugin class implementation to depend on the Spaces plugin API: + ```ts interface PluginStartDeps { @@ -218,11 +224,10 @@ export class MyPlugin implements Plugin<{}, {}, {}, PluginStartDeps> { core.application.register({ ... async mount(appMountParams: AppMountParameters) { - const [coreStart, pluginStartDeps] = await core.getStartServices(); - const { http } = coreStart; + const [, pluginStartDeps] = await core.getStartServices(); const { spaces: spacesApi } = pluginStartDeps; ... - // pass `http` and `spacesApi` to your app when you render it + // pass `spacesApi` to your app when you render it }, }); ... @@ -247,8 +252,8 @@ if (spacesApi && resolveResult.outcome === 'aliasMatch') { ``` <1> The `aliasPurpose` field is required as of 8.2, because the API response now includes the reason the alias was created to inform the client whether a toast should be shown or not. -<2> The `objectNoun` field is optional, it just changes "object" in the toast to whatever you specify -- you may want the toast to say - "dashboard" or "index pattern" instead! +<2> The `objectNoun` field is optional. It just changes "object" in the toast to whatever you specify -- you may want the toast to say + "dashboard" or "data view" instead. 5. And finally, in your deep link page, add a function that will create a callout in the case of a `'conflict'` outcome: + @@ -293,7 +298,7 @@ different outcomes.] NOTE: Reminder, don't forget to add unit tests and functional tests! [[sharing-saved-objects-step-4]] -=== Step 4 +==== Step 4 ⚠️ This step *must* be completed in the 8.0 release (no earlier and no later). ⚠️ @@ -315,7 +320,7 @@ TIP: See an example of this in https://github.com/elastic/kibana/pull/107256#use NOTE: Reminder, don't forget to add integration tests! [[sharing-saved-objects-q3]] -=== Question 3 +==== Question 3 > *Are these objects encrypted?* @@ -323,7 +328,7 @@ Saved objects can optionally be < object types are encrypted, so most plugin developers will not be affected. [[sharing-saved-objects-step-5]] -=== Step 5 +==== Step 5 ⚠️ This step *must* be completed in the 8.0 release (no earlier and no later). ⚠️ @@ -341,12 +346,141 @@ image::images/sharing-saved-objects-step-5.png["Sharing Saved Objects ESO migrat NOTE: Reminder, don't forget to add unit tests and integration tests! +[[sharing-saved-objects-phase-2]] +=== Phase 2 developer flowchart + +This section covers switching a share-capable object type into a shareable one _or_ creating a new shareable saved object type. Refer to +this flowchart: + +image::images/sharing-saved-objects-phase-2-dev-flowchart.png["Sharing Saved Objects phase 2 - developer flowchart"] + [[sharing-saved-objects-step-6]] -=== Step 6 +==== Step 6 + +> *Update your _server-side code_ to mark these objects as "shareable"* + +When you register your object, you need to set the proper `namespaceType`. If you have an existing object type that is "share-capable", you +can simply change it: + +image::images/sharing-saved-objects-step-6.png["Sharing Saved Objects registration (shareable)"] + +[[sharing-saved-objects-step-7]] +==== Step 7 + +> *Update saved object delete API usage to handle multiple spaces* + +If an object is shared to multiple spaces, it cannot be deleted without using the +https://github.com/elastic/kibana/blob/{branch}/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeleteoptions.md[`force` +delete option]. You should always be aware when a saved object exists in multiple spaces, and you should warn users in that case. + +If your UI allows users to delete your objects, you can define a warning message like this: + +```tsx +const { namespaces, id } = savedObject; +const warningMessage = + namespaces.length > 1 || namespaces.includes('*') ? ( + + ) : null; +``` + +The <> in <> uses a +https://github.com/elastic/kibana/blob/{branch}/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx[similar +approach] to show a warning in its delete confirmation modal: + +image::images/sharing-saved-objects-step-7.png["Sharing Saved Objects deletion warning"] + +[[sharing-saved-objects-step-8]] +==== Step 8 + +> *Allow users to view and change assigned spaces for your objects* + +Users will need a way to view what spaces your objects are currently assigned to and share them to additional spaces. You can accomplish +this in two ways, and many consumers will want to implement both: + +1. (Highly recommended) Add reusable components to your application, making it "space-aware". The space-related components are exported by + the spaces plugin, and you can use them in your own application. ++ +First, make sure your page contents are wrapped in a +https://github.com/elastic/kibana/blob/{branch}/x-pack/plugins/spaces/public/spaces_context/types.ts[spaces context provider]: ++ +```tsx +const ContextWrapper = useMemo( + () => + spacesApi ? spacesApi.ui.components.getSpacesContextProvider : getEmptyFunctionComponent, + [spacesApi] +); -> *Update your code to make your objects shareable* +... -_This is not required for the 8.0 release; this additional information will be added in the near future!_ +return ( + + + +); +``` ++ +Second, display a https://github.com/elastic/kibana/blob/{branch}/x-pack/plugins/spaces/public/space_list/types.ts[list of spaces] for an +object, and third, show a +https://github.com/elastic/kibana/blob/{branch}/x-pack/plugins/spaces/public/share_saved_objects_to_space/types.ts[flyout] for the user to +edit the object's assigned spaces. You may want to follow the example of the <> and +https://github.com/elastic/kibana/blob/{branch}/src/plugins/data_view_management/public/components/index_pattern_table/spaces_list.tsx[combine +these into a single component] so that the space list can be clicked to show the flyout: ++ +```tsx +const [showFlyout, setShowFlyout] = useState(false); +const LazySpaceList = useCallback(spacesApi.ui.components.getSpaceList, [spacesApi]); +const LazyShareToSpaceFlyout = useCallback(spacesApi.ui.components.getShareToSpaceFlyout, [spacesApi]); + +const shareToSpaceFlyoutProps: ShareToSpaceFlyoutProps = { + savedObjectTarget: { + type: myObject.type, + namespaces: myObject.namespaces, + id: myObject.id, + icon: 'beaker', <1> + title: myObject.attributes.title, <2> + noun: OBJECT_NOUN, <3> + }, + onUpdate: () => { /* callback when the object is updated */ }, + onClose: () => setShowFlyout(false), +}; + +return ( + <> + + listOnClick={() => setShowFlyout(true)} + /> + {showFlyout && } + +); +``` +<1> The `icon` field is optional. It specifies an https://elastic.github.io/eui/#/display/icons[EUI icon] type that will be displayed in the + flyout header. +<2> The `title` field is optional. It specifies a human-readable identifier for your object that will be displayed in the flyout header. +<3> The `noun` field is optional. It just changes "object" in the flyout to whatever you specify -- you may want the flyout to say + "dashboard" or "data view" instead. +<4> The `behaviorContext` field is optional. It controls how the space list is displayed. When using an `"outside-space"` behavior context, + the space list is rendered outside of any particular space, so the active space is included in the list. On the other hand, when using a + `"within-space"` behavior context, the space list is rendered within the active space, so the active space is excluded from the list. + +2. Allow users to access your objects in the <> in <>. You can do this by + ensuring that your objects are marked as + https://github.com/elastic/kibana/blob/{branch}/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md[importable and exportable] in your <>: ++ +```ts +name: 'my-object-type', +management: { + isImportableAndExportable: true, +}, +... +``` +If you do this, then your objects will be visible in the <>, where users can assign +them to multiple spaces. [[sharing-saved-objects-faq]] === Frequently asked questions (FAQ) diff --git a/docs/developer/architecture/core/saved-objects-service.asciidoc b/docs/developer/architecture/core/saved-objects-service.asciidoc index 54a5c319c62220..cc669be8ec9fa2 100644 --- a/docs/developer/architecture/core/saved-objects-service.asciidoc +++ b/docs/developer/architecture/core/saved-objects-service.asciidoc @@ -32,6 +32,7 @@ wanting to use Saved Objects. === Server side usage +[[saved-objects-type-registration]] ==== Registering a Saved Object type Saved object type definitions should be defined in their own `my_plugin/server/saved_objects` directory. From 130823ac27126f1eca689a186a088f1993fa848a Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 22 Mar 2022 15:58:57 -0700 Subject: [PATCH 27/64] [DOCS] Add find case APIs (#127686) --- docs/api/cases.asciidoc | 9 +- docs/api/cases/cases-api-find-cases.asciidoc | 193 ++++++++++++++++++ .../cases/cases-api-find-connectors.asciidoc | 60 ++++++ 3 files changed, 260 insertions(+), 2 deletions(-) create mode 100644 docs/api/cases/cases-api-find-cases.asciidoc create mode 100644 docs/api/cases/cases-api-find-connectors.asciidoc diff --git a/docs/api/cases.asciidoc b/docs/api/cases.asciidoc index 00fbedc2d12994..45186a4e7d489b 100644 --- a/docs/api/cases.asciidoc +++ b/docs/api/cases.asciidoc @@ -10,9 +10,9 @@ these APIs: * {security-guide}/cases-api-delete-all-comments.html[Delete all comments] * {security-guide}/cases-api-delete-comment.html[Delete comment] * {security-guide}/cases-api-find-alert.html[Find all alerts attached to a case] -* {security-guide}/cases-api-find-cases.html[Find cases] +* <> * {security-guide}/cases-api-find-cases-by-alert.html[Find cases by alert] -* {security-guide}/cases-api-find-connectors.html[Find connectors] +* <> * {security-guide}/cases-api-get-case-activity.html[Get all case activity] * {security-guide}/cases-api-get-all-case-comments.html[Get all case comments] * {security-guide}/cases-api-get-case.html[Get case] @@ -27,5 +27,10 @@ these APIs: * <> * {security-guide}/cases-api-update-comment.html[Update comment] +//CREATE include::cases/cases-api-create.asciidoc[leveloffset=+1] +//FIND +include::cases/cases-api-find-cases.asciidoc[leveloffset=+1] +include::cases/cases-api-find-connectors.asciidoc[leveloffset=+1] +//UPDATE include::cases/cases-api-update.asciidoc[leveloffset=+1] \ No newline at end of file diff --git a/docs/api/cases/cases-api-find-cases.asciidoc b/docs/api/cases/cases-api-find-cases.asciidoc new file mode 100644 index 00000000000000..334f45fee526df --- /dev/null +++ b/docs/api/cases/cases-api-find-cases.asciidoc @@ -0,0 +1,193 @@ +[[cases-api-find-cases]] +== Find cases API +++++ +Find cases +++++ + +Retrieves a paginated subset of cases. + +=== Request + +`GET :/api/cases/_find` + +`GET :/s//api/cases/_find` + +=== Prerequisite + +You must have `read` privileges for the *Cases* feature in the *Management*, +*{observability}*, or *Security* section of the +<>, depending on the +`owner` of the cases you're seeking. + +=== Path parameters + +``:: +(Optional, string) An identifier for the space. If it is not specified, the +default space is used. + +=== Query parameters + +`defaultSearchOperator`:: +(Optional, string) The default operator to use for the `simple_query_string`. +Defaults to `OR`. + +//// +`fields`:: +(Optional, array of strings) The fields in the entity to return in the response. +//// +`owner`:: +(Optional, string or array of strings) A filter to limit the retrieved cases to +a specific set of applications. Valid values are: `cases`, `observability`, +and `securitySolution`. If this parameter is omitted, the response contains all +cases that the user has access to read. + +`page`:: +(Optional, integer) The page number to return. Defaults to `1`. + +`perPage`:: +(Optional, integer) The number of rules to return per page. Defaults to `20`. + +`reporters`:: +(Optional, string or array of strings) Filters the returned cases by the +reporter's `username`. + +`search`:: +(Optional, string) An {es} +{ref}/query-dsl-simple-query-string-query.html[simple_query_string] query that +filters the objects in the response. + +`searchFields`:: +(Optional, string or array of strings) The fields to perform the +`simple_query_string` parsed query against. + +`sortField`:: +(Optional, string) Determines which field is used to sort the results, +`createdAt` or `updatedAt`. Defaults to `createdAt`. ++ +NOTE: Even though the JSON case object uses `created_at` and `updated_at` +fields, you must use `createdAt` and `updatedAt` fields in the URL +query. + +`sortOrder`:: +(Optional, string) Determines the sort order, which can be `desc` or `asc`. +Defaults to `desc`. + +`status`:: +(Optional, string) Filters the returned cases by state, which can be `open`, +`in-progress`, or `closed`. + +`tags`:: +(Optional, string or array of strings) Filters the returned cases by tags. + +=== Response code + +`200`:: + Indicates a successful call. + +=== Example + +Retrieve the first five cases with the `phishing` tag, in ascending order by +last update time: + +[source,sh] +-------------------------------------------------- +GET api/cases/_find?page=1&perPage=5&sortField=updatedAt&sortOrder=asc&tags=phishing +-------------------------------------------------- +// KIBANA + +The API returns a JSON object listing the retrieved cases. For example: + +[source,json] +-------------------------------------------------- +{ + "page": 1, + "per_page": 5, + "total": 2, + "cases": [ + { + "id": "abed3a70-71bd-11ea-a0b2-c51ea50a58e2", + "version": "WzExMCwxXQ==", + "comments": [], + "totalComment": 0, + "totalAlerts": 0, + "title": "The Long Game", + "tags": [ + "windows", + "phishing" + ], + "description": "Windows 95", + "settings": { + "syncAlerts": true + }, + "owner": "securitySolution", + "closed_at": null, + "closed_by": null, + "created_at": "2022-03-29T13:03:23.533Z", + "created_by": { + "email": "rhustler@email.com", + "full_name": "Rat Hustler", + "username": "rhustler" + }, + "status": "open", + "updated_at": null, + "updated_by": null, + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "My connector", + "type": ".jira", + "fields": { + "issueType": "10006", + "priority": null, + } + } + "external_service": null, + }, + { + "id": "a18b38a0-71b0-11ea-a0b2-c51ea50a58e2", + "version": "Wzk4LDFd", + "comments": [], + "totalComment": 0, + "totalAlerts": 0, + "title": "This case will self-destruct in 5 seconds", + "tags": [ + "phishing", + "social engineering", + "bubblegum" + ], + "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active!", + "settings": { + "syncAlerts": false + }, + "owner": "cases", + "closed_at": null, + "closed_by": null, + "created_at": "2022-03-29T11:30:02.658Z", + "created_by": { + "email": "ahunley@imf.usa.gov", + "full_name": "Alan Hunley", + "username": "ahunley" + }, + "status": "open", + "updated_at": "2022-03-29T12:01:50.244Z", + "updated_by": { + "full_name": "Classified", + "email": "classified@hms.oo.gov.uk", + "username": "M" + }, + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "My connector", + "type": ".resilient", + "fields": { + "issueTypes": [13], + "severityCode": 6, + } + }, + "external_service": null, + } + ], + "count_open_cases": 2, + "count_in_progress_cases":0, + "count_closed_cases": 0 +} +-------------------------------------------------- diff --git a/docs/api/cases/cases-api-find-connectors.asciidoc b/docs/api/cases/cases-api-find-connectors.asciidoc new file mode 100644 index 00000000000000..8643d569c980ba --- /dev/null +++ b/docs/api/cases/cases-api-find-connectors.asciidoc @@ -0,0 +1,60 @@ +[[cases-api-find-connectors]] +== Find connectors API +++++ +Find connectors +++++ + +Retrieves information about <>. + +In particular, only the connectors that are supported for use in cases are +returned. Refer to the list of supported external incident management systems in +<>. + +=== Request + +`GET :/api/cases/configure/connectors/_find` + +`GET :/s//api/cases/configure/connectors/_find` + +=== Prerequisite + +You must have `read` privileges for the *Actions and Connectors* feature in the +*Management* section of the +<>. + +=== Path parameters + +``:: +(Optional, string) An identifier for the space. If it is not specified, the +default space is used. + +=== Response code + +`200`:: + Indicates a successful call. + +=== Example + +[source,sh] +-------------------------------------------------- +GET api/cases/configure/connectors/_find +-------------------------------------------------- +// KIBANA + +The API returns a JSON object describing the connectors and their settings: + +[source,json] +-------------------------------------------------- +[{ + "id":"61787f53-4eee-4741-8df6-8fe84fa616f7", + "actionTypeId": ".jira", + "name":"my-Jira", + "isMissingSecrets":false, + "config": { + "apiUrl":"https://elastic.atlassian.net/", + "projectKey":"ES" + }, + "isPreconfigured":false, + "referencedByCount":0 +}] +-------------------------------------------------- \ No newline at end of file From 156ce283a87a652001d800abf070a996eb6e8543 Mon Sep 17 00:00:00 2001 From: Bhavya RM Date: Tue, 22 Mar 2022 20:33:10 -0400 Subject: [PATCH 28/64] Unskip spaces a11y tests with a retry (#128204) --- x-pack/test/accessibility/apps/spaces.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/x-pack/test/accessibility/apps/spaces.ts b/x-pack/test/accessibility/apps/spaces.ts index 78e5dd1f2f2c3d..567f958f5f8a49 100644 --- a/x-pack/test/accessibility/apps/spaces.ts +++ b/x-pack/test/accessibility/apps/spaces.ts @@ -18,8 +18,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const toasts = getService('toasts'); - // FLAKY: https://github.com/elastic/kibana/issues/100968 - describe.skip('Kibana spaces page meets a11y validations', () => { + describe('Kibana spaces page meets a11y validations', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); await PageObjects.common.navigateToApp('home'); @@ -98,7 +97,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // test starts with deleting space b so we can get the space selection page instead of logging out in the test it('a11y test for space selection page', async () => { await PageObjects.spaceSelector.confirmDeletingSpace(); - await a11y.testAppSnapshot(); + await retry.try(async () => { + await a11y.testAppSnapshot(); + }); await PageObjects.spaceSelector.clickSpaceCard('default'); }); }); From 829517bb64c6e499ddfd7c1f0c3d5d4f0bcbeb19 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 23 Mar 2022 07:16:58 +0000 Subject: [PATCH 29/64] [ML] Fixing get_filter endpoint (#128238) * [ML] Fixing get_filter endpoint * unskipping tests --- .../ml/server/models/filter/filter_manager.ts | 46 ++++++------------- .../apis/ml/filters/create_filters.ts | 3 +- .../apis/ml/filters/get_filters.ts | 3 +- .../apis/ml/filters/update_filters.ts | 3 +- 4 files changed, 18 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/ml/server/models/filter/filter_manager.ts b/x-pack/plugins/ml/server/models/filter/filter_manager.ts index baad35de6e5905..a4e902ff449942 100644 --- a/x-pack/plugins/ml/server/models/filter/filter_manager.ts +++ b/x-pack/plugins/ml/server/models/filter/filter_manager.ts @@ -41,44 +41,28 @@ interface FiltersInUse { [id: string]: FilterUsage; } -// interface PartialDetector { -// detector_description: string; -// custom_rules: DetectorRule[]; -// } - -// interface PartialJob { -// job_id: string; -// analysis_config: { -// detectors: PartialDetector[]; -// }; -// } - export class FilterManager { constructor(private _mlClient: MlClient) {} async getFilter(filterId: string) { try { - const [JOBS, FILTERS] = [0, 1]; - const results = await Promise.all([ - this._mlClient.getJobs(), - this._mlClient.getFilters({ filter_id: filterId }), - ]); - - if (results[FILTERS] && (results[FILTERS] as estypes.MlGetFiltersResponse).filters.length) { - let filtersInUse: FiltersInUse = {}; - if (results[JOBS] && (results[JOBS] as estypes.MlGetJobsResponse).jobs) { - filtersInUse = this.buildFiltersInUse((results[JOBS] as estypes.MlGetJobsResponse).jobs); - } - - const filter = (results[FILTERS] as estypes.MlGetFiltersResponse).filters[0]; - return { - ...filter, - used_by: filtersInUse[filter.filter_id], - item_count: 0, - } as FilterStats; - } else { + const { + filters: [filter], + } = await this._mlClient.getFilters({ filter_id: filterId }); + if (filter === undefined) { + // could be an empty list rather than a 404 if a wildcard was used, + // so throw our own 404 throw Boom.notFound(`Filter with the id "${filterId}" not found`); } + + const { jobs } = await this._mlClient.getJobs(); + const filtersInUse = this.buildFiltersInUse(jobs); + + return { + ...filter, + used_by: filtersInUse[filter.filter_id], + item_count: 0, + } as FilterStats; } catch (error) { throw Boom.badRequest(error); } diff --git a/x-pack/test/api_integration/apis/ml/filters/create_filters.ts b/x-pack/test/api_integration/apis/ml/filters/create_filters.ts index 86b607337dc4a1..6eec47456fb512 100644 --- a/x-pack/test/api_integration/apis/ml/filters/create_filters.ts +++ b/x-pack/test/api_integration/apis/ml/filters/create_filters.ts @@ -92,8 +92,7 @@ export default ({ getService }: FtrProviderContext) => { }, ]; - // Failing: See https://github.com/elastic/kibana/issues/126642 - describe.skip('create_filters', function () { + describe('create_filters', function () { before(async () => { await ml.testResources.setKibanaTimeZoneToUTC(); }); diff --git a/x-pack/test/api_integration/apis/ml/filters/get_filters.ts b/x-pack/test/api_integration/apis/ml/filters/get_filters.ts index b111a97fdbba9c..8d99650f6d509a 100644 --- a/x-pack/test/api_integration/apis/ml/filters/get_filters.ts +++ b/x-pack/test/api_integration/apis/ml/filters/get_filters.ts @@ -26,8 +26,7 @@ export default ({ getService }: FtrProviderContext) => { }, ]; - // FLAKY: https://github.com/elastic/kibana/issues/126870 - describe.skip('get_filters', function () { + describe('get_filters', function () { before(async () => { await ml.testResources.setKibanaTimeZoneToUTC(); for (const filter of validFilters) { diff --git a/x-pack/test/api_integration/apis/ml/filters/update_filters.ts b/x-pack/test/api_integration/apis/ml/filters/update_filters.ts index f943378201dfd2..737e2c21cf0f66 100644 --- a/x-pack/test/api_integration/apis/ml/filters/update_filters.ts +++ b/x-pack/test/api_integration/apis/ml/filters/update_filters.ts @@ -31,8 +31,7 @@ export default ({ getService }: FtrProviderContext) => { }, ]; - // FLAKY: https://github.com/elastic/kibana/issues/127678 - describe.skip('update_filters', function () { + describe('update_filters', function () { const updateFilterRequestBody = { description: 'Updated filter #1', removeItems: items, From fb71a2d66e7b1509e547579640f74dab27e5d149 Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Wed, 23 Mar 2022 08:19:14 +0100 Subject: [PATCH 30/64] [Osquery] Add live query to alerts (#128142) [Osquery] Add osquery to alerts and timeline --- .../node_details/tabs/osquery/index.tsx | 2 +- .../fixtures/saved_objects/pack.ndjson | 28 ++++ .../fixtures/saved_objects/rule.ndjson | 99 ++++++++++++++ .../integration/superuser/alerts.spec.ts | 65 +++++++++ .../integration/superuser/metrics.spec.ts | 1 - .../integration/superuser/packs.spec.ts | 14 +- .../superuser/saved_queries.spec.ts | 35 ++--- .../osquery/cypress/screens/live_query.ts | 1 + .../osquery/cypress/tasks/integrations.ts | 2 +- .../osquery/cypress/tasks/live_query.ts | 5 +- x-pack/plugins/osquery/cypress/tasks/packs.ts | 2 +- x-pack/plugins/osquery/public/packs/types.ts | 2 +- x-pack/plugins/osquery/public/plugin.ts | 3 +- .../public/shared_components/index.tsx | 1 + .../osquery_action/index.tsx | 128 +++++++++--------- .../use_is_osquery_available.ts | 46 +++++++ .../use_is_osquery_available_simple.test.ts | 55 ++++++++ .../use_is_osquery_available_simple.tsx | 43 ++++++ x-pack/plugins/osquery/public/types.ts | 1 + x-pack/plugins/security_solution/kibana.json | 3 +- .../osquery/osquery_action_item.tsx | 26 ++++ .../components/osquery/osquery_flyout.tsx | 58 ++++++++ .../osquery/osquery_flyout_footer.tsx | 28 ++++ .../osquery/osquery_flyout_header.tsx | 30 ++++ .../components/osquery/translations.ts | 22 +++ .../take_action_dropdown/index.test.tsx | 13 +- .../components/take_action_dropdown/index.tsx | 37 ++++- .../side_panel/event_details/footer.tsx | 15 +- .../plugins/security_solution/public/types.ts | 2 + .../plugins/security_solution/tsconfig.json | 1 + .../test/osquery_cypress/artifact_manager.ts | 9 +- 31 files changed, 671 insertions(+), 106 deletions(-) create mode 100644 x-pack/plugins/osquery/cypress/fixtures/saved_objects/pack.ndjson create mode 100644 x-pack/plugins/osquery/cypress/fixtures/saved_objects/rule.ndjson create mode 100644 x-pack/plugins/osquery/cypress/integration/superuser/alerts.spec.ts create mode 100644 x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available.ts create mode 100644 x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available_simple.test.ts create mode 100644 x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available_simple.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/osquery/osquery_action_item.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout_footer.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout_header.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/osquery/translations.ts diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/osquery/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/osquery/index.tsx index 9932903ef9f198..1bd6cfd353140e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/osquery/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/osquery/index.tsx @@ -48,7 +48,7 @@ const TabComponent = (props: TabProps) => { return ( - + ); }, [OsqueryAction, loading, metadata]); diff --git a/x-pack/plugins/osquery/cypress/fixtures/saved_objects/pack.ndjson b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/pack.ndjson new file mode 100644 index 00000000000000..d36a9ccb8cabdc --- /dev/null +++ b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/pack.ndjson @@ -0,0 +1,28 @@ +{ + "attributes": { + "created_at": "2022-01-28T09:01:46.147Z", + "created_by": "elastic", + "description": "gfd", + "enabled": true, + "name": "testpack", + "queries": [ + { + "id": "fds", + "interval": 10, + "query": "select * from uptime;" + } + ], + "updated_at": "2022-01-28T09:01:46.147Z", + "updated_by": "elastic" + }, + "coreMigrationVersion": "8.1.0", + "id": "eb92a730-8018-11ec-88ce-bd5b5e3a7526", + "references": [], + "sort": [ + 1643360506152, + 9062 + ], + "type": "osquery-pack", + "updated_at": "2022-01-28T09:01:46.152Z", + "version": "WzgzOTksMV0=" +} \ No newline at end of file diff --git a/x-pack/plugins/osquery/cypress/fixtures/saved_objects/rule.ndjson b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/rule.ndjson new file mode 100644 index 00000000000000..75bdecb5be4287 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/rule.ndjson @@ -0,0 +1,99 @@ +{ + "id": "c8ca6100-802e-11ec-952d-cf6018da8e2b", + "type": "alert", + "namespaces": [ + "default" + ], + "updated_at": "2022-01-28T11:38:23.009Z", + "version": "WzE5MjksMV0=", + "attributes": { + "name": "Test-rule", + "tags": [ + "__internal_rule_id:22308402-5e0e-421b-8d22-a47ddc4b0188", + "__internal_immutable:false" + ], + "alertTypeId": "siem.queryRule", + "consumer": "siem", + "params": { + "author": [], + "description": "asd", + "ruleId": "22308402-5e0e-421b-8d22-a47ddc4b0188", + "falsePositives": [], + "from": "now-360s", + "immutable": false, + "license": "", + "outputIndex": ".siem-signals-default", + "meta": { + "from": "1m", + "kibana_siem_app_url": "http://localhost:5601/app/security" + }, + "maxSignals": 100, + "riskScore": 21, + "riskScoreMapping": [], + "severity": "low", + "severityMapping": [], + "threat": [], + "to": "now", + "references": [], + "version": 1, + "exceptionsList": [], + "type": "query", + "language": "kuery", + "index": [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "query": "_id:*", + "filters": [] + }, + "schedule": { + "interval": "5m" + }, + "enabled": true, + "actions": [], + "throttle": null, + "notifyWhen": "onActiveAlert", + "apiKeyOwner": "elastic", + "legacyId": null, + "createdBy": "elastic", + "updatedBy": "elastic", + "createdAt": "2022-01-28T11:38:17.540Z", + "updatedAt": "2022-01-28T11:38:19.894Z", + "muteAll": true, + "mutedInstanceIds": [], + "executionStatus": { + "status": "ok", + "lastExecutionDate": "2022-01-28T11:38:21.638Z", + "error": null, + "lastDuration": 1369 + }, + "monitoring": { + "execution": { + "history": [ + { + "success": true, + "timestamp": 1643369903007 + } + ], + "calculated_metrics": { + "success_ratio": 1 + } + } + }, + "meta": { + "versionApiKeyLastmodified": "8.1.0" + }, + "scheduledTaskId": "c8ca6100-802e-11ec-952d-cf6018da8e2b" + }, + "references": [], + "migrationVersion": { + "alert": "8.0.0" + }, + "coreMigrationVersion": "8.1.0" +} \ No newline at end of file diff --git a/x-pack/plugins/osquery/cypress/integration/superuser/alerts.spec.ts b/x-pack/plugins/osquery/cypress/integration/superuser/alerts.spec.ts new file mode 100644 index 00000000000000..153fd5d58791e5 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/integration/superuser/alerts.spec.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ArchiverMethod, runKbnArchiverScript } from '../../tasks/archiver'; +import { login } from '../../tasks/login'; +import { + checkResults, + findAndClickButton, + findFormFieldByRowsLabelAndType, + inputQuery, + submitQuery, +} from '../../tasks/live_query'; +import { preparePack } from '../../tasks/packs'; +import { closeModalIfVisible } from '../../tasks/integrations'; +import { navigateTo } from '../../tasks/navigation'; + +describe('Alert Event Details', () => { + before(() => { + runKbnArchiverScript(ArchiverMethod.LOAD, 'pack'); + runKbnArchiverScript(ArchiverMethod.LOAD, 'rule'); + }); + beforeEach(() => { + login(); + }); + + after(() => { + runKbnArchiverScript(ArchiverMethod.UNLOAD, 'pack'); + runKbnArchiverScript(ArchiverMethod.UNLOAD, 'rule'); + }); + + it('should be able to run live query', () => { + const PACK_NAME = 'testpack'; + const RULE_NAME = 'Test-rule'; + navigateTo('/app/osquery/packs'); + preparePack(PACK_NAME); + findAndClickButton('Edit'); + cy.contains(`Edit ${PACK_NAME}`); + findFormFieldByRowsLabelAndType( + 'Scheduled agent policies (optional)', + 'fleet server {downArrow}{enter}' + ); + findAndClickButton('Update pack'); + closeModalIfVisible(); + cy.contains(PACK_NAME); + cy.visit('/app/security/rules'); + cy.contains(RULE_NAME).click(); + cy.wait(2000); + cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'true'); + cy.getBySel('ruleSwitch').click(); + cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'false'); + cy.getBySel('ruleSwitch').click(); + cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'true'); + cy.visit('/app/security/alerts'); + cy.getBySel('expand-event').first().click(); + cy.getBySel('take-action-dropdown-btn').click(); + cy.getBySel('osquery-action-item').click(); + inputQuery('select * from uptime;'); + submitQuery(); + checkResults(); + }); +}); diff --git a/x-pack/plugins/osquery/cypress/integration/superuser/metrics.spec.ts b/x-pack/plugins/osquery/cypress/integration/superuser/metrics.spec.ts index a9524a509c0a19..f64e6b31ae7a56 100644 --- a/x-pack/plugins/osquery/cypress/integration/superuser/metrics.spec.ts +++ b/x-pack/plugins/osquery/cypress/integration/superuser/metrics.spec.ts @@ -45,7 +45,6 @@ describe('Super User - Metrics', () => { cy.getBySel('comboBoxInput').first().click(); cy.wait(500); - cy.get('div[role=listBox]').should('have.lengthOf.above', 0); cy.getBySel('comboBoxInput').first().type('{downArrow}{enter}'); submitQuery(); diff --git a/x-pack/plugins/osquery/cypress/integration/superuser/packs.spec.ts b/x-pack/plugins/osquery/cypress/integration/superuser/packs.spec.ts index 4c72a871b5b58c..fd04d0a62b1602 100644 --- a/x-pack/plugins/osquery/cypress/integration/superuser/packs.spec.ts +++ b/x-pack/plugins/osquery/cypress/integration/superuser/packs.spec.ts @@ -71,7 +71,7 @@ describe('SuperUser - Packs', () => { }); it('to click the edit button and edit pack', () => { - preparePack(PACK_NAME, SAVED_QUERY_ID); + preparePack(PACK_NAME); findAndClickButton('Edit'); cy.contains(`Edit ${PACK_NAME}`); findAndClickButton('Add query'); @@ -89,7 +89,7 @@ describe('SuperUser - Packs', () => { }); it('should trigger validation when saved query is being chosen', () => { - preparePack(PACK_NAME, SAVED_QUERY_ID); + preparePack(PACK_NAME); findAndClickButton('Edit'); findAndClickButton('Add query'); cy.contains('Attach next query'); @@ -103,7 +103,7 @@ describe('SuperUser - Packs', () => { }); // THIS TESTS TAKES TOO LONG FOR NOW - LET ME THINK IT THROUGH it.skip('to click the icon and visit discover', () => { - preparePack(PACK_NAME, SAVED_QUERY_ID); + preparePack(PACK_NAME); cy.react('CustomItemAction', { props: { index: 0, item: { id: SAVED_QUERY_ID } }, }).click(); @@ -124,7 +124,7 @@ describe('SuperUser - Packs', () => { lensUrl = url; }); }); - preparePack(PACK_NAME, SAVED_QUERY_ID); + preparePack(PACK_NAME); cy.react('CustomItemAction', { props: { index: 1, item: { id: SAVED_QUERY_ID } }, }).click(); @@ -154,7 +154,7 @@ describe('SuperUser - Packs', () => { }); it('delete all queries in the pack', () => { - preparePack(PACK_NAME, SAVED_QUERY_ID); + preparePack(PACK_NAME); cy.contains(/^Edit$/).click(); cy.getBySel('checkboxSelectAll').click(); @@ -170,7 +170,7 @@ describe('SuperUser - Packs', () => { }); it('enable changing saved queries and ecs_mappings', () => { - preparePack(PACK_NAME, SAVED_QUERY_ID); + preparePack(PACK_NAME); cy.contains(/^Edit$/).click(); findAndClickButton('Add query'); @@ -210,7 +210,7 @@ describe('SuperUser - Packs', () => { }); it('to click delete button', () => { - preparePack(PACK_NAME, SAVED_QUERY_ID); + preparePack(PACK_NAME); findAndClickButton('Edit'); deleteAndConfirm('pack'); }); diff --git a/x-pack/plugins/osquery/cypress/integration/superuser/saved_queries.spec.ts b/x-pack/plugins/osquery/cypress/integration/superuser/saved_queries.spec.ts index bfeb5adc11f6e0..bc8417d5facf55 100644 --- a/x-pack/plugins/osquery/cypress/integration/superuser/saved_queries.spec.ts +++ b/x-pack/plugins/osquery/cypress/integration/superuser/saved_queries.spec.ts @@ -6,9 +6,10 @@ */ import { navigateTo } from '../../tasks/navigation'; +import { RESULTS_TABLE_BUTTON } from '../../screens/live_query'; import { checkResults, - DEFAULT_QUERY, + BIG_QUERY, deleteAndConfirm, findFormFieldByRowsLabelAndType, inputQuery, @@ -34,18 +35,18 @@ describe('Super User - Saved queries', () => { () => { cy.contains('New live query').click(); selectAllAgents(); - inputQuery(DEFAULT_QUERY); + inputQuery(BIG_QUERY); submitQuery(); checkResults(); // enter fullscreen - cy.getBySel('dataGridFullScreenButton').trigger('mouseover'); - cy.contains(/Full screen$/).should('exist'); - cy.contains('Exit full screen').should('not.exist'); - cy.getBySel('dataGridFullScreenButton').click(); + cy.getBySel(RESULTS_TABLE_BUTTON).trigger('mouseover'); + cy.contains(/Enter fullscreen$/).should('exist'); + cy.contains('Exit fullscreen').should('not.exist'); + cy.getBySel(RESULTS_TABLE_BUTTON).click(); - cy.getBySel('dataGridFullScreenButton').trigger('mouseover'); - cy.contains(/Full screen$/).should('not.exist'); - cy.contains('Exit full screen').should('exist'); + cy.getBySel(RESULTS_TABLE_BUTTON).trigger('mouseover'); + cy.contains(/Enter Fullscreen$/).should('not.exist'); + cy.contains('Exit fullscreen').should('exist'); // hidden columns cy.react('EuiDataGridHeaderCellWrapper', { props: { id: 'osquery.cmdline' } }).click(); @@ -59,10 +60,10 @@ describe('Super User - Saved queries', () => { cy.getBySel('pagination-button-next').click().wait(500).click(); cy.contains('2 columns hidden').should('exist'); - cy.getBySel('dataGridFullScreenButton').trigger('mouseover'); - cy.contains(/Full screen$/).should('not.exist'); - cy.contains('Exit full screen').should('exist'); - cy.getBySel('dataGridFullScreenButton').click(); + cy.getBySel(RESULTS_TABLE_BUTTON).trigger('mouseover'); + cy.contains(/Enter fullscreen$/).should('not.exist'); + cy.contains('Exit fullscreen').should('exist'); + cy.getBySel(RESULTS_TABLE_BUTTON).click(); // sorting cy.react('EuiDataGridHeaderCellWrapper', { @@ -70,8 +71,8 @@ describe('Super User - Saved queries', () => { }).click(); cy.contains(/Sort A-Z$/).click(); cy.contains('2 columns hidden').should('exist'); - cy.getBySel('dataGridFullScreenButton').trigger('mouseover'); - cy.contains(/Full screen$/).should('exist'); + cy.getBySel(RESULTS_TABLE_BUTTON).trigger('mouseover'); + cy.contains(/Enter fullscreen$/).should('exist'); // save new query cy.contains('Exit full screen').should('not.exist'); @@ -111,8 +112,8 @@ describe('Super User - Saved queries', () => { props: { index: 1, item: { attributes: { id: SAVED_QUERY_ID } } }, }).click(); deleteAndConfirm('query'); - cy.contains(SAVED_QUERY_ID); - cy.contains(/^No items found/); + cy.contains(SAVED_QUERY_ID).should('exist'); + cy.contains(SAVED_QUERY_ID).should('not.exist'); } ); }); diff --git a/x-pack/plugins/osquery/cypress/screens/live_query.ts b/x-pack/plugins/osquery/cypress/screens/live_query.ts index 1a521fe1cd6512..cba4a35c057195 100644 --- a/x-pack/plugins/osquery/cypress/screens/live_query.ts +++ b/x-pack/plugins/osquery/cypress/screens/live_query.ts @@ -9,3 +9,4 @@ export const AGENT_FIELD = '[data-test-subj="comboBoxInput"]'; export const ALL_AGENTS_OPTION = '[title="All agents"]'; export const LIVE_QUERY_EDITOR = '#osquery_editor'; export const SUBMIT_BUTTON = '#submit-button'; +export const RESULTS_TABLE_BUTTON = 'dataGridFullScreenButton'; diff --git a/x-pack/plugins/osquery/cypress/tasks/integrations.ts b/x-pack/plugins/osquery/cypress/tasks/integrations.ts index ebf8668483d1c0..9cd9cbd8d4db62 100644 --- a/x-pack/plugins/osquery/cypress/tasks/integrations.ts +++ b/x-pack/plugins/osquery/cypress/tasks/integrations.ts @@ -16,7 +16,7 @@ import { export const addIntegration = (agentPolicy = 'Default Fleet Server policy') => { cy.getBySel(ADD_POLICY_BTN).click(); cy.getBySel(DATA_COLLECTION_SETUP_STEP).find('.euiLoadingSpinner').should('not.exist'); - cy.getBySel('agentPolicySelect').select(agentPolicy); + cy.getBySel('agentPolicySelect').should('have.text', agentPolicy); cy.getBySel(CREATE_PACKAGE_POLICY_SAVE_BTN).click(); // sometimes agent is assigned to default policy, sometimes not closeModalIfVisible(); diff --git a/x-pack/plugins/osquery/cypress/tasks/live_query.ts b/x-pack/plugins/osquery/cypress/tasks/live_query.ts index 4e7bfc63c35ac5..2e199ae453f1b5 100644 --- a/x-pack/plugins/osquery/cypress/tasks/live_query.ts +++ b/x-pack/plugins/osquery/cypress/tasks/live_query.ts @@ -7,13 +7,14 @@ import { LIVE_QUERY_EDITOR } from '../screens/live_query'; -export const DEFAULT_QUERY = 'select * from processes, users;'; +export const DEFAULT_QUERY = 'select * from processes;'; +export const BIG_QUERY = 'select * from processes, users;'; export const selectAllAgents = () => { cy.react('EuiComboBox', { props: { placeholder: 'Select agents or groups' } }).type('All agents'); cy.react('EuiFilterSelectItem').contains('All agents').should('exist'); cy.react('EuiComboBox', { props: { placeholder: 'Select agents or groups' } }).type( - '{downArrow}{enter}' + '{downArrow}{enter}{esc}' ); }; diff --git a/x-pack/plugins/osquery/cypress/tasks/packs.ts b/x-pack/plugins/osquery/cypress/tasks/packs.ts index 3218c792772baa..5f9ace1157a41b 100644 --- a/x-pack/plugins/osquery/cypress/tasks/packs.ts +++ b/x-pack/plugins/osquery/cypress/tasks/packs.ts @@ -5,7 +5,7 @@ * 2.0. */ -export const preparePack = (packName: string, savedQueryId: string) => { +export const preparePack = (packName: string) => { cy.contains('Packs').click(); const createdPack = cy.contains(packName); createdPack.click(); diff --git a/x-pack/plugins/osquery/public/packs/types.ts b/x-pack/plugins/osquery/public/packs/types.ts index 30cae97b006bb2..95e488b8cc6983 100644 --- a/x-pack/plugins/osquery/public/packs/types.ts +++ b/x-pack/plugins/osquery/public/packs/types.ts @@ -16,7 +16,7 @@ export interface IQueryPayload { export type PackSavedObject = SavedObject<{ name: string; description: string | undefined; - queries: Array>; + queries: Array>; enabled: boolean | undefined; created_at: string; created_by: string | undefined; diff --git a/x-pack/plugins/osquery/public/plugin.ts b/x-pack/plugins/osquery/public/plugin.ts index 86a1f89f738b6b..8a0b61d7aaba61 100644 --- a/x-pack/plugins/osquery/public/plugin.ts +++ b/x-pack/plugins/osquery/public/plugin.ts @@ -26,7 +26,7 @@ import { LazyOsqueryManagedPolicyEditExtension, LazyOsqueryManagedCustomButtonExtension, } from './fleet_integration'; -import { getLazyOsqueryAction } from './shared_components'; +import { getLazyOsqueryAction, useIsOsqueryAvailableSimple } from './shared_components'; export class OsqueryPlugin implements Plugin { private kibanaVersion: string; @@ -95,6 +95,7 @@ export class OsqueryPlugin implements Plugin = ({ metadata }) => { +const OsqueryActionComponent: React.FC = ({ agentId, formType = 'simple' }) => { const permissions = useKibana().services.application.capabilities.osquery; - const agentId = metadata?.info?.agent?.id ?? undefined; - const { - data: agentData, - isFetched: agentFetched, - isLoading, - } = useAgentDetails({ - agentId, - silent: true, - skip: !agentId, - }); - const { - data: agentPolicyData, - isFetched: policyFetched, - isError: policyError, - isLoading: policyLoading, - } = useAgentPolicy({ - policyId: agentData?.policy_id, - skip: !agentData, - silent: true, - }); - - const osqueryAvailable = useMemo(() => { - if (policyError) return false; - - const osqueryPackageInstalled = find(agentPolicyData?.package_policies, [ - 'package.name', - OSQUERY_INTEGRATION_NAME, - ]); - return osqueryPackageInstalled?.enabled; - }, [agentPolicyData?.package_policies, policyError]); - if (!(permissions.runSavedQueries || permissions.writeLiveQueries)) { - return ( + const emptyPrompt = useMemo( + () => ( } - title={

Permissions denied

} + title={ +

+ {i18n.translate('xpack.osquery.action.shortEmptyTitle', { + defaultMessage: 'Osquery is not available', + })} +

+ } titleSize="xs" body={

- To access this page, ask your administrator for osquery Kibana - privileges. + {i18n.translate('xpack.osquery.action.empty', { + defaultMessage: + 'An Elastic Agent is not installed on this host. To run queries, install Elastic Agent on the host, and then add the Osquery Manager integration to the agent policy in Fleet.', + })}

} /> - ); - } + ), + [] + ); + const { osqueryAvailable, agentFetched, isLoading, policyFetched, policyLoading, agentData } = + useIsOsqueryAvailable(agentId); - if (isLoading) { - return ; + if (!agentId || (agentFetched && !agentData)) { + return emptyPrompt; } - if (!agentId || (agentFetched && !agentData)) { + if (!(permissions.runSavedQueries || permissions.writeLiveQueries)) { return ( } - title={

Osquery is not available

} + title={ +

+ {i18n.translate('xpack.osquery.action.permissionDenied', { + defaultMessage: 'Permission denied', + })} +

+ } titleSize="xs" body={

- An Elastic Agent is not installed on this host. To run queries, install Elastic Agent on - the host, and then add the Osquery Manager integration to the agent policy in Fleet. + To access this page, ask your administrator for osquery Kibana + privileges.

} /> ); } + if (isLoading) { + return ; + } + if (!policyFetched && policyLoading) { return ; } @@ -104,12 +90,20 @@ const OsqueryActionComponent: React.FC = ({ metadata }) => { return ( } - title={

Osquery is not available

} + title={ +

+ {i18n.translate('xpack.osquery.action.shortEmptyTitle', { + defaultMessage: 'Osquery is not available', + })} +

+ } titleSize="xs" body={

- The Osquery Manager integration is not added to the agent policy. To run queries on the - host, add the Osquery Manager integration to the agent policy in Fleet. + {i18n.translate('xpack.osquery.action.unavailable', { + defaultMessage: + 'The Osquery Manager integration is not added to the agent policy. To run queries on the host, add the Osquery Manager integration to the agent policy in Fleet.', + })}

} /> @@ -120,30 +114,38 @@ const OsqueryActionComponent: React.FC = ({ metadata }) => { return ( } - title={

Osquery is not available

} + title={ +

+ {i18n.translate('xpack.osquery.action.shortEmptyTitle', { + defaultMessage: 'Osquery is not available', + })} +

+ } titleSize="xs" body={

- To run queries on this host, the Elastic Agent must be active. Check the status of this - agent in Fleet. + {i18n.translate('xpack.osquery.action.agentStatus', { + defaultMessage: + 'To run queries on this host, the Elastic Agent must be active. Check the status of this agent in Fleet.', + })}

} /> ); } - return ; + return ; }; -export const OsqueryAction = React.memo(OsqueryActionComponent); +const OsqueryAction = React.memo(OsqueryActionComponent); // @ts-expect-error update types -const OsqueryActionWrapperComponent = ({ services, ...props }) => ( +const OsqueryActionWrapperComponent = ({ services, agentId, formType }) => ( - + diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available.ts b/x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available.ts new file mode 100644 index 00000000000000..595296e4d7b60b --- /dev/null +++ b/x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available.ts @@ -0,0 +1,46 @@ +/* + * 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 { useMemo } from 'react'; +import { find } from 'lodash'; +import { useAgentDetails } from '../../agents/use_agent_details'; +import { useAgentPolicy } from '../../agent_policies'; +import { OSQUERY_INTEGRATION_NAME } from '../../../common'; + +export const useIsOsqueryAvailable = (agentId?: string) => { + const { + data: agentData, + isFetched: agentFetched, + isLoading, + } = useAgentDetails({ + agentId, + silent: true, + skip: !agentId, + }); + const { + data: agentPolicyData, + isFetched: policyFetched, + isError: policyError, + isLoading: policyLoading, + } = useAgentPolicy({ + policyId: agentData?.policy_id, + skip: !agentData, + silent: true, + }); + + const osqueryAvailable = useMemo(() => { + if (policyError) return false; + + const osqueryPackageInstalled = find(agentPolicyData?.package_policies, [ + 'package.name', + OSQUERY_INTEGRATION_NAME, + ]); + return osqueryPackageInstalled?.enabled; + }, [agentPolicyData?.package_policies, policyError]); + + return { osqueryAvailable, agentFetched, isLoading, policyFetched, policyLoading, agentData }; +}; diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available_simple.test.ts b/x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available_simple.test.ts new file mode 100644 index 00000000000000..c293e4c75a910c --- /dev/null +++ b/x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available_simple.test.ts @@ -0,0 +1,55 @@ +/* + * 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 { useKibana } from '../../common/lib/kibana'; +import { useIsOsqueryAvailableSimple } from './use_is_osquery_available_simple'; +import { renderHook } from '@testing-library/react-hooks'; +import { createStartServicesMock } from '../../../../triggers_actions_ui/public/common/lib/kibana/kibana_react.mock'; +import { OSQUERY_INTEGRATION_NAME } from '../../../common'; +import { httpServiceMock } from '../../../../../../src/core/public/mocks'; + +jest.mock('../../common/lib/kibana'); + +const response = { + item: { + policy_id: '4234234234', + package_policies: [ + { + package: { name: OSQUERY_INTEGRATION_NAME }, + enabled: true, + }, + ], + }, +}; + +describe('UseIsOsqueryAvailableSimple', () => { + const mockedHttp = httpServiceMock.createStartContract(); + mockedHttp.get.mockResolvedValue(response); + beforeAll(() => { + (useKibana as jest.Mock).mockImplementation(() => { + const mockStartServicesMock = createStartServicesMock(); + + return { + services: { + ...mockStartServicesMock, + http: mockedHttp, + }, + }; + }); + }); + it('should expect response from API and return enabled flag', async () => { + const { result, waitForValueToChange } = renderHook(() => + useIsOsqueryAvailableSimple({ + agentId: '3242332', + }) + ); + + expect(result.current).toBe(false); + await waitForValueToChange(() => result.current); + + expect(result.current).toBe(true); + }); +}); diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available_simple.tsx b/x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available_simple.tsx new file mode 100644 index 00000000000000..efe34b51ea0a39 --- /dev/null +++ b/x-pack/plugins/osquery/public/shared_components/osquery_action/use_is_osquery_available_simple.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useState } from 'react'; + +import { find } from 'lodash'; +import { useKibana } from '../../common/lib/kibana'; +import { OSQUERY_INTEGRATION_NAME } from '../../../common'; +import { AgentPolicy, FleetServerAgent, NewPackagePolicy } from '../../../../fleet/common'; + +interface IProps { + agentId: string; +} + +export const useIsOsqueryAvailableSimple = ({ agentId }: IProps) => { + const { http } = useKibana().services; + const [isAvailable, setIsAvailable] = useState(false); + useEffect(() => { + (async () => { + try { + const { item: agentInfo }: { item: FleetServerAgent } = await http.get( + `/internal/osquery/fleet_wrapper/agents/${agentId}` + ); + const { item: packageInfo }: { item: AgentPolicy } = await http.get( + `/internal/osquery/fleet_wrapper/agent_policies/${agentInfo.policy_id}/` + ); + const osqueryPackageInstalled = find(packageInfo?.package_policies, [ + 'package.name', + OSQUERY_INTEGRATION_NAME, + ]) as NewPackagePolicy; + setIsAvailable(osqueryPackageInstalled.enabled); + } catch (err) { + return; + } + })(); + }, [agentId, http]); + + return isAvailable; +}; diff --git a/x-pack/plugins/osquery/public/types.ts b/x-pack/plugins/osquery/public/types.ts index fd21b39d25504e..91095b6f169c1a 100644 --- a/x-pack/plugins/osquery/public/types.ts +++ b/x-pack/plugins/osquery/public/types.ts @@ -22,6 +22,7 @@ import { getLazyOsqueryAction } from './shared_components'; export interface OsqueryPluginSetup {} export interface OsqueryPluginStart { OsqueryAction?: ReturnType; + isOsqueryAvailable: (props: { agentId: string }) => boolean; } export interface AppPluginStartDependencies { diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index ba289e48fd6a20..36edfd43d5ea5d 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -39,7 +39,8 @@ "lists", "home", "telemetry", - "dataViewFieldEditor" + "dataViewFieldEditor", + "osquery" ], "server": true, "ui": true, diff --git a/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_action_item.tsx b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_action_item.tsx new file mode 100644 index 00000000000000..ca61e2f3ebf6d8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_action_item.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiContextMenuItem } from '@elastic/eui'; +import { ACTION_OSQUERY } from './translations'; + +interface IProps { + handleClick: () => void; +} + +export const OsqueryActionItem = ({ handleClick }: IProps) => { + return ( + + {ACTION_OSQUERY} + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx new file mode 100644 index 00000000000000..3262fc36abf75b --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout.tsx @@ -0,0 +1,58 @@ +/* + * 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 styled from 'styled-components'; +import { EuiFlyout, EuiFlyoutFooter, EuiFlyoutBody, EuiFlyoutHeader } from '@elastic/eui'; +import { useKibana } from '../../../common/lib/kibana'; +import { OsqueryEventDetailsFooter } from './osquery_flyout_footer'; +import { OsqueryEventDetailsHeader } from './osquery_flyout_header'; +import { ACTION_OSQUERY } from './translations'; + +const OsqueryActionWrapper = styled.div` + padding: 8px; +`; + +export interface OsqueryFlyoutProps { + agentId: string; + onClose: () => void; +} + +export const OsqueryFlyout: React.FC = ({ agentId, onClose }) => { + const { + services: { osquery }, + } = useKibana(); + + // @ts-expect-error + const { OsqueryAction } = osquery; + return ( + + + {ACTION_OSQUERY}
} + handleClick={onClose} + data-test-subj="flyout-header-osquery" + /> + + + + + + + + + + + ); +}; + +OsqueryFlyout.displayName = 'OsqueryFlyout'; diff --git a/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout_footer.tsx b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout_footer.tsx new file mode 100644 index 00000000000000..77cade0e04042e --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout_footer.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +interface EventDetailsFooterProps { + handleClick: () => void; +} + +export const OsqueryEventDetailsFooterComponent = ({ handleClick }: EventDetailsFooterProps) => { + return ( + + + + + + + + ); +}; + +export const OsqueryEventDetailsFooter = React.memo(OsqueryEventDetailsFooterComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout_header.tsx b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout_header.tsx new file mode 100644 index 00000000000000..7a0f7f15f3e748 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/osquery/osquery_flyout_header.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButtonEmpty, EuiText, EuiTitle } from '@elastic/eui'; +import { BACK_TO_ALERT_DETAILS } from './translations'; + +interface IProps { + primaryText: React.ReactElement; + handleClick: () => void; +} + +const OsqueryEventDetailsHeaderComponent: React.FC = ({ primaryText, handleClick }) => { + return ( + <> + + +

{BACK_TO_ALERT_DETAILS}

+
+
+ {primaryText} + + ); +}; + +export const OsqueryEventDetailsHeader = React.memo(OsqueryEventDetailsHeaderComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/osquery/translations.ts b/x-pack/plugins/security_solution/public/detections/components/osquery/translations.ts new file mode 100644 index 00000000000000..d3c92ebdf44e28 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/osquery/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const BACK_TO_ALERT_DETAILS = i18n.translate( + 'xpack.securitySolution.alertsView.osqueryBackToAlertDetails', + { + defaultMessage: 'Alert Details', + } +); + +export const ACTION_OSQUERY = i18n.translate( + 'xpack.securitySolution.alertsView.osqueryAlertTitle', + { + defaultMessage: 'Run Osquery', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx index 938022b5aac5e2..8aa8986d3e5634 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx @@ -78,6 +78,7 @@ describe('take action dropdown', () => { refetch: jest.fn(), refetchFlyoutData: jest.fn(), timelineId: TimelineId.active, + onOsqueryClick: jest.fn(), }; beforeAll(() => { @@ -89,8 +90,11 @@ describe('take action dropdown', () => { ...mockStartServicesMock, timelines: { ...mockTimelines }, cases: mockCasesContract(), + osquery: { + isOsqueryAvailable: jest.fn().mockReturnValue(true), + }, application: { - capabilities: { siem: { crud_alerts: true, read_alerts: true } }, + capabilities: { siem: { crud_alerts: true, read_alerts: true }, osquery: true }, }, }, }; @@ -190,6 +194,13 @@ describe('take action dropdown', () => { ).toEqual('Investigate in timeline'); }); }); + test('should render "Run Osquery"', async () => { + await waitFor(() => { + expect(wrapper.find('[data-test-subj="osquery-action-item"]').first().text()).toEqual( + 'Run Osquery' + ); + }); + }); }); describe('should correctly enable/disable the "Add Endpoint event filter" button', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx index 4a35fdd6a13811..94b9327bb439a5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import React, { useState, useCallback, useMemo } from 'react'; -import { EuiContextMenuPanel, EuiButton, EuiPopover } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; +import { EuiButton, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; import { TAKE_ACTION } from '../alerts_table/alerts_utility_bar/translations'; @@ -23,6 +23,8 @@ import { isAlertFromEndpointAlert } from '../../../common/utils/endpoint_alert_c import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { useUserPrivileges } from '../../../common/components/user_privileges'; import { useAddToCaseActions } from '../alerts_table/timeline_actions/use_add_to_case_actions'; +import { useKibana } from '../../../common/lib/kibana'; +import { OsqueryActionItem } from '../osquery/osquery_action_item'; interface ActionsData { alertStatus: Status; @@ -45,6 +47,7 @@ export interface TakeActionDropdownProps { refetch: (() => void) | undefined; refetchFlyoutData: () => Promise; timelineId: string; + onOsqueryClick: (id: string) => void; } export const TakeActionDropdown = React.memo( @@ -61,6 +64,7 @@ export const TakeActionDropdown = React.memo( refetch, refetchFlyoutData, timelineId, + onOsqueryClick, }: TakeActionDropdownProps) => { const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); const { loading: canAccessEndpointManagementLoading, canAccessEndpointManagement } = @@ -70,6 +74,7 @@ export const TakeActionDropdown = React.memo( () => !canAccessEndpointManagementLoading && canAccessEndpointManagement, [canAccessEndpointManagement, canAccessEndpointManagementLoading] ); + const { osquery } = useKibana().services; const [isPopoverOpen, setIsPopoverOpen] = useState(false); @@ -97,6 +102,11 @@ export const TakeActionDropdown = React.memo( const isEndpointEvent = useMemo(() => isEvent && isAgentEndpoint, [isEvent, isAgentEndpoint]); + const agentId = useMemo( + () => getFieldValue({ category: 'agent', field: 'agent.id' }, detailsData), + [detailsData] + ); + const togglePopoverHandler = useCallback(() => { setIsPopoverOpen(!isPopoverOpen); }, [isPopoverOpen]); @@ -166,6 +176,23 @@ export const TakeActionDropdown = React.memo( onInvestigateInTimelineAlertClick: closePopoverHandler, }); + const osqueryAvailable = osquery?.isOsqueryAvailable({ + agentId, + }); + + const handleOnOsqueryClick = useCallback(() => { + onOsqueryClick(agentId); + setIsPopoverOpen(false); + }, [onOsqueryClick, setIsPopoverOpen, agentId]); + + const osqueryActionItem = useMemo( + () => + OsqueryActionItem({ + handleClick: handleOnOsqueryClick, + }), + [handleOnOsqueryClick] + ); + const alertsActionItems = useMemo( () => !isEvent && actionsData.ruleId @@ -196,13 +223,16 @@ export const TakeActionDropdown = React.memo( ...(tGridEnabled ? addToCaseActionItems : []), ...alertsActionItems, ...hostIsolationActionItems, + ...(osqueryAvailable ? [osqueryActionItem] : []), ...investigateInTimelineActionItems, ], [ tGridEnabled, - alertsActionItems, addToCaseActionItems, + alertsActionItems, hostIsolationActionItems, + osqueryAvailable, + osqueryActionItem, investigateInTimelineActionItems, ] ); @@ -220,7 +250,6 @@ export const TakeActionDropdown = React.memo( ); }, [togglePopoverHandler]); - return items.length && !loadingEventDetails && ecsData ? ( (null); + + const closeOsqueryFlyout = useCallback(() => { + setOsqueryFlyoutOpenWithAgentId(null); + }, [setOsqueryFlyoutOpenWithAgentId]); + return ( <> @@ -128,6 +137,7 @@ export const EventDetailsFooterComponent = React.memo( refetch={refetchAll} indexName={expandedEvent.indexName} timelineId={timelineId} + onOsqueryClick={setOsqueryFlyoutOpenWithAgentId} /> )} @@ -154,6 +164,9 @@ export const EventDetailsFooterComponent = React.memo( maskProps={{ style: 'z-index: 5000' }} /> )} + {isOsqueryFlyoutOpenWithAgentId && detailsEcsData != null && ( + + )} ); } diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index d43f8752c91228..0916bc73f41986 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -28,6 +28,7 @@ import type { TimelinesUIStart } from '../../timelines/public'; import type { ResolverPluginSetup } from './resolver/types'; import type { Inspect } from '../common/search_strategy'; import type { MlPluginSetup, MlPluginStart } from '../../ml/public'; +import type { OsqueryPluginStart } from '../../osquery/public'; import type { Detections } from './detections'; import type { Cases } from './cases'; @@ -69,6 +70,7 @@ export interface StartPlugins { ml?: MlPluginStart; spaces?: SpacesPluginStart; dataViewFieldEditor: IndexPatternFieldEditorStart; + osquery?: OsqueryPluginStart; } export type StartServices = CoreStart & diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index d518eaf7f8243b..b1cb49b7379529 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -39,6 +39,7 @@ { "path": "../lists/tsconfig.json" }, { "path": "../maps/tsconfig.json" }, { "path": "../ml/tsconfig.json" }, + { "path": "../osquery/tsconfig.json" }, { "path": "../spaces/tsconfig.json" }, { "path": "../security/tsconfig.json" }, { "path": "../timelines/tsconfig.json" } diff --git a/x-pack/test/osquery_cypress/artifact_manager.ts b/x-pack/test/osquery_cypress/artifact_manager.ts index 4eaf16a33b6291..d96ef56ec87c8d 100644 --- a/x-pack/test/osquery_cypress/artifact_manager.ts +++ b/x-pack/test/osquery_cypress/artifact_manager.ts @@ -5,10 +5,11 @@ * 2.0. */ -import axios from 'axios'; -import { last } from 'lodash'; +// import axios from 'axios'; +// import { last } from 'lodash'; export async function getLatestVersion(): Promise { - const response: any = await axios('https://artifacts-api.elastic.co/v1/versions'); - return last(response.data.versions as string[]) || '8.2.0-SNAPSHOT'; + return '8.1.0-SNAPSHOT'; + // const response: any = await axios('https://artifacts-api.elastic.co/v1/versions'); + // return last(response.data.versions as string[]) || '8.2.0-SNAPSHOT'; } From 043c40b6d03b7bdf55209c08b681a6bf5963d4e8 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 23 Mar 2022 10:21:09 +0200 Subject: [PATCH 31/64] [Cases] Fix fields query parameter in the `_find` API (#128143) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: lcawl --- docs/api/cases/cases-api-find-cases.asciidoc | 3 +- x-pack/plugins/cases/common/api/cases/case.ts | 2 +- .../cases/server/authorization/utils.test.ts | 4 +- .../cases/server/authorization/utils.ts | 2 +- .../plugins/cases/server/client/cases/find.ts | 6 ++- .../tests/common/cases/find_cases.ts | 40 +++++++++++++++++++ 6 files changed, 49 insertions(+), 8 deletions(-) diff --git a/docs/api/cases/cases-api-find-cases.asciidoc b/docs/api/cases/cases-api-find-cases.asciidoc index 334f45fee526df..68e620aece7b60 100644 --- a/docs/api/cases/cases-api-find-cases.asciidoc +++ b/docs/api/cases/cases-api-find-cases.asciidoc @@ -31,10 +31,9 @@ default space is used. (Optional, string) The default operator to use for the `simple_query_string`. Defaults to `OR`. -//// `fields`:: (Optional, array of strings) The fields in the entity to return in the response. -//// + `owner`:: (Optional, string or array of strings) A filter to limit the retrieved cases to a specific set of applications. Valid values are: `cases`, `observability`, diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index 6d3a0b524f8904..1bc14fa8d3ab93 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -153,7 +153,7 @@ export const CasesFindRequestRt = rt.partial({ /** * The fields in the entity to return in the response */ - fields: rt.array(rt.string), + fields: rt.union([rt.array(rt.string), rt.string]), /** * The page of objects to return */ diff --git a/x-pack/plugins/cases/server/authorization/utils.test.ts b/x-pack/plugins/cases/server/authorization/utils.test.ts index 7717edfc909efc..b0b830f1a014d6 100644 --- a/x-pack/plugins/cases/server/authorization/utils.test.ts +++ b/x-pack/plugins/cases/server/authorization/utils.test.ts @@ -174,8 +174,8 @@ describe('utils', () => { expect(includeFieldsRequiredForAuthentication()).toBeUndefined(); }); - it('returns an array with a single entry containing the owner field', () => { - expect(includeFieldsRequiredForAuthentication([])).toStrictEqual([OWNER_FIELD]); + it('returns undefined when the fields parameter is an empty array', () => { + expect(includeFieldsRequiredForAuthentication([])).toBeUndefined(); }); it('returns an array without duplicates and including the owner field', () => { diff --git a/x-pack/plugins/cases/server/authorization/utils.ts b/x-pack/plugins/cases/server/authorization/utils.ts index ac88f96fb4e141..d33d3dd99a47f8 100644 --- a/x-pack/plugins/cases/server/authorization/utils.ts +++ b/x-pack/plugins/cases/server/authorization/utils.ts @@ -58,7 +58,7 @@ export const ensureFieldIsSafeForQuery = (field: string, value: string): boolean }; export const includeFieldsRequiredForAuthentication = (fields?: string[]): string[] | undefined => { - if (fields === undefined) { + if (fields === undefined || fields.length === 0) { return; } return uniq([...fields, OWNER_FIELD]); diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index c8bdb40b41310a..26ac4603c51e59 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -38,8 +38,10 @@ export const find = async ( const { caseService, authorization, logger } = clientArgs; try { + const fields = asArray(params.fields); + const queryParams = pipe( - excess(CasesFindRequestRt).decode(params), + excess(CasesFindRequestRt).decode({ ...params, fields }), fold(throwErrors(Boom.badRequest), identity) ); @@ -67,7 +69,7 @@ export const find = async ( ...queryParams, ...caseQueryOptions, searchFields: asArray(queryParams.searchFields), - fields: includeFieldsRequiredForAuthentication(queryParams.fields), + fields: includeFieldsRequiredForAuthentication(fields), }, }), caseService.getCaseStatusStats({ diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts index 48d6515d73d0db..89f6f96aeb7d19 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts @@ -195,6 +195,46 @@ export default ({ getService }: FtrProviderContext): void => { expect(cases.count_in_progress_cases).to.eql(1); }); + it('returns the correct fields', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const queryFields: Array> = [ + 'title', + ['title', 'description'], + ]; + + for (const fields of queryFields) { + const cases = await findCases({ supertest, query: { fields } }); + const fieldsAsArray = Array.isArray(fields) ? fields : [fields]; + + const expectedValues = fieldsAsArray.reduce( + (theCase, field) => ({ + ...theCase, + [field]: postedCase[field], + }), + {} + ); + + expect(cases).to.eql({ + ...findCasesResp, + total: 1, + cases: [ + { + id: postedCase.id, + version: postedCase.version, + external_service: postedCase.external_service, + owner: postedCase.owner, + connector: postedCase.connector, + comments: [], + totalAlerts: 0, + totalComment: 0, + ...expectedValues, + }, + ], + count_open_cases: 1, + }); + } + }); + it('unhappy path - 400s when bad query supplied', async () => { await findCases({ supertest, query: { perPage: true }, expectedHttpCode: 400 }); }); From 77034b3f541626cce3e177ee957d772826d44025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ester=20Mart=C3=AD=20Vilaseca?= Date: Wed, 23 Mar 2022 09:21:49 +0100 Subject: [PATCH 32/64] [Unified Observability] Guided setup button on the overview page (#128172) * Add guided setup button to overview page * Update content for status vis flyout * assure boxes order * Fix types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../app/observability_status/content.ts | 7 +++ .../observability_status.stories.tsx | 6 ++ .../observability_status_box.test.tsx | 2 + .../observability_status_box.tsx | 1 + .../observability_status_boxes.test.tsx | 43 +++++++++++++++ .../observability_status_boxes.tsx | 12 ++-- .../pages/overview/old_overview_page.tsx | 55 ++++++++++++++++++- 7 files changed, 120 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/observability/public/components/app/observability_status/content.ts b/x-pack/plugins/observability/public/components/app/observability_status/content.ts index 084d28a5544725..ea264a4387a85d 100644 --- a/x-pack/plugins/observability/public/components/app/observability_status/content.ts +++ b/x-pack/plugins/observability/public/components/app/observability_status/content.ts @@ -17,6 +17,7 @@ export interface ObservabilityStatusContent { learnMoreLink: string; goToAppTitle: string; goToAppLink: string; + weight: number; } export const getContent = ( @@ -42,6 +43,7 @@ export const getContent = ( defaultMessage: 'Show log stream', }), goToAppLink: http.basePath.prepend('/app/logs/stream'), + weight: 1, }, { id: 'apm', @@ -61,6 +63,7 @@ export const getContent = ( defaultMessage: 'Show services inventory', }), goToAppLink: http.basePath.prepend('/app/apm/services'), + weight: 3, }, { id: 'infra_metrics', @@ -79,6 +82,7 @@ export const getContent = ( defaultMessage: 'Show inventory', }), goToAppLink: http.basePath.prepend('/app/metrics/inventory'), + weight: 2, }, { id: 'synthetics', @@ -97,6 +101,7 @@ export const getContent = ( defaultMessage: 'Show monitors ', }), goToAppLink: http.basePath.prepend('/app/uptime'), + weight: 4, }, { id: 'ux', @@ -116,6 +121,7 @@ export const getContent = ( defaultMessage: 'Show dashboard', }), goToAppLink: http.basePath.prepend('/app/ux'), + weight: 5, }, { id: 'alert', @@ -135,6 +141,7 @@ export const getContent = ( defaultMessage: 'Show alerts', }), goToAppLink: http.basePath.prepend('/app/observability/alerts'), + weight: 6, }, ]; }; diff --git a/x-pack/plugins/observability/public/components/app/observability_status/observability_status.stories.tsx b/x-pack/plugins/observability/public/components/app/observability_status/observability_status.stories.tsx index c10ffa0500db6c..283d19210e45ab 100644 --- a/x-pack/plugins/observability/public/components/app/observability_status/observability_status.stories.tsx +++ b/x-pack/plugins/observability/public/components/app/observability_status/observability_status.stories.tsx @@ -27,6 +27,7 @@ const testBoxes = [ goToAppLink: '/app/logs/stream', hasData: false, modules: [], + weight: 1, }, { id: 'apm', @@ -40,6 +41,7 @@ const testBoxes = [ goToAppLink: '/app/apm/services', hasData: false, modules: [], + weight: 2, }, { id: 'infra_metrics', @@ -52,6 +54,7 @@ const testBoxes = [ goToAppLink: '/app/metrics/inventory', hasData: false, modules: [], + weight: 3, }, { id: 'synthetics', @@ -64,6 +67,7 @@ const testBoxes = [ goToAppLink: '/app/uptime', hasData: false, modules: [], + weight: 4, }, { id: 'ux', @@ -77,6 +81,7 @@ const testBoxes = [ goToAppLink: '/app/ux', hasData: true, modules: [], + weight: 5, }, { id: 'alert', @@ -90,6 +95,7 @@ const testBoxes = [ goToAppLink: '/app/observability/alerts', hasData: true, modules: [], + weight: 6, }, ]; diff --git a/x-pack/plugins/observability/public/components/app/observability_status/observability_status_box.test.tsx b/x-pack/plugins/observability/public/components/app/observability_status/observability_status_box.test.tsx index 7bc9cb60ad3497..088e0dea20bd0e 100644 --- a/x-pack/plugins/observability/public/components/app/observability_status/observability_status_box.test.tsx +++ b/x-pack/plugins/observability/public/components/app/observability_status/observability_status_box.test.tsx @@ -24,6 +24,7 @@ describe('ObservabilityStatusBox', () => { learnMoreLink: 'learnMoreUrl.com', goToAppTitle: 'go to app title', goToAppLink: 'go to app link', + weight: 1, }; render( @@ -60,6 +61,7 @@ describe('ObservabilityStatusBox', () => { learnMoreLink: 'http://example.com', goToAppTitle: 'go to app title', goToAppLink: 'go to app link', + weight: 1, }; render( diff --git a/x-pack/plugins/observability/public/components/app/observability_status/observability_status_box.tsx b/x-pack/plugins/observability/public/components/app/observability_status/observability_status_box.tsx index a819afab0bed5c..756fb995d489b3 100644 --- a/x-pack/plugins/observability/public/components/app/observability_status/observability_status_box.tsx +++ b/x-pack/plugins/observability/public/components/app/observability_status/observability_status_box.tsx @@ -31,6 +31,7 @@ export interface ObservabilityStatusBoxProps { learnMoreLink: string; goToAppTitle: string; goToAppLink: string; + weight: number; } export function ObservabilityStatusBox(props: ObservabilityStatusBoxProps) { diff --git a/x-pack/plugins/observability/public/components/app/observability_status/observability_status_boxes.test.tsx b/x-pack/plugins/observability/public/components/app/observability_status/observability_status_boxes.test.tsx index 9ad69b2ce64f8c..4c838eb6a7b2f3 100644 --- a/x-pack/plugins/observability/public/components/app/observability_status/observability_status_boxes.test.tsx +++ b/x-pack/plugins/observability/public/components/app/observability_status/observability_status_boxes.test.tsx @@ -24,6 +24,7 @@ describe('ObservabilityStatusBoxes', () => { learnMoreLink: 'http://example.com', goToAppTitle: 'go to app title', goToAppLink: 'go to app link', + weight: 2, }, { id: 'metrics', @@ -36,6 +37,7 @@ describe('ObservabilityStatusBoxes', () => { learnMoreLink: 'http://example.com', goToAppTitle: 'go to app title', goToAppLink: 'go to app link', + weight: 1, }, ]; @@ -48,4 +50,45 @@ describe('ObservabilityStatusBoxes', () => { expect(screen.getByText('Logs')).toBeInTheDocument(); expect(screen.getByText('Metrics')).toBeInTheDocument(); }); + + it('should render elements by order', () => { + const boxes = [ + { + id: 'logs', + title: 'Logs', + hasData: true, + description: 'This is the description for logs', + modules: [], + addTitle: 'logs add title', + addLink: 'http://example.com', + learnMoreLink: 'http://example.com', + goToAppTitle: 'go to app title', + goToAppLink: 'go to app link', + weight: 2, + }, + { + id: 'metrics', + title: 'Metrics', + hasData: true, + description: 'This is the description for metrics', + modules: [], + addTitle: 'metrics add title', + addLink: 'http://example.com', + learnMoreLink: 'http://example.com', + goToAppTitle: 'go to app title', + goToAppLink: 'go to app link', + weight: 1, + }, + ]; + + render( + + + + ); + + const content = screen.getAllByTestId(/box-*/); + expect(content[0]).toHaveTextContent('Metrics'); + expect(content[1]).toHaveTextContent('Logs'); + }); }); diff --git a/x-pack/plugins/observability/public/components/app/observability_status/observability_status_boxes.tsx b/x-pack/plugins/observability/public/components/app/observability_status/observability_status_boxes.tsx index 0827f7f8c768cc..48779569131d67 100644 --- a/x-pack/plugins/observability/public/components/app/observability_status/observability_status_boxes.tsx +++ b/x-pack/plugins/observability/public/components/app/observability_status/observability_status_boxes.tsx @@ -17,9 +17,13 @@ export interface ObservabilityStatusProps { boxes: ObservabilityStatusBoxProps[]; } +const sortingFn = (a: ObservabilityStatusBoxProps, b: ObservabilityStatusBoxProps) => { + return a.weight - b.weight; +}; + export function ObservabilityStatusBoxes({ boxes }: ObservabilityStatusProps) { - const hasDataBoxes = boxes.filter((box) => box.hasData); - const noHasDataBoxes = boxes.filter((box) => !box.hasData); + const hasDataBoxes = boxes.filter((box) => box.hasData).sort(sortingFn); + const noHasDataBoxes = boxes.filter((box) => !box.hasData).sort(sortingFn); return ( @@ -34,7 +38,7 @@ export function ObservabilityStatusBoxes({ boxes }: ObservabilityStatusProps) { {noHasDataBoxes.map((box) => ( - + ))} @@ -52,7 +56,7 @@ export function ObservabilityStatusBoxes({ boxes }: ObservabilityStatusProps) { {hasDataBoxes.map((box) => ( - + ))} diff --git a/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx b/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx index 39daf9b5aac8e4..88c82d8c355ac7 100644 --- a/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx +++ b/x-pack/plugins/observability/public/pages/overview/old_overview_page.tsx @@ -5,9 +5,21 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiHorizontalRule } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiHorizontalRule, + EuiButton, + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiText, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useMemo, useRef, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React, { useMemo, useRef, useCallback, useState } from 'react'; import { observabilityFeatureId } from '../../../common'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { useTrackPageview } from '../..'; @@ -33,6 +45,7 @@ import { ObservabilityAppServices } from '../../application/types'; import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions'; import { paths } from '../../config'; import { useDatePickerContext } from '../../hooks/use_date_picker_context'; +import { ObservabilityStatus } from '../../components/app/observability_status'; interface Props { routeParams: RouteParams<'/overview'>; } @@ -53,6 +66,7 @@ export function OverviewPage({ routeParams }: Props) { }), }, ]); + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); const indexNames = useAlertIndexNames(); const { cases, docLinks, http } = useKibana().services; @@ -110,6 +124,12 @@ export function OverviewPage({ routeParams }: Props) { ? { pageTitle: overviewPageTitle, rightSideItems: [ + setIsFlyoutVisible(true)}> + + , )} + {isFlyoutVisible && ( + setIsFlyoutVisible(false)} + aria-labelledby="statusVisualizationFlyoutTitle" + > + + +

+ +

+
+ + +

+ +

+
+
+ + + +
+ )} ); } From d3d36cf0c1fba9afa98b780e2efc19b0b1e9ae6b Mon Sep 17 00:00:00 2001 From: Emmanuelle Raffenne <97166868+emma-raffenne@users.noreply.github.com> Date: Wed, 23 Mar 2022 09:08:49 +0000 Subject: [PATCH 33/64] Action to add issue to AO project (#127312) * Action to add issue to AO project * Switch to using richkuz projectnext-assigner as per jasonrhodes suggestion * Add condition to run only on issues labeled as AO Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .github/workflows/add-to-ao-project.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/add-to-ao-project.yml diff --git a/.github/workflows/add-to-ao-project.yml b/.github/workflows/add-to-ao-project.yml new file mode 100644 index 00000000000000..c89e8fcefb712a --- /dev/null +++ b/.github/workflows/add-to-ao-project.yml @@ -0,0 +1,23 @@ +name: Add issues to Actionable Observability project +on: + issues: + types: [labeled] +jobs: + sync_issues_with_table: + runs-on: ubuntu-latest + name: Add issues to project + if: | + github.event.label.name == 'Team: Actionable Observability' + steps: + - name: Add + uses: richkuz/projectnext-label-assigner@1.0.2 + id: add_to_projects + with: + config: | + [ + {"label": "Team: Actionable Observability", "projectNumber": 669} + ] + env: + GRAPHQL_API_BASE: 'https://api.github.com' + PAT_TOKEN: ${{ secrets.PROJECT_ASSIGNER_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From daea5a8519dafccdab851f7436941ef58a974ba7 Mon Sep 17 00:00:00 2001 From: Faisal Kanout Date: Wed, 23 Mar 2022 12:18:50 +0300 Subject: [PATCH 34/64] [RAC][UPTIME] -126229 - Add view in app url as an action variable in the alert message for uptime app (#127478) * Expose getMonitorRouteFromMonitorId in the common folder * Remove unused import * WIP * Fix some issues * Add 5 min before when the alert is raised * Update status * Cover the autogenerated use case * Update tests * Updated tests * Use indexedStartedAt and full URL * Take into consideration the kibanaBase path * Fix URL * LINT * Fix tests * Use IBasePath for clarity and update related tests * Optim - use getViewInAppUrl * Add duration anomaly and fix tests * Fix tests * Rename server var * Remove join --- .../uptime/common/utils/get_monitor_url.ts | 40 +++++++++++++++ .../uptime/public/lib/alert_types/common.ts | 34 ------------- .../lib/alert_types/duration_anomaly.tsx | 2 +- .../public/lib/alert_types/monitor_status.tsx | 2 +- .../lib/adapters/framework/adapter_types.ts | 8 ++- .../server/lib/alerts/action_variables.ts | 11 +++++ .../uptime/server/lib/alerts/common.ts | 5 ++ .../lib/alerts/duration_anomaly.test.ts | 4 +- .../server/lib/alerts/duration_anomaly.ts | 25 +++++++--- .../server/lib/alerts/status_check.test.ts | 6 +++ .../uptime/server/lib/alerts/status_check.ts | 49 +++++++++++++++---- .../server/lib/alerts/test_utils/index.ts | 12 ++++- .../plugins/uptime/server/lib/alerts/types.ts | 1 + x-pack/plugins/uptime/server/plugin.ts | 1 + 14 files changed, 145 insertions(+), 55 deletions(-) create mode 100644 x-pack/plugins/uptime/common/utils/get_monitor_url.ts diff --git a/x-pack/plugins/uptime/common/utils/get_monitor_url.ts b/x-pack/plugins/uptime/common/utils/get_monitor_url.ts new file mode 100644 index 00000000000000..09b02150957d03 --- /dev/null +++ b/x-pack/plugins/uptime/common/utils/get_monitor_url.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { stringify } from 'querystring'; + +export const format = ({ + pathname, + query, +}: { + pathname: string; + query: Record; +}): string => { + return `${pathname}?${stringify(query)}`; +}; + +export const getMonitorRouteFromMonitorId = ({ + monitorId, + dateRangeStart, + dateRangeEnd, + filters = {}, +}: { + monitorId: string; + dateRangeStart: string; + dateRangeEnd: string; + filters?: Record; +}) => + format({ + pathname: `/app/uptime/monitor/${btoa(monitorId)}`, + query: { + dateRangeEnd, + dateRangeStart, + ...(Object.keys(filters).length + ? { filters: JSON.stringify(Object.keys(filters).map((key) => [key, filters[key]])) } + : {}), + }, + }); diff --git a/x-pack/plugins/uptime/public/lib/alert_types/common.ts b/x-pack/plugins/uptime/public/lib/alert_types/common.ts index 6a45f73357597e..0835cc4b5202c9 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/common.ts +++ b/x-pack/plugins/uptime/public/lib/alert_types/common.ts @@ -5,40 +5,6 @@ * 2.0. */ -import { stringify } from 'querystring'; - -export const format = ({ - pathname, - query, -}: { - pathname: string; - query: Record; -}): string => { - return `${pathname}?${stringify(query)}`; -}; - -export const getMonitorRouteFromMonitorId = ({ - monitorId, - dateRangeStart, - dateRangeEnd, - filters = {}, -}: { - monitorId: string; - dateRangeStart: string; - dateRangeEnd: string; - filters?: Record; -}) => - format({ - pathname: `/app/uptime/monitor/${btoa(monitorId)}`, - query: { - dateRangeEnd, - dateRangeStart, - ...(Object.keys(filters).length - ? { filters: JSON.stringify(Object.keys(filters).map((key) => [key, filters[key]])) } - : {}), - }, - }); - export const getUrlForAlert = (id: string, basePath: string) => { return basePath + '/app/management/insightsAndAlerting/triggersActions/alert/' + id; }; diff --git a/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx b/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx index cdd0441575b337..d6498015e41ce0 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx @@ -11,7 +11,7 @@ import moment from 'moment'; import { ALERT_END, ALERT_STATUS, ALERT_STATUS_ACTIVE, ALERT_REASON } from '@kbn/rule-data-utils'; import { AlertTypeInitializer } from '.'; -import { getMonitorRouteFromMonitorId } from './common'; +import { getMonitorRouteFromMonitorId } from '../../../common/utils/get_monitor_url'; import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts'; import { DurationAnomalyTranslations } from '../../../common/translations'; import { ObservabilityRuleTypeModel } from '../../../../observability/public'; diff --git a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx index 9737753df0225e..5d6f8f3fea3337 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx @@ -17,7 +17,7 @@ import { } from '@kbn/rule-data-utils'; import { AlertTypeInitializer } from '.'; -import { getMonitorRouteFromMonitorId } from './common'; +import { getMonitorRouteFromMonitorId } from '../../../common/utils/get_monitor_url'; import { MonitorStatusTranslations } from '../../../common/translations'; import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts'; import { ObservabilityRuleTypeModel } from '../../../../observability/public'; diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index 9efb7e36ebab9e..d9dadc81397cec 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -6,7 +6,12 @@ */ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import type { SavedObjectsClientContract, IScopedClusterClient, Logger } from 'src/core/server'; +import type { + SavedObjectsClientContract, + IScopedClusterClient, + Logger, + IBasePath, +} from 'src/core/server'; import type { TelemetryPluginSetup, TelemetryPluginStart } from 'src/plugins/telemetry/server'; import { ObservabilityPluginSetup } from '../../../../../observability/server'; import { @@ -56,6 +61,7 @@ export interface UptimeServerSetup { logger: Logger; telemetry: TelemetryEventsSender; uptimeEsClient: UptimeESClient; + basePath: IBasePath; } export interface UptimeCorePluginsSetup { diff --git a/x-pack/plugins/uptime/server/lib/alerts/action_variables.ts b/x-pack/plugins/uptime/server/lib/alerts/action_variables.ts index 48fa6e45f19a89..763cccb404e51c 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/action_variables.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/action_variables.ts @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; export const MESSAGE = 'message'; export const MONITOR_WITH_GEO = 'downMonitorsWithGeo'; export const ALERT_REASON_MSG = 'reason'; +export const VIEW_IN_APP_URL = 'viewInAppUrl'; export const ACTION_VARIABLES = { [MESSAGE]: { @@ -40,4 +41,14 @@ export const ACTION_VARIABLES = { } ), }, + [VIEW_IN_APP_URL]: { + name: VIEW_IN_APP_URL, + description: i18n.translate( + 'xpack.uptime.alerts.monitorStatus.actionVariables.context.viewInAppUrl.description', + { + defaultMessage: + 'Link to the view or feature within Elastic that can be used to investigate the alert and its context further', + } + ), + }, }; diff --git a/x-pack/plugins/uptime/server/lib/alerts/common.ts b/x-pack/plugins/uptime/server/lib/alerts/common.ts index 6bf9d28c2da9e1..542aaa27819a3e 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/common.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/common.ts @@ -7,6 +7,7 @@ import { isRight } from 'fp-ts/lib/Either'; import Mustache from 'mustache'; +import { IBasePath } from 'kibana/server'; import { UptimeCommonState, UptimeCommonStateType } from '../../../common/runtime_types'; export type UpdateUptimeAlertState = ( @@ -60,3 +61,7 @@ export const updateState: UpdateUptimeAlertState = (state, isTriggeredNow) => { export const generateAlertMessage = (messageTemplate: string, fields: Record) => { return Mustache.render(messageTemplate, { state: { ...fields } }); }; +export const getViewInAppUrl = (relativeViewInAppUrl: string, basePath: IBasePath) => + basePath.publicBaseUrl + ? new URL(basePath.prepend(relativeViewInAppUrl), basePath.publicBaseUrl).toString() + : relativeViewInAppUrl; diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts index 208f19354a0f3a..2848df14776b54 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts @@ -16,7 +16,7 @@ import { DynamicSettings } from '../../../common/runtime_types'; import { createRuleTypeMocks, bootstrapDependencies } from './test_utils'; import { getSeverityType } from '../../../../ml/common/util/anomaly_utils'; import { Ping } from '../../../common/runtime_types/ping'; -import { ALERT_REASON_MSG } from './action_variables'; +import { ALERT_REASON_MSG, VIEW_IN_APP_URL } from './action_variables'; interface MockAnomaly { severity: AnomaliesTableRecord['severity']; @@ -219,6 +219,7 @@ Response times as high as ${slowestResponse} ms have been detected from location "xpack.uptime.alerts.actionGroups.durationAnomaly", Object { "${ALERT_REASON_MSG}": "${reasonMessages[0]}", + "${VIEW_IN_APP_URL}": "http://localhost:5601/hfe/app/uptime/monitor/eHBhY2sudXB0aW1lLmFsZXJ0cy5hY3Rpb25Hcm91cHMuZHVyYXRpb25Bbm9tYWx5MA==?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z", }, ] `); @@ -227,6 +228,7 @@ Response times as high as ${slowestResponse} ms have been detected from location "xpack.uptime.alerts.actionGroups.durationAnomaly", Object { "${ALERT_REASON_MSG}": "${reasonMessages[1]}", + "${VIEW_IN_APP_URL}": "http://localhost:5601/hfe/app/uptime/monitor/eHBhY2sudXB0aW1lLmFsZXJ0cy5hY3Rpb25Hcm91cHMuZHVyYXRpb25Bbm9tYWx5MQ==?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z", }, ] `); diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts index 1dcb91b9e5270f..d1e83c917d6f7f 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts @@ -13,7 +13,7 @@ import { ALERT_REASON, } from '@kbn/rule-data-utils'; import { ActionGroupIdsOf } from '../../../../alerting/common'; -import { updateState, generateAlertMessage } from './common'; +import { updateState, generateAlertMessage, getViewInAppUrl } from './common'; import { DURATION_ANOMALY } from '../../../common/constants/alerts'; import { commonStateTranslations, durationAnomalyTranslations } from './translations'; import { AnomaliesTableRecord } from '../../../../ml/common/types/anomalies'; @@ -24,9 +24,10 @@ import { Ping } from '../../../common/runtime_types/ping'; import { getMLJobId } from '../../../common/lib'; import { DurationAnomalyTranslations as CommonDurationAnomalyTranslations } from '../../../common/translations'; +import { getMonitorRouteFromMonitorId } from '../../../common/utils/get_monitor_url'; import { createUptimeESClient } from '../lib'; -import { ALERT_REASON_MSG, ACTION_VARIABLES } from './action_variables'; +import { ALERT_REASON_MSG, ACTION_VARIABLES, VIEW_IN_APP_URL } from './action_variables'; export type ActionGroupIds = ActionGroupIdsOf; @@ -72,7 +73,7 @@ const getAnomalies = async ( }; export const durationAnomalyAlertFactory: UptimeAlertTypeFactory = ( - _server, + server, libs, plugins ) => ({ @@ -93,20 +94,23 @@ export const durationAnomalyAlertFactory: UptimeAlertTypeFactory }, ], actionVariables: { - context: [ACTION_VARIABLES[ALERT_REASON_MSG]], + context: [ACTION_VARIABLES[ALERT_REASON_MSG], ACTION_VARIABLES[VIEW_IN_APP_URL]], state: [...durationAnomalyTranslations.actionVariables, ...commonStateTranslations], }, isExportable: true, minimumLicenseRequired: 'platinum', async executor({ params, - services: { alertWithLifecycle, scopedClusterClient, savedObjectsClient }, + services: { alertWithLifecycle, scopedClusterClient, savedObjectsClient, getAlertStartedDate }, state, + startedAt, }) { const uptimeEsClient = createUptimeESClient({ esClient: scopedClusterClient.asCurrentUser, savedObjectsClient, }); + const { basePath } = server; + const { anomalies } = (await getAnomalies(plugins, savedObjectsClient, params, state.lastCheckedAt as string)) ?? {}; @@ -128,8 +132,16 @@ export const durationAnomalyAlertFactory: UptimeAlertTypeFactory summary ); + const alertId = DURATION_ANOMALY.id + index; + const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString(); + const relativeViewInAppUrl = getMonitorRouteFromMonitorId({ + monitorId: DURATION_ANOMALY.id + index, + dateRangeEnd: 'now', + dateRangeStart: indexedStartedAt, + }); + const alertInstance = alertWithLifecycle({ - id: DURATION_ANOMALY.id + index, + id: alertId, fields: { 'monitor.id': params.monitorId, 'url.full': summary.monitorUrl, @@ -147,6 +159,7 @@ export const durationAnomalyAlertFactory: UptimeAlertTypeFactory }); alertInstance.scheduleActions(DURATION_ANOMALY.id, { [ALERT_REASON_MSG]: alertReasonMessage, + [VIEW_IN_APP_URL]: getViewInAppUrl(relativeViewInAppUrl, basePath), }); }); } diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts index d2e4a8dbc044ec..84e7c0d68400c6 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts @@ -243,6 +243,7 @@ describe('status check alert', () => { "xpack.uptime.alerts.actionGroups.monitorStatus", Object { "reason": "First from harrisburg failed 234 times in the last 15 mins. Alert when > 5.", + "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zmlyc3Q=?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22harrisburg%22%5D%5D%5D", }, ] `); @@ -313,6 +314,7 @@ describe('status check alert', () => { "xpack.uptime.alerts.actionGroups.monitorStatus", Object { "reason": "First from harrisburg failed 234 times in the last 15m. Alert when > 5.", + "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zmlyc3Q=?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22harrisburg%22%5D%5D%5D", }, ] `); @@ -784,24 +786,28 @@ describe('status check alert', () => { "xpack.uptime.alerts.actionGroups.monitorStatus", Object { "reason": "Foo from harrisburg 35 days availability is 99.28%. Alert when < 99.34%.", + "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zm9v?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22harrisburg%22%5D%5D%5D", }, ], Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", Object { "reason": "Foo from fairbanks 35 days availability is 98.03%. Alert when < 99.34%.", + "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zm9v?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22fairbanks%22%5D%5D%5D", }, ], Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", Object { "reason": "Unreliable from fairbanks 35 days availability is 90.92%. Alert when < 99.34%.", + "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/dW5yZWxpYWJsZQ==?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22fairbanks%22%5D%5D%5D", }, ], Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", Object { "reason": "no-name from fairbanks 35 days availability is 90.92%. Alert when < 99.34%.", + "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/bm8tbmFtZQ==?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22fairbanks%22%5D%5D%5D", }, ], ] diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index fe93928cb7e02e..6d9a0d23d9d329 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -5,6 +5,7 @@ * 2.0. */ import { min } from 'lodash'; + import datemath from '@elastic/datemath'; import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; @@ -18,7 +19,7 @@ import { GetMonitorAvailabilityParams, } from '../../../common/runtime_types'; import { MONITOR_STATUS } from '../../../common/constants/alerts'; -import { updateState } from './common'; +import { updateState, getViewInAppUrl } from './common'; import { commonMonitorStateI18, commonStateTranslations, @@ -36,7 +37,14 @@ import { getUptimeIndexPattern, IndexPatternTitleAndFields } from '../requests/g import { UMServerLibs, UptimeESClient, createUptimeESClient } from '../lib'; import { ActionGroupIdsOf } from '../../../../alerting/common'; import { formatDurationFromTimeUnitChar, TimeUnitChar } from '../../../../observability/common'; -import { ALERT_REASON_MSG, MESSAGE, MONITOR_WITH_GEO, ACTION_VARIABLES } from './action_variables'; +import { + ALERT_REASON_MSG, + MESSAGE, + MONITOR_WITH_GEO, + ACTION_VARIABLES, + VIEW_IN_APP_URL, +} from './action_variables'; +import { getMonitorRouteFromMonitorId } from '../../../common/utils/get_monitor_url'; export type ActionGroupIds = ActionGroupIdsOf; /** @@ -214,7 +222,7 @@ export const getInstanceId = (monitorInfo: Ping, monIdByLoc: string) => { return `${urlText}_${monIdByLoc}`; }; -export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) => ({ +export const statusCheckAlertFactory: UptimeAlertTypeFactory = (server, libs) => ({ id: 'xpack.uptime.alerts.monitorStatus', producer: 'uptime', name: i18n.translate('xpack.uptime.alerts.monitorStatus', { @@ -272,6 +280,7 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( ACTION_VARIABLES[MESSAGE], ACTION_VARIABLES[MONITOR_WITH_GEO], ACTION_VARIABLES[ALERT_REASON_MSG], + ACTION_VARIABLES[VIEW_IN_APP_URL], ], state: [...commonMonitorStateI18, ...commonStateTranslations], }, @@ -280,10 +289,11 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( async executor({ params: rawParams, state, - services: { savedObjectsClient, scopedClusterClient, alertWithLifecycle }, + services: { savedObjectsClient, scopedClusterClient, alertWithLifecycle, getAlertStartedDate }, rule: { schedule: { interval }, }, + startedAt, }) { const { filters, @@ -297,7 +307,7 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( isAutoGenerated, timerange: oldVersionTimeRange, } = rawParams; - + const { basePath } = server; const uptimeEsClient = createUptimeESClient({ esClient: scopedClusterClient.asCurrentUser, savedObjectsClient, @@ -336,7 +346,6 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( if (isAutoGenerated) { for (const monitorLoc of downMonitorsByLocation) { const monitorInfo = monitorLoc.monitorInfo; - const monitorStatusMessageParams = getMonitorDownStatusMessageParams( monitorInfo, monitorLoc.count, @@ -348,8 +357,10 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( const statusMessage = getStatusMessage(monitorStatusMessageParams); const monitorSummary = getMonitorSummary(monitorInfo, statusMessage); + const alertId = getInstanceId(monitorInfo, monitorLoc.location); + const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString(); const alert = alertWithLifecycle({ - id: getInstanceId(monitorInfo, monitorLoc.location), + id: alertId, fields: getMonitorAlertDocument(monitorSummary), }); @@ -360,8 +371,18 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( ...updateState(state, true), }); + const relativeViewInAppUrl = getMonitorRouteFromMonitorId({ + monitorId: monitorSummary.monitorId, + dateRangeEnd: 'now', + dateRangeStart: indexedStartedAt, + filters: { + 'observer.geo.name': [monitorSummary.observerLocation], + }, + }); + alert.scheduleActions(MONITOR_STATUS.id, { [ALERT_REASON_MSG]: monitorSummary.reason, + [VIEW_IN_APP_URL]: getViewInAppUrl(relativeViewInAppUrl, basePath), }); } return updateState(state, downMonitorsByLocation.length > 0); @@ -408,8 +429,10 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( availability ); const monitorSummary = getMonitorSummary(monitorInfo, statusMessage); + const alertId = getInstanceId(monitorInfo, monIdByLoc); + const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString(); const alert = alertWithLifecycle({ - id: getInstanceId(monitorInfo, monIdByLoc), + id: alertId, fields: getMonitorAlertDocument(monitorSummary), }); @@ -418,12 +441,20 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( ...monitorSummary, statusMessage, }); + const relativeViewInAppUrl = getMonitorRouteFromMonitorId({ + monitorId: monitorSummary.monitorId, + dateRangeEnd: 'now', + dateRangeStart: indexedStartedAt, + filters: { + 'observer.geo.name': [monitorSummary.observerLocation], + }, + }); alert.scheduleActions(MONITOR_STATUS.id, { [ALERT_REASON_MSG]: monitorSummary.reason, + [VIEW_IN_APP_URL]: getViewInAppUrl(relativeViewInAppUrl, basePath), }); }); - return updateState(state, downMonitorsByLocation.length > 0); }, }); diff --git a/x-pack/plugins/uptime/server/lib/alerts/test_utils/index.ts b/x-pack/plugins/uptime/server/lib/alerts/test_utils/index.ts index 826259cfa1405c..374719172405fe 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/test_utils/index.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/test_utils/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Logger } from 'kibana/server'; +import { IBasePath, Logger } from 'kibana/server'; import { UMServerLibs } from '../../lib'; import { UptimeCorePluginsSetup, UptimeServerSetup } from '../../adapters'; import type { UptimeRouter } from '../../../types'; @@ -25,9 +25,16 @@ import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; */ export const bootstrapDependencies = (customRequests?: any, customPlugins: any = {}) => { const router = {} as UptimeRouter; + const basePath = { + prepend: (url: string) => { + return `/hfe${url}`; + }, + publicBaseUrl: 'http://localhost:5601/hfe', + serverBasePath: '/hfe', + } as IBasePath; // these server/libs parameters don't have any functionality, which is fine // because we aren't testing them here - const server = { router, config: {} } as UptimeServerSetup; + const server = { router, config: {}, basePath } as UptimeServerSetup; const plugins: UptimeCorePluginsSetup = customPlugins as any; const libs: UMServerLibs = { requests: {} } as UMServerLibs; libs.requests = { ...libs.requests, ...customRequests }; @@ -56,6 +63,7 @@ export const createRuleTypeMocks = ( ...getUptimeESMockClient(), ...alertsMock.createAlertServices(), alertWithLifecycle: jest.fn().mockReturnValue({ scheduleActions, replaceState }), + getAlertStartedDate: jest.fn().mockReturnValue('2022-03-17T13:13:33.755Z'), logger: loggerMock, }; diff --git a/x-pack/plugins/uptime/server/lib/alerts/types.ts b/x-pack/plugins/uptime/server/lib/alerts/types.ts index e8e496cba997e8..5275cddae9d248 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/types.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/types.ts @@ -26,6 +26,7 @@ export type DefaultUptimeAlertInstance = AlertTy AlertInstanceContext, TActionGroupIds >; + getAlertStartedDate: (alertId: string) => string | null; } >; diff --git a/x-pack/plugins/uptime/server/plugin.ts b/x-pack/plugins/uptime/server/plugin.ts index d2afb3f16fb6a5..2f329aa83a5c4c 100644 --- a/x-pack/plugins/uptime/server/plugin.ts +++ b/x-pack/plugins/uptime/server/plugin.ts @@ -78,6 +78,7 @@ export class Plugin implements PluginType { router: core.http.createRouter(), cloud: plugins.cloud, kibanaVersion: this.initContext.env.packageInfo.version, + basePath: core.http.basePath, logger: this.logger, telemetry: this.telemetryEventsSender, } as UptimeServerSetup; From 36c614ab0b953aeec31bdbc7653e4298fce0ab7f Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 23 Mar 2022 13:16:42 +0100 Subject: [PATCH 35/64] Bump minimist from v1.2.5 to v1.2.6 (#128348) --- package.json | 6 +++--- packages/kbn-pm/dist/index.js | 8 ++++++-- yarn.lock | 16 ++++++++-------- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index a847db572fa4c6..24367fa77216ee 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "**/istanbul-lib-coverage": "^3.2.0", "**/json-schema": "^0.4.0", "**/minimatch": "^3.1.2", - "**/minimist": "^1.2.5", + "**/minimist": "^1.2.6", "**/node-forge": "^1.3.0", "**/pdfkit/crypto-js": "4.0.0", "**/react-syntax-highlighter": "^15.3.1", @@ -648,7 +648,7 @@ "@types/mime": "^2.0.1", "@types/mime-types": "^2.1.0", "@types/minimatch": "^2.0.29", - "@types/minimist": "^1.2.1", + "@types/minimist": "^1.2.2", "@types/mocha": "^9.1.0", "@types/mock-fs": "^4.13.1", "@types/moment-timezone": "^0.5.12", @@ -841,7 +841,7 @@ "lmdb-store": "^1.6.11", "marge": "^1.0.1", "micromatch": "3.1.10", - "minimist": "^1.2.5", + "minimist": "^1.2.6", "mkdirp": "0.5.1", "mocha": "^9.1.0", "mocha-junit-reporter": "^2.0.2", diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index d04285f60b5611..99ab81d2b539fe 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -16447,7 +16447,7 @@ module.exports = function (args, opts) { var o = obj; for (var i = 0; i < keys.length-1; i++) { var key = keys[i]; - if (key === '__proto__') return; + if (isConstructorOrProto(o, key)) return; if (o[key] === undefined) o[key] = {}; if (o[key] === Object.prototype || o[key] === Number.prototype || o[key] === String.prototype) o[key] = {}; @@ -16456,7 +16456,7 @@ module.exports = function (args, opts) { } var key = keys[keys.length - 1]; - if (key === '__proto__') return; + if (isConstructorOrProto(o, key)) return; if (o === Object.prototype || o === Number.prototype || o === String.prototype) o = {}; if (o === Array.prototype) o = []; @@ -16621,6 +16621,10 @@ function isNumber (x) { } +function isConstructorOrProto (obj, key) { + return key === 'constructor' && typeof obj[key] === 'function' || key === '__proto__'; +} + /***/ }), /* 229 */ diff --git a/yarn.lock b/yarn.lock index 396cda03b1235c..5163a6e68be50b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6284,10 +6284,10 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-2.0.29.tgz#5002e14f75e2d71e564281df0431c8c1b4a2a36a" integrity sha1-UALhT3Xi1x5WQoHfBDHIwbSio2o= -"@types/minimist@^1.2.0", "@types/minimist@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256" - integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg== +"@types/minimist@^1.2.0", "@types/minimist@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" + integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== "@types/minipass@*": version "2.2.0" @@ -20059,10 +20059,10 @@ minimist-options@4.1.0, minimist-options@^4.0.2: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@0.0.8, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@~1.2.0: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minimist@0.0.8, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6, minimist@~1.2.0: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== minipass-collect@^1.0.2: version "1.0.2" From 30b507545b70e762973f676b2c33032e351a94ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Wed, 23 Mar 2022 09:24:59 -0400 Subject: [PATCH 36/64] [APM] Service groups: Add EuiTour steps for assisting users in creating their first service group and provide guidance on the navigation changes (#128068) --- .../service_group_save/create_button.tsx | 47 +++++++++++ .../service_group_save/edit_button.tsx | 47 +++++++++++ .../service_group_save/save_button.tsx | 31 +++----- .../service_group_card.tsx | 37 ++++++++- .../service_groups_list.tsx | 1 + .../service_groups/service_groups_tour.tsx | 78 +++++++++++++++++++ .../use_service_groups_tour.tsx | 31 ++++++++ 7 files changed, 252 insertions(+), 20 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/service_groups/service_group_save/create_button.tsx create mode 100644 x-pack/plugins/apm/public/components/app/service_groups/service_group_save/edit_button.tsx create mode 100644 x-pack/plugins/apm/public/components/app/service_groups/service_groups_tour.tsx create mode 100644 x-pack/plugins/apm/public/components/app/service_groups/use_service_groups_tour.tsx diff --git a/x-pack/plugins/apm/public/components/app/service_groups/service_group_save/create_button.tsx b/x-pack/plugins/apm/public/components/app/service_groups/service_group_save/create_button.tsx new file mode 100644 index 00000000000000..e80fae85812710 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_groups/service_group_save/create_button.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { ServiceGroupsTour } from '../service_groups_tour'; +import { useServiceGroupsTour } from '../use_service_groups_tour'; + +interface Props { + onClick: () => void; +} + +export function CreateButton({ onClick }: Props) { + const { tourEnabled, dismissTour } = useServiceGroupsTour('createGroup'); + return ( + + { + dismissTour(); + onClick(); + }} + > + {i18n.translate('xpack.apm.serviceGroups.createGroupLabel', { + defaultMessage: 'Create group', + })} + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/service_groups/service_group_save/edit_button.tsx b/x-pack/plugins/apm/public/components/app/service_groups/service_group_save/edit_button.tsx new file mode 100644 index 00000000000000..8325ffd4019579 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_groups/service_group_save/edit_button.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { ServiceGroupsTour } from '../service_groups_tour'; +import { useServiceGroupsTour } from '../use_service_groups_tour'; + +interface Props { + onClick: () => void; +} + +export function EditButton({ onClick }: Props) { + const { tourEnabled, dismissTour } = useServiceGroupsTour('editGroup'); + return ( + + { + dismissTour(); + onClick(); + }} + > + {i18n.translate('xpack.apm.serviceGroups.editGroupLabel', { + defaultMessage: 'Edit group', + })} + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/service_groups/service_group_save/save_button.tsx b/x-pack/plugins/apm/public/components/app/service_groups/service_group_save/save_button.tsx index 61828e240c20ab..c0da29625d7ca3 100644 --- a/x-pack/plugins/apm/public/components/app/service_groups/service_group_save/save_button.tsx +++ b/x-pack/plugins/apm/public/components/app/service_groups/service_group_save/save_button.tsx @@ -4,11 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiButton } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import { useAnyOfApmParams } from '../../../../hooks/use_apm_params'; import { useFetcher } from '../../../../hooks/use_fetcher'; +import { CreateButton } from './create_button'; +import { EditButton } from './edit_button'; import { SaveGroupModal } from './save_modal'; export function ServiceGroupSaveButton() { @@ -32,16 +32,18 @@ export function ServiceGroupSaveButton() { ); const savedServiceGroup = data?.serviceGroup; + function onClick() { + setIsModalVisible((state) => !state); + } + return ( <> - { - setIsModalVisible((state) => !state); - }} - > - {isGroupEditMode ? EDIT_GROUP_LABEL : CREATE_GROUP_LABEL} - + {isGroupEditMode ? ( + + ) : ( + + )} + {isModalVisible && ( ); } - -const CREATE_GROUP_LABEL = i18n.translate( - 'xpack.apm.serviceGroups.createGroupLabel', - { defaultMessage: 'Create group' } -); -const EDIT_GROUP_LABEL = i18n.translate( - 'xpack.apm.serviceGroups.editGroupLabel', - { defaultMessage: 'Edit group' } -); diff --git a/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/service_group_card.tsx b/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/service_group_card.tsx index 0975bbb4ae3079..a73b849ee014ee 100644 --- a/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/service_group_card.tsx +++ b/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/service_group_card.tsx @@ -18,12 +18,15 @@ import { ServiceGroup, SERVICE_GROUP_COLOR_DEFAULT, } from '../../../../../common/service_groups'; +import { ServiceGroupsTour } from '../service_groups_tour'; +import { useServiceGroupsTour } from '../use_service_groups_tour'; interface Props { serviceGroup: ServiceGroup; hideServiceCount?: boolean; onClick?: () => void; href?: string; + withTour?: boolean; } export function ServiceGroupsCard({ @@ -31,7 +34,10 @@ export function ServiceGroupsCard({ hideServiceCount = false, onClick, href, + withTour, }: Props) { + const { tourEnabled, dismissTour } = useServiceGroupsTour('serviceGroupCard'); + const cardProps: EuiCardProps = { style: { width: 286, height: 186 }, icon: ( @@ -69,10 +75,39 @@ export function ServiceGroupsCard({ )}
), - onClick, + onClick: () => { + dismissTour(); + if (onClick) { + onClick(); + } + }, href, }; + if (withTour) { + return ( + + + + + + ); + } + return ( diff --git a/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/service_groups_list.tsx b/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/service_groups_list.tsx index 06c138f7f01cd0..224e9822a8b60d 100644 --- a/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/service_groups_list.tsx +++ b/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/service_groups_list.tsx @@ -38,6 +38,7 @@ export function ServiceGroupsListItems({ items }: Props) { /> ))} void; + children: React.ReactElement; +} + +export function ServiceGroupsTour({ + tourEnabled, + dismissTour, + title, + content, + children, +}: Props) { + return ( + + {content} + + + {i18n.translate( + 'xpack.apm.serviceGroups.tour.content.link.docs', + { + defaultMessage: 'docs', + } + )} + + ), + }} + /> + + } + isStepOpen={tourEnabled} + onFinish={() => {}} + maxWidth={300} + minWidth={300} + step={1} + stepsTotal={1} + title={title} + anchorPosition="leftUp" + footerAction={ + + {i18n.translate('xpack.apm.serviceGroups.tour.dismiss', { + defaultMessage: 'Dismiss', + })} + + } + > + {children} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/service_groups/use_service_groups_tour.tsx b/x-pack/plugins/apm/public/components/app/service_groups/use_service_groups_tour.tsx new file mode 100644 index 00000000000000..ba27b0e2640e8a --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_groups/use_service_groups_tour.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useLocalStorage } from '../../../hooks/use_local_storage'; +import { TourType } from './service_groups_tour'; + +const INITIAL_STATE: Record = { + createGroup: true, + editGroup: true, + serviceGroupCard: true, +}; + +export function useServiceGroupsTour(type: TourType) { + const [tourEnabled, setTourEnabled] = useLocalStorage( + 'apm.serviceGroupsTour', + INITIAL_STATE + ); + + return { + tourEnabled: tourEnabled[type], + dismissTour: () => + setTourEnabled({ + ...tourEnabled, + [type]: false, + }), + }; +} From 6be9d1e79793f7d2a5995ef29689af382a872bd2 Mon Sep 17 00:00:00 2001 From: Dmitrii Shevchenko Date: Wed, 23 Mar 2022 14:36:13 +0100 Subject: [PATCH 37/64] [Security Solution][Detections] Cleanup usages of old bulk rule CRUD endpoints (#126068) * Cleanup usages of old bulk CRUD endpoints * Apply suggestions from code review Co-authored-by: Garrett Spong Co-authored-by: Garrett Spong --- .../detection_alerts/acknowledged.spec.ts | 2 +- .../detection_rules/export_rule.spec.ts | 12 +- .../security_solution/cypress/tasks/common.ts | 22 +- .../cypress/tasks/rule_details.ts | 5 +- .../cypress/tasks/rules_bulk_edit.ts | 4 +- .../common/hooks/use_app_toasts.mock.ts | 14 +- .../rule_actions_overflow/index.test.tsx | 58 ++-- .../rules/rule_actions_overflow/index.tsx | 61 ++-- .../rules/rule_switch/index.test.tsx | 40 +-- .../components/rules/rule_switch/index.tsx | 31 +- .../detection_engine/rules/api.test.ts | 83 ----- .../containers/detection_engine/rules/api.ts | 58 ---- .../detection_engine/rules/types.ts | 13 - .../detection_engine/rules/all/actions.ts | 307 ++++++++--------- .../bulk_actions/bulk_edit_confirmation.tsx | 2 +- .../all/bulk_actions/use_bulk_actions.tsx | 148 +++----- .../rules/all/helpers.test.ts | 45 +-- .../detection_engine/rules/all/helpers.ts | 26 +- .../rules/all/rules_table_actions.test.tsx | 51 +-- .../rules/all/rules_table_actions.tsx | 51 +-- .../rules/all/use_columns.tsx | 8 +- .../detection_engine/rules/translations.ts | 319 ++++++++++++------ .../routes/rules/perform_bulk_action_route.ts | 6 + .../translations/translations/fr-FR.json | 10 +- .../translations/translations/ja-JP.json | 17 +- .../translations/translations/zh-CN.json | 17 +- 26 files changed, 615 insertions(+), 795 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/acknowledged.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/acknowledged.spec.ts index 12f5587ce0d6cf..a65abae52ae7e2 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/acknowledged.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/acknowledged.spec.ts @@ -72,7 +72,7 @@ describe('Marking alerts as acknowledged with read only role', () => { loginAndWaitForPage(ALERTS_URL, ROLES.t2_analyst); createCustomRuleEnabled(getNewRule()); refreshPage(); - waitForAlertsToPopulate(100); + waitForAlertsToPopulate(500); }); it('Mark one alert as acknowledged when more than one open alerts are selected', () => { diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/export_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/export_rule.spec.ts index 7b84845d46323a..8a6527c502b423 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/export_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/export_rule.spec.ts @@ -7,7 +7,7 @@ import { expectedExportedRule, getNewRule } from '../../objects/rule'; -import { TOASTER } from '../../screens/alerts_detection_rules'; +import { TOASTER_BODY } from '../../screens/alerts_detection_rules'; import { exportFirstRule } from '../../tasks/alerts_detection_rules'; import { createCustomRule } from '../../tasks/api_calls/rules'; @@ -19,19 +19,17 @@ import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; describe('Export rules', () => { beforeEach(() => { cleanKibana(); - cy.intercept( - 'POST', - '/api/detection_engine/rules/_export?exclude_export_details=false&file_name=rules_export.ndjson' - ).as('export'); + // Rules get exported via _bulk_action endpoint + cy.intercept('POST', '/api/detection_engine/rules/_bulk_action').as('bulk_action'); loginAndWaitForPageWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); createCustomRule(getNewRule()).as('ruleResponse'); }); it('Exports a custom rule', function () { exportFirstRule(); - cy.wait('@export').then(({ response }) => { + cy.wait('@bulk_action').then(({ response }) => { cy.wrap(response?.body).should('eql', expectedExportedRule(this.ruleResponse)); - cy.get(TOASTER).should( + cy.get(TOASTER_BODY).should( 'have.text', 'Successfully exported 1 of 1 rule. Prebuilt rules were excluded from the resulting file.' ); diff --git a/x-pack/plugins/security_solution/cypress/tasks/common.ts b/x-pack/plugins/security_solution/cypress/tasks/common.ts index 65480e52dea400..bafe429180fd1f 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/common.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/common.ts @@ -6,7 +6,6 @@ */ import { esArchiverResetKibana } from './es_archiver'; -import { RuleEcs } from '../../common/ecs/rule'; import { LOADING_INDICATOR } from '../screens/security_header'; const primaryButton = 0; @@ -68,19 +67,14 @@ export const reload = () => { export const cleanKibana = () => { const kibanaIndexUrl = `${Cypress.env('ELASTICSEARCH_URL')}/.kibana_\*`; - cy.request('GET', '/api/detection_engine/rules/_find').then((response) => { - const rules: RuleEcs[] = response.body.data; - - if (response.body.data.length > 0) { - rules.forEach((rule) => { - const jsonRule = rule; - cy.request({ - method: 'DELETE', - url: `/api/detection_engine/rules?rule_id=${jsonRule.rule_id}`, - headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, - }); - }); - } + cy.request({ + method: 'POST', + url: '/api/detection_engine/rules/_bulk_action', + body: { + query: '', + action: 'delete', + }, + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, }); cy.request('POST', `${kibanaIndexUrl}/_delete_by_query?conflicts=proceed`, { diff --git a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts index d42ebcf9da68ed..35def6967485c4 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts @@ -32,10 +32,11 @@ import { import { addsFields, closeFieldsBrowser, filterFieldsBrowser } from './fields_browser'; export const enablesRule = () => { - cy.intercept('PATCH', '/api/detection_engine/rules/_bulk_update').as('bulk_update'); + // Rules get enabled via _bulk_action endpoint + cy.intercept('POST', '/api/detection_engine/rules/_bulk_action').as('bulk_action'); cy.get(RULE_SWITCH).should('be.visible'); cy.get(RULE_SWITCH).click(); - cy.wait('@bulk_update').then(({ response }) => { + cy.wait('@bulk_action').then(({ response }) => { cy.wrap(response?.statusCode).should('eql', 200); }); }; diff --git a/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts b/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts index b665762fbd0c57..387fe63cad9cb3 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts @@ -74,7 +74,7 @@ export const confirmBulkEditForm = () => cy.get(RULES_BULK_EDIT_FORM_CONFIRM_BTN export const waitForBulkEditActionToFinish = ({ rulesCount }: { rulesCount: number }) => { cy.get(BULK_ACTIONS_PROGRESS_BTN).should('be.disabled'); - cy.contains(TOASTER_BODY, `You’ve successfully updated ${rulesCount} rule`); + cy.contains(TOASTER_BODY, `You've successfully updated ${rulesCount} rule`); }; export const waitForElasticRulesBulkEditModal = (rulesCount: number) => { @@ -99,6 +99,6 @@ export const waitForMixedRulesBulkEditModal = ( cy.get(MODAL_CONFIRMATION_BODY).should( 'have.text', - `The update action will only be applied to ${customRulesCount} Custom rules you’ve selected.` + `The update action will only be applied to ${customRulesCount} Custom rules you've selected.` ); }; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts index 25c0f5411f25cf..c0bb52b20c534a 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts @@ -5,10 +5,22 @@ * 2.0. */ -const createAppToastsMock = () => ({ +import { UseAppToasts } from './use_app_toasts'; + +const createAppToastsMock = (): jest.Mocked => ({ addError: jest.fn(), addSuccess: jest.fn(), addWarning: jest.fn(), + api: { + get$: jest.fn(), + add: jest.fn(), + remove: jest.fn(), + addSuccess: jest.fn(), + addWarning: jest.fn(), + addDanger: jest.fn(), + addError: jest.fn(), + addInfo: jest.fn(), + }, }); export const useAppToastsMock = { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx index 6a62b05c2e3194..3037a3c82f946b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx @@ -9,13 +9,13 @@ import { shallow, mount } from 'enzyme'; import React from 'react'; import { - deleteRulesAction, - duplicateRulesAction, - editRuleAction, + goToRuleEditPage, + executeRulesBulkAction, } from '../../../pages/detection_engine/rules/all/actions'; import { RuleActionsOverflow } from './index'; import { mockRule } from '../../../pages/detection_engine/rules/all/__mocks__/mock'; +jest.mock('../../../../common/hooks/use_app_toasts'); jest.mock('../../../../common/lib/kibana', () => { const actual = jest.requireActual('../../../../common/lib/kibana'); return { @@ -29,25 +29,9 @@ jest.mock('../../../../common/lib/kibana', () => { }), }; }); +jest.mock('../../../pages/detection_engine/rules/all/actions'); -jest.mock('react-router-dom', () => ({ - useHistory: () => ({ - push: jest.fn(), - }), -})); - -jest.mock('../../../pages/detection_engine/rules/all/actions', () => { - const actual = jest.requireActual('../../../../common/lib/kibana'); - return { - ...actual, - exportRulesAction: jest.fn(), - deleteRulesAction: jest.fn(), - duplicateRulesAction: jest.fn(), - editRuleAction: jest.fn(), - }; -}); - -const duplicateRulesActionMock = duplicateRulesAction as jest.Mock; +const executeRulesBulkActionMock = executeRulesBulkAction as jest.Mock; const flushPromises = () => new Promise(setImmediate); describe('RuleActionsOverflow', () => { @@ -206,7 +190,9 @@ describe('RuleActionsOverflow', () => { wrapper.update(); wrapper.find('[data-test-subj="rules-details-duplicate-rule"] button').simulate('click'); wrapper.update(); - expect(duplicateRulesAction).toHaveBeenCalled(); + expect(executeRulesBulkAction).toHaveBeenCalledWith( + expect.objectContaining({ action: 'duplicate' }) + ); }); test('it calls duplicateRulesAction with the rule and rule.id when rules-details-duplicate-rule is clicked', () => { @@ -218,11 +204,8 @@ describe('RuleActionsOverflow', () => { wrapper.update(); wrapper.find('[data-test-subj="rules-details-duplicate-rule"] button').simulate('click'); wrapper.update(); - expect(duplicateRulesAction).toHaveBeenCalledWith( - [rule], - [rule.id], - expect.anything(), - expect.anything() + expect(executeRulesBulkAction).toHaveBeenCalledWith( + expect.objectContaining({ action: 'duplicate', search: { ids: ['id'] } }) ); }); }); @@ -230,7 +213,9 @@ describe('RuleActionsOverflow', () => { test('it calls editRuleAction after the rule is duplicated', async () => { const rule = mockRule('id'); const ruleDuplicate = mockRule('newRule'); - duplicateRulesActionMock.mockImplementation(() => Promise.resolve([ruleDuplicate])); + executeRulesBulkActionMock.mockImplementation(() => + Promise.resolve({ attributes: { results: { created: [ruleDuplicate] } } }) + ); const wrapper = mount( ); @@ -240,8 +225,10 @@ describe('RuleActionsOverflow', () => { wrapper.update(); await flushPromises(); - expect(duplicateRulesAction).toHaveBeenCalled(); - expect(editRuleAction).toHaveBeenCalledWith(ruleDuplicate.id, expect.anything()); + expect(executeRulesBulkAction).toHaveBeenCalledWith( + expect.objectContaining({ action: 'duplicate' }) + ); + expect(goToRuleEditPage).toHaveBeenCalledWith(ruleDuplicate.id, expect.anything()); }); describe('rules details export rule', () => { @@ -340,7 +327,9 @@ describe('RuleActionsOverflow', () => { wrapper.update(); wrapper.find('[data-test-subj="rules-details-delete-rule"] button').simulate('click'); wrapper.update(); - expect(deleteRulesAction).toHaveBeenCalled(); + expect(executeRulesBulkAction).toHaveBeenCalledWith( + expect.objectContaining({ action: 'delete' }) + ); }); test('it calls deleteRulesAction with the rule.id when rules-details-delete-rule is clicked', () => { @@ -352,11 +341,8 @@ describe('RuleActionsOverflow', () => { wrapper.update(); wrapper.find('[data-test-subj="rules-details-delete-rule"] button').simulate('click'); wrapper.update(); - expect(deleteRulesAction).toHaveBeenCalledWith( - [rule.id], - expect.anything(), - expect.anything(), - expect.anything() + expect(executeRulesBulkAction).toHaveBeenCalledWith( + expect.objectContaining({ action: 'delete', search: { ids: ['id'] } }) ); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx index c97ae9d7d77560..d45159c61ce495 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx @@ -12,32 +12,30 @@ import { EuiPopover, EuiToolTip, } from '@elastic/eui'; +import { noop } from 'lodash'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; - -import { noop } from 'lodash/fp'; -import { Rule } from '../../../containers/detection_engine/rules'; -import * as i18n from './translations'; -import * as i18nActions from '../../../pages/detection_engine/rules/translations'; -import { useStateToaster } from '../../../../common/components/toasters'; -import { - deleteRulesAction, - duplicateRulesAction, - editRuleAction, - exportRulesAction, -} from '../../../pages/detection_engine/rules/all/actions'; +import { APP_UI_ID, SecurityPageName } from '../../../../../common/constants'; +import { BulkAction } from '../../../../../common/detection_engine/schemas/common'; import { getRulesUrl } from '../../../../common/components/link_to/redirect_to_detection_engine'; -import { getToolTipContent } from '../../../../common/utils/privileges'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { useBoolState } from '../../../../common/hooks/use_bool_state'; import { useKibana } from '../../../../common/lib/kibana'; -import { APP_UI_ID, SecurityPageName } from '../../../../../common/constants'; +import { getToolTipContent } from '../../../../common/utils/privileges'; +import { Rule } from '../../../containers/detection_engine/rules'; +import { + executeRulesBulkAction, + goToRuleEditPage, +} from '../../../pages/detection_engine/rules/all/actions'; +import * as i18nActions from '../../../pages/detection_engine/rules/translations'; +import * as i18n from './translations'; const MyEuiButtonIcon = styled(EuiButtonIcon)` &.euiButtonIcon { svg { transform: rotate(90deg); } - border: 1px solid  ${({ theme }) => theme.euiColorPrimary}; + border: 1px solid ${({ theme }) => theme.euiColorPrimary}; width: 40px; height: 40px; } @@ -59,7 +57,7 @@ const RuleActionsOverflowComponent = ({ }: RuleActionsOverflowComponentProps) => { const [isPopoverOpen, , closePopover, togglePopover] = useBoolState(); const { navigateToApp } = useKibana().services.application; - const [, dispatchToaster] = useStateToaster(); + const toasts = useAppToasts(); const onRuleDeletedCallback = useCallback(() => { navigateToApp(APP_UI_ID, { @@ -79,14 +77,15 @@ const RuleActionsOverflowComponent = ({ data-test-subj="rules-details-duplicate-rule" onClick={async () => { closePopover(); - const createdRules = await duplicateRulesAction( - [rule], - [rule.id], - dispatchToaster, - noop - ); + const result = await executeRulesBulkAction({ + action: BulkAction.duplicate, + onSuccess: noop, + search: { ids: [rule.id] }, + toasts, + }); + const createdRules = result?.attributes.results.created; if (createdRules?.length) { - editRuleAction(createdRules[0].id, navigateToApp); + goToRuleEditPage(createdRules[0].id, navigateToApp); } }} > @@ -104,7 +103,12 @@ const RuleActionsOverflowComponent = ({ data-test-subj="rules-details-export-rule" onClick={async () => { closePopover(); - await exportRulesAction([rule.rule_id], dispatchToaster, noop); + await executeRulesBulkAction({ + action: BulkAction.export, + onSuccess: noop, + search: { ids: [rule.id] }, + toasts, + }); }} > {i18nActions.EXPORT_RULE} @@ -116,7 +120,12 @@ const RuleActionsOverflowComponent = ({ data-test-subj="rules-details-delete-rule" onClick={async () => { closePopover(); - await deleteRulesAction([rule.id], dispatchToaster, noop, onRuleDeletedCallback); + await executeRulesBulkAction({ + action: BulkAction.delete, + onSuccess: onRuleDeletedCallback, + search: { ids: [rule.id] }, + toasts, + }); }} > {i18nActions.DELETE_RULE} @@ -126,10 +135,10 @@ const RuleActionsOverflowComponent = ({ [ canDuplicateRuleWithActions, closePopover, - dispatchToaster, navigateToApp, onRuleDeletedCallback, rule, + toasts, userHasPermissions, ] ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx index 7c10fd63b463ac..ec643962d41a6d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx @@ -9,22 +9,30 @@ import { mount } from 'enzyme'; import React from 'react'; import { waitFor } from '@testing-library/react'; -import { enableRules } from '../../../containers/detection_engine/rules'; +import { performBulkAction } from '../../../containers/detection_engine/rules'; import { RuleSwitchComponent } from './index'; import { getRulesSchemaMock } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; -import { useStateToaster, displayErrorToast } from '../../../../common/components/toasters'; import { useRulesTableContextOptional } from '../../../pages/detection_engine/rules/all/rules_table/rules_table_context'; import { useRulesTableContextMock } from '../../../pages/detection_engine/rules/all/rules_table/__mocks__/rules_table_context'; import { TestProviders } from '../../../../common/mock'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; -jest.mock('../../../../common/components/toasters'); +jest.mock('../../../../common/hooks/use_app_toasts'); jest.mock('../../../containers/detection_engine/rules'); jest.mock('../../../pages/detection_engine/rules/all/rules_table/rules_table_context'); +const useAppToastsValueMock = useAppToastsMock.create(); + describe('RuleSwitch', () => { beforeEach(() => { - (useStateToaster as jest.Mock).mockImplementation(() => [[], jest.fn()]); - (enableRules as jest.Mock).mockResolvedValue([getRulesSchemaMock()]); + (useAppToasts as jest.Mock).mockReturnValue(useAppToastsValueMock); + (performBulkAction as jest.Mock).mockResolvedValue({ + attributes: { + summary: { created: 0, updated: 1, deleted: 0 }, + results: { updated: [getRulesSchemaMock()] }, + }, + }); (useRulesTableContextOptional as jest.Mock).mockReturnValue(null); }); @@ -65,25 +73,7 @@ describe('RuleSwitch', () => { test('it dispatches error toaster if "enableRules" call rejects', async () => { const mockError = new Error('uh oh'); - (enableRules as jest.Mock).mockRejectedValue(mockError); - - const wrapper = mount(, { - wrappingComponent: TestProviders, - }); - wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); - - await waitFor(() => { - wrapper.update(); - expect(displayErrorToast).toHaveBeenCalledTimes(1); - }); - }); - - test('it dispatches error toaster if "enableRules" call resolves with some errors', async () => { - (enableRules as jest.Mock).mockResolvedValue([ - getRulesSchemaMock(), - { error: { status_code: 400, message: 'error' } }, - { error: { status_code: 400, message: 'error' } }, - ]); + (performBulkAction as jest.Mock).mockRejectedValue(mockError); const wrapper = mount(, { wrappingComponent: TestProviders, @@ -92,7 +82,7 @@ describe('RuleSwitch', () => { await waitFor(() => { wrapper.update(); - expect(displayErrorToast).toHaveBeenCalledTimes(1); + expect(useAppToastsValueMock.addError).toHaveBeenCalledTimes(1); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx index 893a0d4d8de8b4..a5f80de7acbdc7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx @@ -12,11 +12,13 @@ import { EuiSwitch, EuiSwitchEvent, } from '@elastic/eui'; +import { noop } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; -import { useStateToaster } from '../../../../common/components/toasters'; +import { BulkAction } from '../../../../../common/detection_engine/schemas/common'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { useUpdateRulesCache } from '../../../containers/detection_engine/rules/use_find_rules_query'; -import { enableRulesAction } from '../../../pages/detection_engine/rules/all/actions'; +import { executeRulesBulkAction } from '../../../pages/detection_engine/rules/all/actions'; import { useRulesTableContextOptional } from '../../../pages/detection_engine/rules/all/rules_table/rules_table_context'; const StaticSwitch = styled(EuiSwitch)` @@ -47,26 +49,29 @@ export const RuleSwitchComponent = ({ onChange, }: RuleSwitchProps) => { const [myIsLoading, setMyIsLoading] = useState(false); - const [, dispatchToaster] = useStateToaster(); const rulesTableContext = useRulesTableContextOptional(); const updateRulesCache = useUpdateRulesCache(); + const toasts = useAppToasts(); const onRuleStateChange = useCallback( async (event: EuiSwitchEvent) => { setMyIsLoading(true); - const rules = await enableRulesAction( - [id], - event.target.checked, - dispatchToaster, - rulesTableContext?.actions.setLoadingRules - ); - if (rules?.[0]) { - updateRulesCache(rules); - onChange?.(rules[0].enabled); + const bulkActionResponse = await executeRulesBulkAction({ + setLoadingRules: rulesTableContext?.actions.setLoadingRules, + toasts, + onSuccess: rulesTableContext ? undefined : noop, + action: event.target.checked ? BulkAction.enable : BulkAction.disable, + search: { ids: [id] }, + visibleRuleIds: [], + }); + if (bulkActionResponse?.attributes.results.updated.length) { + // The rule was successfully updated + updateRulesCache(bulkActionResponse.attributes.results.updated); + onChange?.(bulkActionResponse.attributes.results.updated[0].enabled); } setMyIsLoading(false); }, - [dispatchToaster, id, onChange, rulesTableContext?.actions.setLoadingRules, updateRulesCache] + [id, onChange, rulesTableContext, toasts, updateRulesCache] ); const showLoader = useMemo((): boolean => { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts index 004d1c3b7693c1..ecfa98bfa30763 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts @@ -12,9 +12,6 @@ import { patchRule, fetchRules, fetchRuleById, - enableRules, - deleteRules, - duplicateRules, createPrepackagedRules, importRules, exportRules, @@ -395,86 +392,6 @@ describe('Detections Rules API', () => { }); }); - describe('enableRules', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(rulesMock); - }); - - test('check parameter url, body when enabling rules', async () => { - await enableRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'], enabled: true }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_update', { - body: '[{"id":"mySuperRuleId","enabled":true},{"id":"mySuperRuleId_II","enabled":true}]', - method: 'PATCH', - }); - }); - test('check parameter url, body when disabling rules', async () => { - await enableRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'], enabled: false }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_update', { - body: '[{"id":"mySuperRuleId","enabled":false},{"id":"mySuperRuleId_II","enabled":false}]', - method: 'PATCH', - }); - }); - test('happy path', async () => { - const ruleResp = await enableRules({ - ids: ['mySuperRuleId', 'mySuperRuleId_II'], - enabled: true, - }); - expect(ruleResp).toEqual(rulesMock); - }); - }); - - describe('deleteRules', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(rulesMock); - }); - - test('check parameter url, body when deleting rules', async () => { - await deleteRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'] }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_delete', { - body: '[{"id":"mySuperRuleId"},{"id":"mySuperRuleId_II"}]', - method: 'POST', - }); - }); - - test('happy path', async () => { - const ruleResp = await deleteRules({ - ids: ['mySuperRuleId', 'mySuperRuleId_II'], - }); - expect(ruleResp).toEqual(rulesMock); - }); - }); - - describe('duplicateRules', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(rulesMock); - }); - - test('check parameter url, body when duplicating rules', async () => { - await duplicateRules({ rules: rulesMock.data }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_create', { - body: '[{"actions":[],"author":[],"description":"Elastic Endpoint detected Credential Dumping. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.","enabled":false,"false_positives":[],"from":"now-660s","index":["endgame-*"],"interval":"10m","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":73,"risk_score_mapping":[],"name":"Credential Dumping - Detected - Elastic Endpoint [Duplicate]","query":"event.kind:alert and event.module:endgame and event.action:cred_theft_event and endgame.metadata.type:detection","filters":[],"references":[],"severity":"high","severity_mapping":[],"tags":["Elastic","Endpoint"],"to":"now","type":"query","threat":[],"throttle":null,"version":1},{"actions":[],"author":[],"description":"Elastic Endpoint detected an Adversary Behavior. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.","enabled":false,"false_positives":[],"from":"now-660s","index":["endgame-*"],"interval":"10m","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":47,"risk_score_mapping":[],"name":"Adversary Behavior - Detected - Elastic Endpoint [Duplicate]","query":"event.kind:alert and event.module:endgame and event.action:rules_engine_event","filters":[],"references":[],"severity":"medium","severity_mapping":[],"tags":["Elastic","Endpoint"],"to":"now","type":"query","threat":[],"throttle":null,"version":1}]', - method: 'POST', - }); - }); - - test('check duplicated rules are disabled by default', async () => { - await duplicateRules({ rules: rulesMock.data.map((rule) => ({ ...rule, enabled: true })) }); - expect(fetchMock).toHaveBeenCalledTimes(1); - const [path, options] = fetchMock.mock.calls[0]; - expect(path).toBe('/api/detection_engine/rules/_bulk_create'); - const rules = JSON.parse(options.body); - expect(rules).toMatchObject([{ enabled: false }, { enabled: false }]); - }); - - test('happy path', async () => { - const ruleResp = await duplicateRules({ rules: rulesMock.data }); - expect(ruleResp).toEqual(rulesMock); - }); - }); - describe('createPrepackagedRules', () => { beforeEach(() => { fetchMock.mockClear(); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index 427cf28ef8f2f7..5b29671d05bac5 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -30,9 +30,6 @@ import { import { UpdateRulesProps, CreateRulesProps, - DeleteRulesProps, - DuplicateRulesProps, - EnableRulesProps, FetchRulesProps, FetchRulesResponse, Rule, @@ -42,7 +39,6 @@ import { ExportDocumentsProps, ImportDataResponse, PrePackagedRulesStatusResponse, - BulkRuleResponse, PatchRuleProps, BulkActionProps, BulkActionResponseMap, @@ -197,60 +193,6 @@ export const pureFetchRuleById = async ({ signal, }); -/** - * Enables/Disables provided Rule ID's - * - * @param ids array of Rule ID's (not rule_id) to enable/disable - * @param enabled to enable or disable - * - * @throws An error if response is not OK - */ -export const enableRules = async ({ ids, enabled }: EnableRulesProps): Promise => - KibanaServices.get().http.fetch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`, { - method: 'PATCH', - body: JSON.stringify(ids.map((id) => ({ id, enabled }))), - }); - -/** - * Deletes provided Rule ID's - * - * @param ids array of Rule ID's (not rule_id) to delete - * - * @throws An error if response is not OK - */ -export const deleteRules = async ({ ids }: DeleteRulesProps): Promise => - KibanaServices.get().http.fetch(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, { - method: 'POST', - body: JSON.stringify(ids.map((id) => ({ id }))), - }); - -/** - * Duplicates provided Rules - * - * @param rules to duplicate - * - * @throws An error if response is not OK - */ -export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise => - KibanaServices.get().http.fetch(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`, { - method: 'POST', - body: JSON.stringify( - rules.map((rule) => ({ - ...rule, - name: `${rule.name} [${i18n.DUPLICATE}]`, - created_at: undefined, - created_by: undefined, - id: undefined, - rule_id: undefined, - updated_at: undefined, - updated_by: undefined, - enabled: false, - immutable: undefined, - execution_summary: undefined, - })) - ), - }); - /** * Perform bulk action with rules selected by a filter query * diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index 85df24ec0258e6..797f67e1fbae5a 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -219,19 +219,6 @@ export interface FetchRuleProps { signal: AbortSignal; } -export interface EnableRulesProps { - ids: string[]; - enabled: boolean; -} - -export interface DeleteRulesProps { - ids: string[]; -} - -export interface DuplicateRulesProps { - rules: Rule[]; -} - export interface BulkActionProps { action: Action; query?: string; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.ts index 8e98d24b172460..10c099e4bfcc81 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.ts @@ -5,40 +5,28 @@ * 2.0. */ -import { Dispatch } from 'react'; import type { NavigateToAppOptions } from '../../../../../../../../../src/core/public'; - -import type { UseAppToasts } from '../../../../../common/hooks/use_app_toasts'; import { APP_UI_ID } from '../../../../../../common/constants'; import { BulkAction, BulkActionEditPayload, } from '../../../../../../common/detection_engine/schemas/common/schemas'; -import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request'; +import { HTTPError } from '../../../../../../common/detection_engine/types'; import { SecurityPageName } from '../../../../../app/types'; import { getEditRuleUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine'; -import { - ActionToaster, - displayErrorToast, - displaySuccessToast, - errorToToaster, -} from '../../../../../common/components/toasters'; +import type { UseAppToasts } from '../../../../../common/hooks/use_app_toasts'; import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../../../../common/lib/telemetry'; import { downloadBlob } from '../../../../../common/utils/download_blob'; import { - deleteRules, - duplicateRules, - enableRules, - exportRules, + BulkActionResponse, + BulkActionSummary, performBulkAction, - Rule, } from '../../../../containers/detection_engine/rules'; -import { RulesTableActions } from './rules_table/rules_table_context'; -import { transformOutput } from '../../../../containers/detection_engine/rules/transforms'; import * as i18n from '../translations'; -import { bucketRulesResponse, getExportedRulesCounts } from './helpers'; +import { getExportedRulesCounts } from './helpers'; +import { RulesTableActions } from './rules_table/rules_table_context'; -export const editRuleAction = ( +export const goToRuleEditPage = ( ruleId: string, navigateToApp: (appId: string, options?: NavigateToAppOptions | undefined) => Promise ) => { @@ -48,177 +36,48 @@ export const editRuleAction = ( }); }; -export const duplicateRulesAction = async ( - rules: Rule[], - ruleIds: string[], - dispatchToaster: Dispatch, - setLoadingRules: RulesTableActions['setLoadingRules'] -): Promise => { - try { - setLoadingRules({ ids: ruleIds, action: 'duplicate' }); - const response = await duplicateRules({ - // We cast this back and forth here as the front end types are not really the right io-ts ones - // and the two types conflict with each other. - rules: rules.map((rule) => transformOutput(rule as CreateRulesSchema) as Rule), - }); - const { errors, rules: createdRules } = bucketRulesResponse(response); - if (errors.length > 0) { - displayErrorToast( - i18n.DUPLICATE_RULE_ERROR, - errors.map((e) => e.error.message), - dispatchToaster - ); - } else { - displaySuccessToast(i18n.SUCCESSFULLY_DUPLICATED_RULES(ruleIds.length), dispatchToaster); - } - return createdRules; - } catch (error) { - errorToToaster({ title: i18n.DUPLICATE_RULE_ERROR, error, dispatchToaster }); - } finally { - setLoadingRules({ ids: [], action: null }); - } -}; - -export const exportRulesAction = async ( - exportRuleId: string[], - dispatchToaster: Dispatch, - setLoadingRules: RulesTableActions['setLoadingRules'] -) => { - try { - setLoadingRules({ ids: exportRuleId, action: 'export' }); - const blob = await exportRules({ ids: exportRuleId }); - downloadBlob(blob, `${i18n.EXPORT_FILENAME}.ndjson`); - - const { exported } = await getExportedRulesCounts(blob); - displaySuccessToast( - i18n.SUCCESSFULLY_EXPORTED_RULES(exported, exportRuleId.length), - dispatchToaster - ); - } catch (e) { - displayErrorToast(i18n.BULK_ACTION_FAILED, [e.message], dispatchToaster); - } finally { - setLoadingRules({ ids: [], action: null }); - } -}; - -export const deleteRulesAction = async ( - ruleIds: string[], - dispatchToaster: Dispatch, - setLoadingRules: RulesTableActions['setLoadingRules'], - onRuleDeleted?: () => void -) => { - try { - setLoadingRules({ ids: ruleIds, action: 'delete' }); - const response = await deleteRules({ ids: ruleIds }); - const { errors } = bucketRulesResponse(response); - if (errors.length > 0) { - displayErrorToast( - i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ruleIds.length), - errors.map((e) => e.error.message), - dispatchToaster - ); - } else if (onRuleDeleted) { - onRuleDeleted(); - } - } catch (error) { - errorToToaster({ - title: i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ruleIds.length), - error, - dispatchToaster, - }); - } finally { - setLoadingRules({ ids: [], action: null }); - } -}; - -export const enableRulesAction = async ( - ids: string[], - enabled: boolean, - dispatchToaster: Dispatch, - setLoadingRules?: RulesTableActions['setLoadingRules'] -) => { - const errorTitle = enabled - ? i18n.BATCH_ACTION_ENABLE_SELECTED_ERROR(ids.length) - : i18n.BATCH_ACTION_DISABLE_SELECTED_ERROR(ids.length); - - try { - setLoadingRules?.({ ids, action: enabled ? 'enable' : 'disable' }); - - const response = await enableRules({ ids, enabled }); - const { rules, errors } = bucketRulesResponse(response); - - if (errors.length > 0) { - displayErrorToast( - errorTitle, - errors.map((e) => e.error.message), - dispatchToaster - ); - } - - if (rules.some((rule) => rule.immutable)) { - track( - METRIC_TYPE.COUNT, - enabled ? TELEMETRY_EVENT.SIEM_RULE_ENABLED : TELEMETRY_EVENT.SIEM_RULE_DISABLED - ); - } - if (rules.some((rule) => !rule.immutable)) { - track( - METRIC_TYPE.COUNT, - enabled ? TELEMETRY_EVENT.CUSTOM_RULE_ENABLED : TELEMETRY_EVENT.CUSTOM_RULE_DISABLED - ); - } - - return rules; - } catch (e) { - displayErrorToast(errorTitle, [e.message], dispatchToaster); - } finally { - setLoadingRules?.({ ids: [], action: null }); - } -}; - interface ExecuteRulesBulkActionArgs { - visibleRuleIds: string[]; + visibleRuleIds?: string[]; action: BulkAction; toasts: UseAppToasts; search: { query: string } | { ids: string[] }; payload?: { edit?: BulkActionEditPayload[] }; - onSuccess?: (arg: { rulesCount: number }) => void; - onError?: (error: Error) => void; - setLoadingRules: RulesTableActions['setLoadingRules']; + onSuccess?: (toasts: UseAppToasts, action: BulkAction, summary: BulkActionSummary) => void; + onError?: (toasts: UseAppToasts, action: BulkAction, error: HTTPError) => void; + onFinish?: () => void; + setLoadingRules?: RulesTableActions['setLoadingRules']; } -const executeRulesBulkAction = async ({ - visibleRuleIds, +export const executeRulesBulkAction = async ({ + visibleRuleIds = [], action, setLoadingRules, toasts, search, payload, - onSuccess, - onError, + onSuccess = defaultSuccessHandler, + onError = defaultErrorHandler, + onFinish, }: ExecuteRulesBulkActionArgs) => { try { - setLoadingRules({ ids: visibleRuleIds, action }); + setLoadingRules?.({ ids: visibleRuleIds, action }); if (action === BulkAction.export) { - const blob = await performBulkAction({ ...search, action }); - downloadBlob(blob, `${i18n.EXPORT_FILENAME}.ndjson`); - const { exported, total } = await getExportedRulesCounts(blob); - - toasts.addSuccess(i18n.SUCCESSFULLY_EXPORTED_RULES(exported, total)); + const response = await performBulkAction({ ...search, action }); + downloadBlob(response, `${i18n.EXPORT_FILENAME}.ndjson`); + onSuccess(toasts, action, await getExportedRulesCounts(response)); } else { const response = await performBulkAction({ ...search, action, edit: payload?.edit }); + sendTelemetry(action, response); + onSuccess(toasts, action, response.attributes.summary); - onSuccess?.({ rulesCount: response.attributes.summary.succeeded }); - } - } catch (e) { - if (onError) { - onError(e); - } else { - toasts.addError(e, { title: i18n.BULK_ACTION_FAILED }); + return response; } + } catch (error) { + onError(toasts, action, error); } finally { - setLoadingRules({ ids: [], action: null }); + setLoadingRules?.({ ids: [], action: null }); + onFinish?.(); } }; @@ -240,3 +99,113 @@ export const initRulesBulkAction = (params: Omit rule.immutable)) { + track( + METRIC_TYPE.COUNT, + action === BulkAction.enable + ? TELEMETRY_EVENT.SIEM_RULE_ENABLED + : TELEMETRY_EVENT.SIEM_RULE_DISABLED + ); + } + if (response.attributes.results.updated.some((rule) => !rule.immutable)) { + track( + METRIC_TYPE.COUNT, + action === BulkAction.disable + ? TELEMETRY_EVENT.CUSTOM_RULE_ENABLED + : TELEMETRY_EVENT.CUSTOM_RULE_DISABLED + ); + } + } +} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_edit_confirmation.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_edit_confirmation.tsx index 445bd33860be2e..f4c8e7f9bf2a07 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_edit_confirmation.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_edit_confirmation.tsx @@ -64,7 +64,7 @@ const BulkEditConfirmationComponent = ({ > diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx index fd12f9a71bf296..491b693a442ba6 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx @@ -26,29 +26,18 @@ import { BulkActionEditPayload, } from '../../../../../../../common/detection_engine/schemas/common/schemas'; import { isMlRule } from '../../../../../../../common/machine_learning/helpers'; -import { displayWarningToast, useStateToaster } from '../../../../../../common/components/toasters'; import { canEditRuleWithActions } from '../../../../../../common/utils/privileges'; import { useRulesTableContext } from '../rules_table/rules_table_context'; import * as detectionI18n from '../../../translations'; import * as i18n from '../../translations'; -import { - deleteRulesAction, - duplicateRulesAction, - enableRulesAction, - exportRulesAction, - initRulesBulkAction, -} from '../actions'; +import { executeRulesBulkAction, initRulesBulkAction } from '../actions'; import { useHasActionsPrivileges } from '../use_has_actions_privileges'; import { useHasMlPermissions } from '../use_has_ml_permissions'; import { getCustomRulesCountFromCache } from './use_custom_rules_count'; import { useAppToasts } from '../../../../../../common/hooks/use_app_toasts'; import { convertRulesFilterToKQL } from '../../../../../containers/detection_engine/rules/utils'; -import type { - BulkActionResponse, - FilterOptions, -} from '../../../../../containers/detection_engine/rules/types'; -import type { HTTPError } from '../../../../../../../common/detection_engine/types'; +import type { FilterOptions } from '../../../../../containers/detection_engine/rules/types'; import { useInvalidateRules } from '../../../../../containers/detection_engine/rules/use_find_rules_query'; interface UseBulkActionsArgs { @@ -72,7 +61,6 @@ export const useBulkActions = ({ const hasMlPermissions = useHasMlPermissions(); const rulesTableContext = useRulesTableContext(); const invalidateRules = useInvalidateRules(); - const [, dispatchToaster] = useStateToaster(); const hasActionsPrivileges = useHasActionsPrivileges(); const toasts = useAppToasts(); const getIsMounted = useIsMounted(); @@ -117,65 +105,45 @@ export const useBulkActions = ({ const mlRuleCount = disabledRules.length - disabledRulesNoML.length; if (!hasMlPermissions && mlRuleCount > 0) { - displayWarningToast(detectionI18n.ML_RULES_UNAVAILABLE(mlRuleCount), dispatchToaster); + toasts.addWarning(detectionI18n.ML_RULES_UNAVAILABLE(mlRuleCount)); } const ruleIds = hasMlPermissions ? disabledRules.map(({ id }) => id) : disabledRulesNoML.map(({ id }) => id); - if (isAllSelected) { - const rulesBulkAction = initRulesBulkAction({ - visibleRuleIds: ruleIds, - action: BulkAction.enable, - setLoadingRules, - toasts, - }); - - await rulesBulkAction.byQuery(filterQuery); - } else { - await enableRulesAction(ruleIds, true, dispatchToaster, setLoadingRules); - } + await executeRulesBulkAction({ + visibleRuleIds: ruleIds, + action: BulkAction.enable, + setLoadingRules, + toasts, + search: isAllSelected ? { query: filterQuery } : { ids: ruleIds }, + }); invalidateRules(); }; const handleDisableActions = async () => { closePopover(); const enabledIds = selectedRules.filter(({ enabled }) => enabled).map(({ id }) => id); - if (isAllSelected) { - const rulesBulkAction = initRulesBulkAction({ - visibleRuleIds: enabledIds, - action: BulkAction.disable, - setLoadingRules, - toasts, - }); - - await rulesBulkAction.byQuery(filterQuery); - } else { - await enableRulesAction(enabledIds, false, dispatchToaster, setLoadingRules); - } + await executeRulesBulkAction({ + visibleRuleIds: enabledIds, + action: BulkAction.disable, + setLoadingRules, + toasts, + search: isAllSelected ? { query: filterQuery } : { ids: enabledIds }, + }); invalidateRules(); }; const handleDuplicateAction = async () => { closePopover(); - if (isAllSelected) { - const rulesBulkAction = initRulesBulkAction({ - visibleRuleIds: selectedRuleIds, - action: BulkAction.duplicate, - setLoadingRules, - toasts, - }); - - await rulesBulkAction.byQuery(filterQuery); - } else { - await duplicateRulesAction( - selectedRules, - selectedRuleIds, - dispatchToaster, - setLoadingRules - ); - } + await executeRulesBulkAction({ + visibleRuleIds: selectedRuleIds, + action: BulkAction.duplicate, + setLoadingRules, + toasts, + search: isAllSelected ? { query: filterQuery } : { ids: selectedRuleIds }, + }); invalidateRules(); }; @@ -186,39 +154,28 @@ export const useBulkActions = ({ // User has cancelled deletion return; } - - const rulesBulkAction = initRulesBulkAction({ - visibleRuleIds: selectedRuleIds, - action: BulkAction.delete, - setLoadingRules, - toasts, - }); - - await rulesBulkAction.byQuery(filterQuery); - } else { - await deleteRulesAction(selectedRuleIds, dispatchToaster, setLoadingRules); } + + await executeRulesBulkAction({ + visibleRuleIds: selectedRuleIds, + action: BulkAction.delete, + setLoadingRules, + toasts, + search: isAllSelected ? { query: filterQuery } : { ids: selectedRuleIds }, + }); invalidateRules(); }; const handleExportAction = async () => { closePopover(); - if (isAllSelected) { - const rulesBulkAction = initRulesBulkAction({ - visibleRuleIds: selectedRuleIds, - action: BulkAction.export, - setLoadingRules, - toasts, - }); - await rulesBulkAction.byQuery(filterQuery); - } else { - await exportRulesAction( - selectedRules.map((r) => r.rule_id), - dispatchToaster, - setLoadingRules - ); - } + await executeRulesBulkAction({ + visibleRuleIds: selectedRuleIds, + action: BulkAction.export, + setLoadingRules, + toasts, + search: isAllSelected ? { query: filterQuery } : { ids: selectedRuleIds }, + }); }; const handleBulkEdit = (bulkEditActionType: BulkActionEditType) => async () => { @@ -288,31 +245,7 @@ export const useBulkActions = ({ setLoadingRules, toasts, payload: { edit: [editPayload] }, - onSuccess: ({ rulesCount }) => { - hideWarningToast(); - toasts.addSuccess({ - title: i18n.BULK_EDIT_SUCCESS_TOAST_TITLE, - text: i18n.BULK_EDIT_SUCCESS_TOAST_DESCRIPTION(rulesCount), - iconType: undefined, - }); - }, - onError: (error: HTTPError) => { - hideWarningToast(); - // if response doesn't have number of failed rules, it means the whole bulk action failed - // and general error toast will be shown. Otherwise - error toast for partial failure - const failedRulesCount = (error?.body as BulkActionResponse)?.attributes?.summary - ?.failed; - - if (isNaN(failedRulesCount)) { - toasts.addError(error, { title: i18n.BULK_ACTION_FAILED }); - } else { - error.stack = JSON.stringify(error.body, null, 2); - toasts.addError(error, { - title: i18n.BULK_EDIT_ERROR_TOAST_TITLE, - toastMessage: i18n.BULK_EDIT_ERROR_TOAST_DESCRIPTION(failedRulesCount), - }); - } - }, + onFinish: () => hideWarningToast(), }); // only edit custom rules, as elastic rule are immutable @@ -477,7 +410,6 @@ export const useBulkActions = ({ loadingRuleIds, hasMlPermissions, invalidateRules, - dispatchToaster, setLoadingRules, toasts, filterQuery, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.test.ts index ebd059971b1401..30f9db5d8f7e59 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.test.ts @@ -5,54 +5,11 @@ * 2.0. */ -import { - bucketRulesResponse, - caseInsensitiveSort, - showRulesTable, - getSearchFilters, -} from './helpers'; -import { mockRule, mockRuleError } from './__mocks__/mock'; -import uuid from 'uuid'; -import { Rule, RuleError } from '../../../../containers/detection_engine/rules'; import { Query } from '@elastic/eui'; import { EXCEPTIONS_SEARCH_SCHEMA } from './exceptions/exceptions_search_bar'; +import { caseInsensitiveSort, getSearchFilters, showRulesTable } from './helpers'; describe('AllRulesTable Helpers', () => { - const mockRule1: Readonly = mockRule(uuid.v4()); - const mockRule2: Readonly = mockRule(uuid.v4()); - const mockRuleError1: Readonly = mockRuleError(uuid.v4()); - const mockRuleError2: Readonly = mockRuleError(uuid.v4()); - - describe('bucketRulesResponse', () => { - test('buckets empty response', () => { - const bucketedResponse = bucketRulesResponse([]); - expect(bucketedResponse).toEqual({ rules: [], errors: [] }); - }); - - test('buckets all error response', () => { - const bucketedResponse = bucketRulesResponse([mockRuleError1, mockRuleError2]); - expect(bucketedResponse).toEqual({ rules: [], errors: [mockRuleError1, mockRuleError2] }); - }); - - test('buckets all success response', () => { - const bucketedResponse = bucketRulesResponse([mockRule1, mockRule2]); - expect(bucketedResponse).toEqual({ rules: [mockRule1, mockRule2], errors: [] }); - }); - - test('buckets mixed success/error response', () => { - const bucketedResponse = bucketRulesResponse([ - mockRule1, - mockRuleError1, - mockRule2, - mockRuleError2, - ]); - expect(bucketedResponse).toEqual({ - rules: [mockRule1, mockRule2], - errors: [mockRuleError1, mockRuleError2], - }); - }); - }); - describe('showRulesTable', () => { test('returns false when rulesCustomInstalled and rulesInstalled are null', () => { const result = showRulesTable({ diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.ts index 7ed7d1bae60a6e..301e5cbe99b50f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/helpers.ts @@ -7,25 +7,7 @@ import { Query } from '@elastic/eui'; import { ExportRulesDetails } from '../../../../../../common/detection_engine/schemas/response/export_rules_details_schema'; -import { - BulkRuleResponse, - RuleResponseBuckets, -} from '../../../../containers/detection_engine/rules'; - -/** - * Separates rules/errors from bulk rules API response (create/update/delete) - * - * @param response BulkRuleResponse from bulk rules API - */ -export const bucketRulesResponse = (response: BulkRuleResponse) => - response.reduce( - (acc, cv): RuleResponseBuckets => { - return 'error' in cv - ? { rules: [...acc.rules], errors: [...acc.errors, cv] } - : { rules: [...acc.rules, cv], errors: [...acc.errors] }; - }, - { rules: [], errors: [] } - ); +import { BulkActionSummary } from '../../../../containers/detection_engine/rules'; export const showRulesTable = ({ rulesCustomInstalled, @@ -93,12 +75,12 @@ export const getExportedRulesDetails = async (blob: Blob): Promise { +export const getExportedRulesCounts = async (blob: Blob): Promise => { const details = await getExportedRulesDetails(blob); return { - exported: details.exported_rules_count, - missing: details.missing_rules_count, + succeeded: details.exported_rules_count, + failed: details.missing_rules_count, total: details.exported_rules_count + details.missing_rules_count, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.test.tsx index 44651172a6b267..d1e769f03bc9c8 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.test.tsx @@ -6,42 +6,38 @@ */ import uuid from 'uuid'; +import { useAppToastsMock } from '../../../../../common/hooks/use_app_toasts.mock'; import '../../../../../common/mock/match_media'; -import { deleteRulesAction, duplicateRulesAction, editRuleAction } from './actions'; +import { goToRuleEditPage, executeRulesBulkAction } from './actions'; import { getRulesTableActions } from './rules_table_actions'; import { mockRule } from './__mocks__/mock'; -jest.mock('./actions', () => ({ - duplicateRulesAction: jest.fn(), - deleteRulesAction: jest.fn(), - editRuleAction: jest.fn(), -})); +jest.mock('./actions'); -const duplicateRulesActionMock = duplicateRulesAction as jest.Mock; -const deleteRulesActionMock = deleteRulesAction as jest.Mock; -const editRuleActionMock = editRuleAction as jest.Mock; +const executeRulesBulkActionMock = executeRulesBulkAction as jest.Mock; +const goToRuleEditPageMock = goToRuleEditPage as jest.Mock; describe('getRulesTableActions', () => { const rule = mockRule(uuid.v4()); - const dispatchToaster = jest.fn(); - const reFetchRules = jest.fn(); + const toasts = useAppToastsMock.create(); + const invalidateRules = jest.fn(); const setLoadingRules = jest.fn(); beforeEach(() => { - duplicateRulesActionMock.mockClear(); - deleteRulesActionMock.mockClear(); - reFetchRules.mockClear(); + jest.clearAllMocks(); }); test('duplicate rule onClick should call rule edit after the rule is duplicated', async () => { const ruleDuplicate = mockRule('newRule'); const navigateToApp = jest.fn(); - duplicateRulesActionMock.mockImplementation(() => Promise.resolve([ruleDuplicate])); + executeRulesBulkActionMock.mockImplementation(() => + Promise.resolve({ attributes: { results: { created: [ruleDuplicate] } } }) + ); const duplicateRulesActionObject = getRulesTableActions( - dispatchToaster, + toasts, navigateToApp, - reFetchRules, + invalidateRules, true, setLoadingRules )[1]; @@ -49,17 +45,19 @@ describe('getRulesTableActions', () => { expect(duplicateRulesActionHandler).toBeDefined(); await duplicateRulesActionHandler!(rule); - expect(duplicateRulesActionMock).toHaveBeenCalled(); - expect(editRuleActionMock).toHaveBeenCalledWith(ruleDuplicate.id, navigateToApp); + expect(executeRulesBulkAction).toHaveBeenCalledWith( + expect.objectContaining({ action: 'duplicate' }) + ); + expect(goToRuleEditPageMock).toHaveBeenCalledWith(ruleDuplicate.id, navigateToApp); }); test('delete rule onClick should call refetch after the rule is deleted', async () => { const navigateToApp = jest.fn(); const deleteRulesActionObject = getRulesTableActions( - dispatchToaster, + toasts, navigateToApp, - reFetchRules, + invalidateRules, true, setLoadingRules )[3]; @@ -67,10 +65,13 @@ describe('getRulesTableActions', () => { expect(deleteRuleActionHandler).toBeDefined(); await deleteRuleActionHandler!(rule); - expect(deleteRulesActionMock).toHaveBeenCalledTimes(1); - expect(reFetchRules).toHaveBeenCalledTimes(1); - expect(deleteRulesActionMock.mock.invocationCallOrder[0]).toBeLessThan( - reFetchRules.mock.invocationCallOrder[0] + expect(executeRulesBulkAction).toHaveBeenCalledTimes(1); + expect(executeRulesBulkAction).toHaveBeenCalledWith( + expect.objectContaining({ action: 'delete' }) + ); + expect(invalidateRules).toHaveBeenCalledTimes(1); + expect(executeRulesBulkActionMock.mock.invocationCallOrder[0]).toBeLessThan( + invalidateRules.mock.invocationCallOrder[0] ); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.tsx index 3c960108fddf8d..9f0c0d0cb9695d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.tsx @@ -11,26 +11,22 @@ import { EuiTableActionsColumnType, EuiToolTip, } from '@elastic/eui'; -import React, { Dispatch } from 'react'; +import React from 'react'; import { NavigateToAppOptions } from '../../../../../../../../../src/core/public'; -import { ActionToaster } from '../../../../../common/components/toasters'; +import { BulkAction } from '../../../../../../common/detection_engine/schemas/common/schemas'; +import { UseAppToasts } from '../../../../../common/hooks/use_app_toasts'; import { canEditRuleWithActions } from '../../../../../common/utils/privileges'; import { Rule } from '../../../../containers/detection_engine/rules'; -import { RulesTableActions } from './rules_table/rules_table_context'; import * as i18n from '../translations'; -import { - deleteRulesAction, - duplicateRulesAction, - editRuleAction, - exportRulesAction, -} from './actions'; +import { executeRulesBulkAction, goToRuleEditPage } from './actions'; +import { RulesTableActions } from './rules_table/rules_table_context'; type NavigateToApp = (appId: string, options?: NavigateToAppOptions | undefined) => Promise; export type TableColumn = EuiBasicTableColumn | EuiTableActionsColumnType; export const getRulesTableActions = ( - dispatchToaster: Dispatch, + toasts: UseAppToasts, navigateToApp: NavigateToApp, invalidateRules: () => void, actionsPrivileges: boolean, @@ -48,7 +44,7 @@ export const getRulesTableActions = ( i18n.EDIT_RULE_SETTINGS ), icon: 'controlsHorizontal', - onClick: (rule: Rule) => editRuleAction(rule.id, navigateToApp), + onClick: (rule: Rule) => goToRuleEditPage(rule.id, navigateToApp), enabled: (rule: Rule) => canEditRuleWithActions(rule, actionsPrivileges), }, { @@ -65,15 +61,17 @@ export const getRulesTableActions = ( ), enabled: (rule: Rule) => canEditRuleWithActions(rule, actionsPrivileges), onClick: async (rule: Rule) => { - const createdRules = await duplicateRulesAction( - [rule], - [rule.id], - dispatchToaster, - setLoadingRules - ); + const result = await executeRulesBulkAction({ + action: BulkAction.duplicate, + setLoadingRules, + visibleRuleIds: [rule.id], + toasts, + search: { ids: [rule.id] }, + }); invalidateRules(); + const createdRules = result?.attributes.results.created; if (createdRules?.length) { - editRuleAction(createdRules[0].id, navigateToApp); + goToRuleEditPage(createdRules[0].id, navigateToApp); } }, }, @@ -83,7 +81,14 @@ export const getRulesTableActions = ( description: i18n.EXPORT_RULE, icon: 'exportAction', name: i18n.EXPORT_RULE, - onClick: (rule: Rule) => exportRulesAction([rule.rule_id], dispatchToaster, setLoadingRules), + onClick: (rule: Rule) => + executeRulesBulkAction({ + action: BulkAction.export, + setLoadingRules, + visibleRuleIds: [rule.id], + toasts, + search: { ids: [rule.id] }, + }), enabled: (rule: Rule) => !rule.immutable, }, { @@ -93,7 +98,13 @@ export const getRulesTableActions = ( icon: 'trash', name: i18n.DELETE_RULE, onClick: async (rule: Rule) => { - await deleteRulesAction([rule.id], dispatchToaster, setLoadingRules); + await executeRulesBulkAction({ + action: BulkAction.delete, + setLoadingRules, + visibleRuleIds: [rule.id], + toasts, + search: { ids: [rule.id] }, + }); invalidateRules(); }, }, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx index 37882030082384..5882cc9a72d9a4 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx @@ -27,7 +27,6 @@ import { LinkAnchor } from '../../../../../common/components/links'; import { useFormatUrl } from '../../../../../common/components/link_to'; import { getRuleDetailsUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine'; import { PopoverItems } from '../../../../../common/components/popover_items'; -import { useStateToaster } from '../../../../../common/components/toasters'; import { useKibana } from '../../../../../common/lib/kibana'; import { canEditRuleWithActions, getToolTipContent } from '../../../../../common/utils/privileges'; import { RuleSwitch } from '../../../../components/rules/rule_switch'; @@ -45,6 +44,7 @@ import { DurationMetric, RuleExecutionSummary, } from '../../../../../../common/detection_engine/schemas/common'; +import { useAppToasts } from '../../../../../common/hooks/use_app_toasts'; export type TableColumn = EuiBasicTableColumn | EuiTableActionsColumnType; @@ -160,13 +160,13 @@ const TAGS_COLUMN: TableColumn = { const useActionsColumn = (): EuiTableActionsColumnType => { const { navigateToApp } = useKibana().services.application; const hasActionsPrivileges = useHasActionsPrivileges(); - const [, dispatchToaster] = useStateToaster(); + const toasts = useAppToasts(); const { reFetchRules, setLoadingRules } = useRulesTableContext().actions; return useMemo( () => ({ actions: getRulesTableActions( - dispatchToaster, + toasts, navigateToApp, reFetchRules, hasActionsPrivileges, @@ -174,7 +174,7 @@ const useActionsColumn = (): EuiTableActionsColumnType => { ), width: '40px', }), - [dispatchToaster, hasActionsPrivileges, navigateToApp, reFetchRules, setLoadingRules] + [hasActionsPrivileges, navigateToApp, reFetchRules, setLoadingRules, toasts] ); }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index b1cc2e4f0388cc..3a9f233d9bffb2 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -244,23 +244,6 @@ export const BULK_ACTION_MENU_TITLE = i18n.translate( } ); -export const BULK_EDIT_SUCCESS_TOAST_TITLE = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditSuccessToastTitle', - { - defaultMessage: 'Rules changes updated', - } -); - -export const BULK_EDIT_SUCCESS_TOAST_DESCRIPTION = (rulesCount: number) => - i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditSuccessToastDescription', - { - values: { rulesCount }, - defaultMessage: - 'You’ve successfully updated {rulesCount, plural, =1 {# rule} other {# rules}}.', - } - ); - export const BULK_EDIT_WARNING_TOAST_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditWarningToastTitle', { @@ -284,22 +267,6 @@ export const BULK_EDIT_WARNING_TOAST_NOTIFY = i18n.translate( } ); -export const BULK_EDIT_ERROR_TOAST_TITLE = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditErrorToastTitle', - { - defaultMessage: 'Rule updates failed', - } -); - -export const BULK_EDIT_ERROR_TOAST_DESCRIPTION = (rulesCount: number) => - i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditErrorToastDescription', - { - values: { rulesCount }, - defaultMessage: '{rulesCount, plural, =1 {# rule is} other {# rules are}} failed to update.', - } - ); - export const BULK_EDIT_CONFIRMATION_TITLE = (elasticRulesCount: number) => i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditConfirmationTitle', @@ -454,24 +421,6 @@ export const BULK_EDIT_FLYOUT_FORM_DELETE_TAGS_TITLE = i18n.translate( } ); -export const BATCH_ACTION_ENABLE_SELECTED_ERROR = (totalRules: number) => - i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.enableSelectedErrorTitle', - { - values: { totalRules }, - defaultMessage: 'Error enabling {totalRules, plural, =1 {rule} other {rules}}', - } - ); - -export const BATCH_ACTION_DISABLE_SELECTED_ERROR = (totalRules: number) => - i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.disableSelectedErrorTitle', - { - values: { totalRules }, - defaultMessage: 'Error disabling {totalRules, plural, =1 {rule} other {rules}}', - } - ); - export const BATCH_ACTION_DELETE_SELECTED_IMMUTABLE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedImmutableTitle', { @@ -479,15 +428,6 @@ export const BATCH_ACTION_DELETE_SELECTED_IMMUTABLE = i18n.translate( } ); -export const BATCH_ACTION_DELETE_SELECTED_ERROR = (totalRules: number) => - i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedErrorTitle', - { - values: { totalRules }, - defaultMessage: 'Error deleting {totalRules, plural, =1 {rule} other {rules}}', - } - ); - export const EXPORT_FILENAME = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.exportFilenameTitle', { @@ -495,16 +435,6 @@ export const EXPORT_FILENAME = i18n.translate( } ); -export const SUCCESSFULLY_EXPORTED_RULES = (exportedRules: number, totalRules: number) => - i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.allRules.successfullyExportedXofYRulesTitle', - { - values: { totalRules, exportedRules }, - defaultMessage: - 'Successfully exported {exportedRules} of {totalRules} {totalRules, plural, =1 {rule} other {rules}}. Prebuilt rules were excluded from the resulting file.', - } - ); - export const ALL_RULES = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.tableTitle', { @@ -579,30 +509,6 @@ export const DUPLICATE_RULE = i18n.translate( } ); -export const SUCCESSFULLY_DUPLICATED_RULES = (totalRules: number) => - i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.allRules.successfullyDuplicatedRulesTitle', - { - values: { totalRules }, - defaultMessage: - 'Successfully duplicated {totalRules, plural, =1 {{totalRules} rule} other {{totalRules} rules}}', - } - ); - -export const DUPLICATE_RULE_ERROR = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleErrorDescription', - { - defaultMessage: 'Error duplicating rule', - } -); - -export const BULK_ACTION_FAILED = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.allRules.actions.bulkActionFailedDescription', - { - defaultMessage: 'Failed to execute bulk action', - } -); - export const EXPORT_RULE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.actions.exportRuleDescription', { @@ -611,7 +517,7 @@ export const EXPORT_RULE = i18n.translate( ); export const DELETE_RULE = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.allRules.actions.deleteeRuleDescription', + 'xpack.securitySolution.detectionEngine.rules.allRules.actions.deleteRuleDescription', { defaultMessage: 'Delete rule', } @@ -992,3 +898,226 @@ export const SHOWING_EXCEPTION_LISTS = (totalLists: number) => values: { totalLists }, defaultMessage: 'Showing {totalLists} {totalLists, plural, =1 {list} other {lists}}', }); + +/** + * Bulk Export + */ + +export const RULES_BULK_EXPORT_SUCCESS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.export.successToastTitle', + { + defaultMessage: 'Rules exported', + } +); + +export const RULES_BULK_EXPORT_SUCCESS_DESCRIPTION = (exportedRules: number, totalRules: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.export.successToastDescription', + { + values: { totalRules, exportedRules }, + defaultMessage: + 'Successfully exported {exportedRules} of {totalRules} {totalRules, plural, =1 {rule} other {rules}}. Prebuilt rules were excluded from the resulting file.', + } + ); + +export const RULES_BULK_EXPORT_FAILURE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.export.errorToastTitle', + { + defaultMessage: 'Error exporting rules', + } +); + +export const RULES_BULK_EXPORT_FAILURE_DESCRIPTION = (rulesCount: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.export.errorToastDescription', + { + values: { rulesCount }, + defaultMessage: '{rulesCount, plural, =1 {# rule is} other {# rules are}} failed to export.', + } + ); + +/** + * Bulk Duplicate + */ + +export const RULES_BULK_DUPLICATE_SUCCESS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.successToastTitle', + { + defaultMessage: 'Rules duplicated', + } +); + +export const RULES_BULK_DUPLICATE_SUCCESS_DESCRIPTION = (totalRules: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.successToastDescription', + { + values: { totalRules }, + defaultMessage: + 'Successfully duplicated {totalRules, plural, =1 {{totalRules} rule} other {{totalRules} rules}}', + } + ); + +export const RULES_BULK_DUPLICATE_FAILURE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.errorToastTitle', + { + defaultMessage: 'Error duplicating rule', + } +); + +export const RULES_BULK_DUPLICATE_FAILURE_DESCRIPTION = (rulesCount: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.errorToastDescription', + { + values: { rulesCount }, + defaultMessage: + '{rulesCount, plural, =1 {# rule is} other {# rules are}} failed to duplicate.', + } + ); + +/** + * Bulk Delete + */ + +export const RULES_BULK_DELETE_SUCCESS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.delete.successToastTitle', + { + defaultMessage: 'Rules deleted', + } +); + +export const RULES_BULK_DELETE_SUCCESS_DESCRIPTION = (totalRules: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.delete.successToastDescription', + { + values: { totalRules }, + defaultMessage: + 'Successfully deleted {totalRules, plural, =1 {{totalRules} rule} other {{totalRules} rules}}', + } + ); + +export const RULES_BULK_DELETE_FAILURE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.delete.errorToastTitle', + { + defaultMessage: 'Error deleting rules', + } +); + +export const RULES_BULK_DELETE_FAILURE_DESCRIPTION = (rulesCount: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.delete.errorToastDescription', + { + values: { rulesCount }, + defaultMessage: '{rulesCount, plural, =1 {# rule is} other {# rules are}} failed to delete.', + } + ); + +/** + * Bulk Enable + */ + +export const RULES_BULK_ENABLE_SUCCESS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.enable.successToastTitle', + { + defaultMessage: 'Rules enabled', + } +); + +export const RULES_BULK_ENABLE_SUCCESS_DESCRIPTION = (totalRules: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkAction.enable.successToastDescription', + { + values: { totalRules }, + defaultMessage: + 'Successfully enabled {totalRules, plural, =1 {{totalRules} rule} other {{totalRules} rules}}', + } + ); + +export const RULES_BULK_ENABLE_FAILURE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.enable.errorToastTitle', + { + defaultMessage: 'Error enabling rules', + } +); + +export const RULES_BULK_ENABLE_FAILURE_DESCRIPTION = (rulesCount: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.enable.errorToastDescription', + { + values: { rulesCount }, + defaultMessage: '{rulesCount, plural, =1 {# rule is} other {# rules are}} failed to enable.', + } + ); + +/** + * Bulk Disable + */ + +export const RULES_BULK_DISABLE_SUCCESS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.disable.successToastTitle', + { + defaultMessage: 'Rules disabled', + } +); + +export const RULES_BULK_DISABLE_SUCCESS_DESCRIPTION = (totalRules: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.disable.successToastDescription', + { + values: { totalRules }, + defaultMessage: + 'Successfully disabled {totalRules, plural, =1 {{totalRules} rule} other {{totalRules} rules}}', + } + ); + +export const RULES_BULK_DISABLE_FAILURE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.disable.errorToastTitle', + { + defaultMessage: 'Error disabling rules', + } +); + +export const RULES_BULK_DISABLE_FAILURE_DESCRIPTION = (rulesCount: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.disable.errorToastDescription', + { + values: { rulesCount }, + defaultMessage: '{rulesCount, plural, =1 {# rule is} other {# rules are}} failed to disable.', + } + ); + +/** + * Bulk Edit + */ + +export const RULES_BULK_EDIT_SUCCESS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.successToastTitle', + { + defaultMessage: 'Rules updated', + } +); + +export const RULES_BULK_EDIT_SUCCESS_DESCRIPTION = (rulesCount: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.successToastDescription', + { + values: { rulesCount }, + defaultMessage: + "You've successfully updated {rulesCount, plural, =1 {# rule} other {# rules}}.", + } + ); + +export const RULES_BULK_EDIT_FAILURE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.errorToastTitle', + { + defaultMessage: 'Error updating rules', + } +); + +export const RULES_BULK_EDIT_FAILURE_DESCRIPTION = (rulesCount: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.errorToastDescription', + { + values: { rulesCount }, + defaultMessage: '{rulesCount, plural, =1 {# rule} other {# rules}} failed to update.', + } + ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts index 1e1c894ad097cb..d8978cd8b11aae 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts @@ -95,13 +95,19 @@ const buildBulkResponse = ( total: bulkActionOutcome.results.length + errors.length, }; + // Whether rules will be updated, created or deleted depends on the bulk + // action type being processed. However, in order to avoid doing a switch-case + // by the action type, we can figure it out indirectly. const results = { + // We had a rule, now there's a rule with the same id - the existing rule was modified updated: bulkActionOutcome.results .filter(({ item, result }) => item.id === result?.id) .map(({ result }) => result && internalRuleToAPIResponse(result)), + // We had a rule, now there's a rule with a different id - a new rule was created created: bulkActionOutcome.results .filter(({ item, result }) => result != null && result.id !== item.id) .map(({ result }) => result && internalRuleToAPIResponse(result)), + // We had a rule, now it's null - the rule was deleted deleted: bulkActionOutcome.results .filter(({ result }) => result == null) .map(({ item }) => internalRuleToAPIResponse(item)), diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 4a9913fe97aba7..ac95693301e545 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -20812,16 +20812,14 @@ "xpack.securitySolution.detectionEngine.rules.allExceptionLists.search.placeholder": "Rechercher les listes d'exceptions", "xpack.securitySolution.detectionEngine.rules.allExceptions.filters.noListsBody": "Nous n'avons trouvé aucune liste d'exceptions.", "xpack.securitySolution.detectionEngine.rules.allExceptions.tableTitle": "Exceptions", - "xpack.securitySolution.detectionEngine.rules.allRules.actions.bulkActionFailedDescription": "Impossible d'exécuter l'action groupée", - "xpack.securitySolution.detectionEngine.rules.allRules.actions.deleteeRuleDescription": "Supprimer la règle", + "xpack.securitySolution.detectionEngine.rules.allRules.actions.deleteRuleDescription": "Supprimer la règle", "xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleDescription": "Dupliquer la règle", - "xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleErrorDescription": "Erreur lors de la duplication de la règle", + "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.errorToastTitle": "Erreur lors de la duplication de la règle", "xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateTitle": "Dupliquer", "xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsDescription": "Modifier les paramètres de règles", "xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsToolTip": "Vous ne disposez pas des privilèges d'actions Kibana", "xpack.securitySolution.detectionEngine.rules.allRules.actions.exportRuleDescription": "Exporter la règle", "xpack.securitySolution.detectionEngine.rules.allRules.activeRuleDescription": "active", - "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedErrorTitle": "Erreur lors de la suppression de {totalRules, plural, =1 {règle} other {règles}}", "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedImmutableTitle": "La sélection contient des règles immuables qui ne peuvent pas être supprimées", "xpack.securitySolution.detectionEngine.rules.allRules.batchActionsTitle": "Actions groupées", "xpack.securitySolution.detectionEngine.rules.allRules.clearSelectionTitle": "Effacer la sélection", @@ -20852,8 +20850,8 @@ "xpack.securitySolution.detectionEngine.rules.allRules.selectedRulesTitle": "Sélection de {selectedRules} {selectedRules, plural, =1 {règle} other {règles}} effectuée", "xpack.securitySolution.detectionEngine.rules.allRules.showingExceptionLists": "Affichage de {totalLists} {totalLists, plural, =1 {liste} other {listes}}", "xpack.securitySolution.detectionEngine.rules.allRules.showingRulesTitle": "Affichage de {totalRules} {totalRules, plural, =1 {règle} other {règles}}", - "xpack.securitySolution.detectionEngine.rules.allRules.successfullyDuplicatedRulesTitle": "Duplication réussie de {totalRules, plural, =1 {{totalRules} règle} other {{totalRules} règles}}", - "xpack.securitySolution.detectionEngine.rules.allRules.successfullyExportedXofYRulesTitle": "Exportation réussie de {exportedRules} sur {totalRules} {totalRules, plural, =1 {règle} other {règles}}. Les règles prédéfinies ont été exclues du fichier résultant.", + "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.successToastDescription": "Duplication réussie de {totalRules, plural, =1 {{totalRules} règle} other {{totalRules} règles}}", + "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.export.successToastDescription": "Exportation réussie de {exportedRules} sur {totalRules} {totalRules, plural, =1 {règle} other {règles}}. Les règles prédéfinies ont été exclues du fichier résultant.", "xpack.securitySolution.detectionEngine.rules.allRules.tableTitle": "Toutes les règles", "xpack.securitySolution.detectionEngine.rules.allRules.tabs.exceptions": "Listes d'exceptions", "xpack.securitySolution.detectionEngine.rules.allRules.tabs.monitoring": "Monitoring des règles", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1dea85f7f14999..2337f25502da36 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -23808,27 +23808,24 @@ "xpack.securitySolution.detectionEngine.rules.allExceptionLists.search.placeholder": "検索例外リスト", "xpack.securitySolution.detectionEngine.rules.allExceptions.filters.noListsBody": "例外リストが見つかりませんでした。", "xpack.securitySolution.detectionEngine.rules.allExceptions.tableTitle": "例外リスト", - "xpack.securitySolution.detectionEngine.rules.allRules.actions.bulkActionFailedDescription": "一括アクションを実行できませんでした", - "xpack.securitySolution.detectionEngine.rules.allRules.actions.deleteeRuleDescription": "ルールの削除", + "xpack.securitySolution.detectionEngine.rules.allRules.actions.deleteRuleDescription": "ルールの削除", "xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleDescription": "ルールの複製", - "xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleErrorDescription": "ルールの複製エラー", + "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.errorToastTitle": "ルールの複製エラー", "xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateTitle": "複製", "xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsDescription": "ルール設定の編集", "xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsToolTip": "Kibana アクション特権がありません", "xpack.securitySolution.detectionEngine.rules.allRules.actions.exportRuleDescription": "ルールのエクスポート", "xpack.securitySolution.detectionEngine.rules.allRules.activeRuleDescription": "アクティブ", - "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedErrorTitle": "{totalRules, plural, other {ルール}}の削除エラー", "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedImmutableTitle": "選択には削除できないイミュータブルルールがあります", "xpack.securitySolution.detectionEngine.rules.allRules.batchActionsTitle": "一斉アクション", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.addIndexPatternsTitle": "インデックスパターンを追加", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.addTagsTitle": "タグを追加", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditConfirmationDescription": "更新アクションは、選択した{customRulesCount, plural, other {#個のカスタムルール}}にのみ適用されます。", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditConfirmationTitle": "{elasticRulesCount, plural, other {#個のElasticルール}}を編集できません", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditErrorToastDescription": "{rulesCount, plural, other {#個のルール}}を更新できませんでした。", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditErrorToastTitle": "ルールの更新が失敗しました", + "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.errorToastDescription": "{rulesCount, plural, other {#個のルール}}を更新できませんでした。", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditRejectionDescription": "Elasticルールは変更できません。更新アクションはカスタムルールにのみ適用されます。", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditSuccessToastDescription": "{rulesCount, plural, other {#個のルール}}を正常に更新しました。", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditSuccessToastTitle": "ルール変更が更新されました", + "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.successToastDescription": "{rulesCount, plural, other {#個のルール}}を正常に更新しました。", + "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.successToastTitle": "ルール変更が更新されました", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditWarningToastDescription": "{rulesCount, plural, other {#個のルール}}を更新しています。", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditWarningToastNotifyButtonLabel": "完了時に通知", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditWarningToastTitle": "ルールを更新しています", @@ -23874,8 +23871,8 @@ "xpack.securitySolution.detectionEngine.rules.allRules.selectedRulesTitle": "{selectedRules} {selectedRules, plural, other {ルール}}を選択しました", "xpack.securitySolution.detectionEngine.rules.allRules.showingExceptionLists": "{totalLists} {totalLists, plural, other {件のリスト}}を表示しています。", "xpack.securitySolution.detectionEngine.rules.allRules.showingRulesTitle": "{totalRules} {totalRules, plural, other {ルール}}を表示中", - "xpack.securitySolution.detectionEngine.rules.allRules.successfullyDuplicatedRulesTitle": "{totalRules, plural, other {{totalRules}ルール}}を正常に複製しました", - "xpack.securitySolution.detectionEngine.rules.allRules.successfullyExportedXofYRulesTitle": "{exportedRules}/{totalRules} {totalRules, plural, other {件のルール}}を正常にエクスポートしました事前構築済みルールは結果のファイルから除外されました。", + "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.successToastDescription": "{totalRules, plural, other {{totalRules}ルール}}を正常に複製しました", + "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.export.successToastDescription": "{exportedRules}/{totalRules} {totalRules, plural, other {件のルール}}を正常にエクスポートしました事前構築済みルールは結果のファイルから除外されました。", "xpack.securitySolution.detectionEngine.rules.allRules.tableTitle": "すべてのルール", "xpack.securitySolution.detectionEngine.rules.allRules.tabs.exceptions": "例外リスト", "xpack.securitySolution.detectionEngine.rules.allRules.tabs.monitoring": "ルール監視", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fabf4d7a2d5902..158534694943a4 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23837,27 +23837,24 @@ "xpack.securitySolution.detectionEngine.rules.allExceptionLists.search.placeholder": "搜索例外列表", "xpack.securitySolution.detectionEngine.rules.allExceptions.filters.noListsBody": "我们找不到任何例外列表。", "xpack.securitySolution.detectionEngine.rules.allExceptions.tableTitle": "例外列表", - "xpack.securitySolution.detectionEngine.rules.allRules.actions.bulkActionFailedDescription": "无法执行批量操作", - "xpack.securitySolution.detectionEngine.rules.allRules.actions.deleteeRuleDescription": "删除规则", + "xpack.securitySolution.detectionEngine.rules.allRules.actions.deleteRuleDescription": "删除规则", "xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleDescription": "复制规则", - "xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleErrorDescription": "复制规则时出错", + "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.errorToastTitle": "复制规则时出错", "xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateTitle": "复制", "xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsDescription": "编辑规则设置", "xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsToolTip": "您没有 Kibana 操作权限", "xpack.securitySolution.detectionEngine.rules.allRules.actions.exportRuleDescription": "导出规则", "xpack.securitySolution.detectionEngine.rules.allRules.activeRuleDescription": "活动", - "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedErrorTitle": "删除{totalRules, plural, other {规则}}时出错", "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedImmutableTitle": "选择内容包含无法删除的不可变规则", "xpack.securitySolution.detectionEngine.rules.allRules.batchActionsTitle": "批处理操作", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.addIndexPatternsTitle": "添加索引模式", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.addTagsTitle": "添加标签", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditConfirmationDescription": "将仅对您选定的 {customRulesCount, plural, other {# 个定制规则}}应用更新操作。", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditConfirmationTitle": "无法编辑 {elasticRulesCount, plural, other {# 个 Elastic 规则}}", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditErrorToastDescription": "无法更新 {rulesCount, plural, other {# 个规则}}。", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditErrorToastTitle": "规则更新失败", + "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.errorToastDescription": "无法更新 {rulesCount, plural, other {# 个规则}}。", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditRejectionDescription": "无法修改 Elastic 规则。将仅对定制规则应用更新操作。", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditSuccessToastDescription": "您已成功更新 {rulesCount, plural, other {# 个规则}}。", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditSuccessToastTitle": "规则更改已更新", + "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.successToastDescription": "您已成功更新 {rulesCount, plural, other {# 个规则}}。", + "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.successToastTitle": "规则更改已更新", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditWarningToastDescription": "{rulesCount, plural, other {# 个规则}}正在更新。", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditWarningToastNotifyButtonLabel": "在完成时通知我", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditWarningToastTitle": "正在进行规则更新", @@ -23903,8 +23900,8 @@ "xpack.securitySolution.detectionEngine.rules.allRules.selectedRulesTitle": "已选择 {selectedRules} 个{selectedRules, plural, other {规则}}", "xpack.securitySolution.detectionEngine.rules.allRules.showingExceptionLists": "正在显示 {totalLists} 个{totalLists, plural, other {列表}}", "xpack.securitySolution.detectionEngine.rules.allRules.showingRulesTitle": "正在显示 {totalRules} 个{totalRules, plural, other {规则}}", - "xpack.securitySolution.detectionEngine.rules.allRules.successfullyDuplicatedRulesTitle": "已成功复制 {totalRules, plural, other {{totalRules} 个规则}}", - "xpack.securitySolution.detectionEngine.rules.allRules.successfullyExportedXofYRulesTitle": "已成功导出 {exportedRules}/{totalRules} 个{totalRules, plural, other {规则}}。预置规则已从结果文件中排除。", + "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.successToastDescription": "已成功复制 {totalRules, plural, other {{totalRules} 个规则}}", + "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.export.successToastDescription": "已成功导出 {exportedRules}/{totalRules} 个{totalRules, plural, other {规则}}。预置规则已从结果文件中排除。", "xpack.securitySolution.detectionEngine.rules.allRules.tableTitle": "所有规则", "xpack.securitySolution.detectionEngine.rules.allRules.tabs.exceptions": "例外列表", "xpack.securitySolution.detectionEngine.rules.allRules.tabs.monitoring": "规则监测", From 46a4da2cc27ddfabfd48eb354d40f6994ba43102 Mon Sep 17 00:00:00 2001 From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> Date: Wed, 23 Mar 2022 09:38:24 -0400 Subject: [PATCH 38/64] [Security Solution][Analyzer] Updates the selector used on the process button to match panel (#127401) --- .../public/resolver/view/process_event_dot.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 214f8eba0eec89..f6064fe54f6dbf 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -18,6 +18,8 @@ import { ResolverNode } from '../../../common/endpoint/types'; import { useResolverDispatch } from './use_resolver_dispatch'; import { SideEffectContext } from './side_effect_context'; import * as nodeModel from '../../../common/endpoint/models/node'; +import * as eventModel from '../../../common/endpoint/models/event'; +import * as nodeDataModel from '../models/node_data'; import * as selectors from '../store/selectors'; import { fontSize } from './font_size'; import { useCubeAssets } from './use_cube_assets'; @@ -327,8 +329,18 @@ const UnstyledProcessEventDot = React.memo( const grandTotal: number | null = useSelector((state: ResolverState) => selectors.statsTotalForNode(state)(node) ); - const nodeName = nodeModel.nodeName(node); + const processEvent = useSelector((state: ResolverState) => + nodeDataModel.firstEvent(selectors.nodeDataForID(state)(String(node.id))) + ); + const processName = useMemo(() => { + if (processEvent !== undefined) { + return eventModel.processNameSafeVersion(processEvent); + } else { + return nodeName; + } + }, [processEvent, nodeName]); + /* eslint-disable jsx-a11y/click-events-have-key-events */ return (
@@ -476,7 +488,7 @@ const UnstyledProcessEventDot = React.memo( defaultMessage: `{nodeState, select, error {Reload {nodeName}} other {{nodeName}}}`, values: { nodeState, - nodeName, + nodeName: processName, }, })} From cf538aaa28d93f87f183149493e96776496340c9 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 23 Mar 2022 13:40:33 +0000 Subject: [PATCH 39/64] skip flaky suite (#128332) --- x-pack/test/accessibility/apps/maps.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/accessibility/apps/maps.ts b/x-pack/test/accessibility/apps/maps.ts index 079972273c19bb..1eb4ad433c6610 100644 --- a/x-pack/test/accessibility/apps/maps.ts +++ b/x-pack/test/accessibility/apps/maps.ts @@ -119,7 +119,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); - it('single cancel modal', async function () { + // FLAKY: https://github.com/elastic/kibana/issues/128332 + it.skip('single cancel modal', async function () { await testSubjects.click('confirmModalCancelButton'); await a11y.testAppSnapshot(); }); From 27e170a6649367137a241b24f5fef8940a3712bd Mon Sep 17 00:00:00 2001 From: Sandra G Date: Wed, 23 Mar 2022 09:45:31 -0400 Subject: [PATCH 40/64] [Stack Monitoring] fix sorting by node status on nodes listing page (#128323) * fix sorting by node status * fix type * code cleanup --- .../nodes/get_nodes/get_paginated_nodes.ts | 11 +++++++--- .../apps/monitoring/elasticsearch/nodes_mb.js | 22 ++++++++++--------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_paginated_nodes.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_paginated_nodes.ts index 7f250289cf3b63..541320e8499f5d 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_paginated_nodes.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_paginated_nodes.ts @@ -14,6 +14,7 @@ import { sortNodes } from './sort_nodes'; import { paginate } from '../../../pagination/paginate'; import { getMetrics } from '../../../details/get_metrics'; import { LegacyRequest } from '../../../../types'; +import { ElasticsearchModifiedSource } from '../../../../../common/types/es'; /** * This function performs an optimization around the node listing tables in the UI. To avoid @@ -51,7 +52,8 @@ export async function getPaginatedNodes( nodesShardCount, }: { clusterStats: { - cluster_state: { nodes: Record }; + cluster_state?: { nodes: Record }; + elasticsearch?: ElasticsearchModifiedSource['elasticsearch']; }; nodesShardCount: { nodes: Record }; } @@ -61,9 +63,12 @@ export async function getPaginatedNodes( const nodes: Node[] = await getNodeIds(req, { clusterUuid }, size); // Add `isOnline` and shards from the cluster state and shard stats - const clusterState = clusterStats?.cluster_state ?? { nodes: {} }; + const clusterStateNodes = + clusterStats?.cluster_state?.nodes ?? + clusterStats?.elasticsearch?.cluster?.stats?.state?.nodes ?? + {}; for (const node of nodes) { - node.isOnline = !isUndefined(clusterState?.nodes[node.uuid]); + node.isOnline = !isUndefined(clusterStateNodes && clusterStateNodes[node.uuid]); node.shardCount = nodesShardCount?.nodes[node.uuid]?.shardCount ?? 0; } diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/nodes_mb.js b/x-pack/test/functional/apps/monitoring/elasticsearch/nodes_mb.js index aa12619ca447c1..059e18bc865ff9 100644 --- a/x-pack/test/functional/apps/monitoring/elasticsearch/nodes_mb.js +++ b/x-pack/test/functional/apps/monitoring/elasticsearch/nodes_mb.js @@ -189,19 +189,21 @@ export default function ({ getService, getPageObjects }) { }); }); - // this is actually broken, see https://github.com/elastic/kibana/issues/122338 - it.skip('should sort by status', async () => { - const sortedStatusesAscending = ['Status: Offline', 'Status: Online', 'Status: Online']; - const sortedStatusesDescending = [...sortedStatusesAscending].reverse(); - + it('should sort by status', async () => { await nodesList.clickStatusCol(); - await retry.try(async () => { - expect(await nodesList.getNodeStatuses()).to.eql(sortedStatusesDescending); - }); - await nodesList.clickStatusCol(); + + // retry in case the table hasn't had time to re-render await retry.try(async () => { - expect(await nodesList.getNodeStatuses()).to.eql(sortedStatusesAscending); + const nodesAll = await nodesList.getNodesAll(); + const tableData = [ + { status: 'Status: Online' }, + { status: 'Status: Online' }, + { status: 'Status: Offline' }, + ]; + nodesAll.forEach((obj, node) => { + expect(nodesAll[node].status).to.be(tableData[node].status); + }); }); }); From 7c31c8749654ea4cfc895caeb956e6dd9f493f30 Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Wed, 23 Mar 2022 15:05:37 +0100 Subject: [PATCH 41/64] [Expressions] Return total hits count in the `datatable` metadata (#127316) * Add datatable metadata support * Fix datatable-based expressions to preserve metadata * Update ES expression functions to return hits total count in the metadata --- .../datatable_utilities_service.test.ts | 12 ++++++- .../datatable_utilities_service.ts | 6 +++- .../eql_raw_response.test.ts.snap | 6 ++++ .../es_raw_response.test.ts.snap | 9 +++++ .../expressions/eql_raw_response.test.ts | 16 +++++++++ .../search/expressions/eql_raw_response.ts | 3 ++ .../search/expressions/es_raw_response.ts | 6 ++++ .../data/common/search/tabify/tabify.test.ts | 4 +++ .../data/common/search/tabify/tabify.ts | 14 ++++++-- .../expression_functions/specs/map_column.ts | 4 +-- .../expression_functions/specs/math_column.ts | 4 +-- .../expression_types/specs/datatable.ts | 33 +++++++++++++++++++ .../snapshots/baseline/combined_test2.json | 2 +- .../snapshots/baseline/combined_test3.json | 2 +- .../snapshots/baseline/final_output_test.json | 2 +- .../snapshots/baseline/metric_all_data.json | 2 +- .../snapshots/baseline/metric_empty_data.json | 2 +- .../baseline/metric_multi_metric_data.json | 2 +- .../baseline/metric_percentage_mode.json | 2 +- .../baseline/metric_single_metric_data.json | 2 +- .../snapshots/baseline/partial_test_1.json | 2 +- .../snapshots/baseline/partial_test_2.json | 2 +- .../snapshots/baseline/step_output_test2.json | 2 +- .../snapshots/baseline/step_output_test3.json | 2 +- .../snapshots/baseline/tagcloud_all_data.json | 2 +- .../baseline/tagcloud_empty_data.json | 2 +- .../snapshots/baseline/tagcloud_fontsize.json | 2 +- .../baseline/tagcloud_metric_data.json | 2 +- .../snapshots/baseline/tagcloud_options.json | 2 +- .../snapshots/session/combined_test2.json | 2 +- .../snapshots/session/combined_test3.json | 2 +- .../snapshots/session/final_output_test.json | 2 +- .../snapshots/session/metric_all_data.json | 2 +- .../snapshots/session/metric_empty_data.json | 2 +- .../session/metric_multi_metric_data.json | 2 +- .../session/metric_percentage_mode.json | 2 +- .../session/metric_single_metric_data.json | 2 +- .../snapshots/session/partial_test_1.json | 2 +- .../snapshots/session/partial_test_2.json | 2 +- .../snapshots/session/step_output_test2.json | 2 +- .../snapshots/session/step_output_test3.json | 2 +- .../snapshots/session/tagcloud_all_data.json | 2 +- .../session/tagcloud_empty_data.json | 2 +- .../snapshots/session/tagcloud_fontsize.json | 2 +- .../session/tagcloud_metric_data.json | 2 +- .../snapshots/session/tagcloud_options.json | 2 +- .../functions/common/alterColumn.ts | 2 +- .../canvas_plugin_src/functions/common/ply.ts | 2 +- .../functions/common/staticColumn.ts | 2 +- .../rename_columns/rename_columns_fn.ts | 2 +- 50 files changed, 147 insertions(+), 46 deletions(-) diff --git a/src/plugins/data/common/datatable_utilities/datatable_utilities_service.test.ts b/src/plugins/data/common/datatable_utilities/datatable_utilities_service.test.ts index d626bc22265435..0a178a78f1e221 100644 --- a/src/plugins/data/common/datatable_utilities/datatable_utilities_service.test.ts +++ b/src/plugins/data/common/datatable_utilities/datatable_utilities_service.test.ts @@ -8,7 +8,7 @@ import { createStubDataView } from 'src/plugins/data_views/common/mocks'; import type { DataViewsContract } from 'src/plugins/data_views/common'; -import type { DatatableColumn } from 'src/plugins/expressions/common'; +import type { Datatable, DatatableColumn } from 'src/plugins/expressions/common'; import { FieldFormat } from 'src/plugins/field_formats/common'; import { fieldFormatsMock } from 'src/plugins/field_formats/common/mocks'; import type { AggsCommonStart } from '../search'; @@ -106,6 +106,16 @@ describe('DatatableUtilitiesService', () => { }); }); + describe('getTotalCount', () => { + it('should return a total hits count', () => { + const table = { + meta: { statistics: { totalCount: 100 } }, + } as unknown as Datatable; + + expect(datatableUtilitiesService.getTotalCount(table)).toBe(100); + }); + }); + describe('setFieldFormat', () => { it('should set new field format', () => { const column = { meta: {} } as DatatableColumn; diff --git a/src/plugins/data/common/datatable_utilities/datatable_utilities_service.ts b/src/plugins/data/common/datatable_utilities/datatable_utilities_service.ts index cf4e65f31cce34..7430b3f6bc09c6 100644 --- a/src/plugins/data/common/datatable_utilities/datatable_utilities_service.ts +++ b/src/plugins/data/common/datatable_utilities/datatable_utilities_service.ts @@ -7,7 +7,7 @@ */ import type { DataView, DataViewsContract, DataViewField } from 'src/plugins/data_views/common'; -import type { DatatableColumn } from 'src/plugins/expressions/common'; +import type { Datatable, DatatableColumn } from 'src/plugins/expressions/common'; import type { FieldFormatsStartCommon, FieldFormat } from 'src/plugins/field_formats/common'; import type { AggsCommonStart, AggConfig, CreateAggConfigParams, IAggType } from '../search'; @@ -77,6 +77,10 @@ export class DatatableUtilitiesService { return params?.interval; } + getTotalCount(table: Datatable): number | undefined { + return table.meta?.statistics?.totalCount; + } + isFilterable(column: DatatableColumn): boolean { if (column.meta.source !== 'esaggs') { return false; diff --git a/src/plugins/data/common/search/expressions/__snapshots__/eql_raw_response.test.ts.snap b/src/plugins/data/common/search/expressions/__snapshots__/eql_raw_response.test.ts.snap index 341a04cef373f0..ef62a04c301e70 100644 --- a/src/plugins/data/common/search/expressions/__snapshots__/eql_raw_response.test.ts.snap +++ b/src/plugins/data/common/search/expressions/__snapshots__/eql_raw_response.test.ts.snap @@ -24,6 +24,9 @@ Object { ], "meta": Object { "source": "*", + "statistics": Object { + "totalCount": undefined, + }, "type": "eql", }, "rows": Array [ @@ -145,6 +148,9 @@ Object { ], "meta": Object { "source": "*", + "statistics": Object { + "totalCount": undefined, + }, "type": "eql", }, "rows": Array [ diff --git a/src/plugins/data/common/search/expressions/__snapshots__/es_raw_response.test.ts.snap b/src/plugins/data/common/search/expressions/__snapshots__/es_raw_response.test.ts.snap index c43663a50a2bae..89f26aeee7aaf6 100644 --- a/src/plugins/data/common/search/expressions/__snapshots__/es_raw_response.test.ts.snap +++ b/src/plugins/data/common/search/expressions/__snapshots__/es_raw_response.test.ts.snap @@ -42,6 +42,9 @@ Object { ], "meta": Object { "source": "*", + "statistics": Object { + "totalCount": 1977, + }, "type": "esdsl", }, "rows": Array [ @@ -86,6 +89,9 @@ Object { ], "meta": Object { "source": "*", + "statistics": Object { + "totalCount": 1977, + }, "type": "esdsl", }, "rows": Array [ @@ -172,6 +178,9 @@ Object { ], "meta": Object { "source": "*", + "statistics": Object { + "totalCount": 1977, + }, "type": "esdsl", }, "rows": Array [ diff --git a/src/plugins/data/common/search/expressions/eql_raw_response.test.ts b/src/plugins/data/common/search/expressions/eql_raw_response.test.ts index 80d7ca25c89df8..d0fee449d2b009 100644 --- a/src/plugins/data/common/search/expressions/eql_raw_response.test.ts +++ b/src/plugins/data/common/search/expressions/eql_raw_response.test.ts @@ -43,6 +43,22 @@ describe('eqlRawResponse', () => { const result = eqlRawResponse.to!.datatable(response, {}); expect(result).toMatchSnapshot(); }); + + test('extracts total hits number', () => { + const response: EqlRawResponse = { + type: 'eql_raw_response', + body: { + hits: { + events: [], + total: { + value: 2, + }, + }, + }, + }; + const result = eqlRawResponse.to!.datatable(response, {}); + expect(result).toHaveProperty('meta.statistics.totalCount', 2); + }); }); describe('converts sequences to table', () => { diff --git a/src/plugins/data/common/search/expressions/eql_raw_response.ts b/src/plugins/data/common/search/expressions/eql_raw_response.ts index 64e41332a8c172..ccdaed47e16761 100644 --- a/src/plugins/data/common/search/expressions/eql_raw_response.ts +++ b/src/plugins/data/common/search/expressions/eql_raw_response.ts @@ -125,6 +125,9 @@ export const eqlRawResponse: EqlRawResponseExpressionTypeDefinition = { meta: { type: 'eql', source: '*', + statistics: { + totalCount: (context.body as EqlSearchResponse).hits.total?.value, + }, }, columns, rows, diff --git a/src/plugins/data/common/search/expressions/es_raw_response.ts b/src/plugins/data/common/search/expressions/es_raw_response.ts index 61d79939e86356..97c685777e4c78 100644 --- a/src/plugins/data/common/search/expressions/es_raw_response.ts +++ b/src/plugins/data/common/search/expressions/es_raw_response.ts @@ -82,6 +82,12 @@ export const esRawResponse: EsRawResponseExpressionTypeDefinition = { meta: { type: 'esdsl', source: '*', + statistics: { + totalCount: + typeof context.body.hits.total === 'number' + ? context.body.hits.total + : context.body.hits.total?.value, + }, }, columns, rows, diff --git a/src/plugins/data/common/search/tabify/tabify.test.ts b/src/plugins/data/common/search/tabify/tabify.test.ts index 3e1b856de4100a..1f4d23a897c6ed 100644 --- a/src/plugins/data/common/search/tabify/tabify.test.ts +++ b/src/plugins/data/common/search/tabify/tabify.test.ts @@ -52,6 +52,10 @@ describe('tabifyAggResponse Integration', () => { expect(resp.rows[0]).toEqual({ 'col-0-1': 1000 }); expect(resp.columns[0]).toHaveProperty('name', aggConfigs.aggs[0].makeLabel()); + + expect(resp).toHaveProperty('meta.type', 'esaggs'); + expect(resp).toHaveProperty('meta.source', '1234'); + expect(resp).toHaveProperty('meta.statistics.totalCount', 1000); }); describe('scaleMetricValues performance check', () => { diff --git a/src/plugins/data/common/search/tabify/tabify.ts b/src/plugins/data/common/search/tabify/tabify.ts index 7bc02ce353d53b..a640e75bac3c4d 100644 --- a/src/plugins/data/common/search/tabify/tabify.ts +++ b/src/plugins/data/common/search/tabify/tabify.ts @@ -7,6 +7,7 @@ */ import { get } from 'lodash'; +import type { Datatable } from 'src/plugins/expressions'; import { TabbedAggResponseWriter } from './response_writer'; import { TabifyBuckets } from './buckets'; import type { TabbedResponseWriterOptions } from './types'; @@ -20,7 +21,7 @@ export function tabifyAggResponse( aggConfigs: IAggConfigs, esResponse: Record, respOpts?: Partial -) { +): Datatable { /** * read an aggregation from a bucket, which *might* be found at key (if * the response came in object form), and will recurse down the aggregation @@ -152,5 +153,14 @@ export function tabifyAggResponse( collectBucket(aggConfigs, write, topLevelBucket, '', 1); - return write.response(); + return { + ...write.response(), + meta: { + type: 'esaggs', + source: aggConfigs.indexPattern.id, + statistics: { + totalCount: esResponse.hits?.total, + }, + }, + }; } diff --git a/src/plugins/expressions/common/expression_functions/specs/map_column.ts b/src/plugins/expressions/common/expression_functions/specs/map_column.ts index 7b2266637bfb5a..c3a267d9dca6cf 100644 --- a/src/plugins/expressions/common/expression_functions/specs/map_column.ts +++ b/src/plugins/expressions/common/expression_functions/specs/map_column.ts @@ -95,7 +95,7 @@ export const mapColumn: ExpressionFunctionDefinition< input.rows.map((row) => args .expression({ - type: 'datatable', + ...input, columns: [...input.columns], rows: [row], }) @@ -129,9 +129,9 @@ export const mapColumn: ExpressionFunctionDefinition< }; return { + ...input, columns, rows, - type: 'datatable', }; }) ); diff --git a/src/plugins/expressions/common/expression_functions/specs/math_column.ts b/src/plugins/expressions/common/expression_functions/specs/math_column.ts index ae6cc8b755fe17..b513ef5d274091 100644 --- a/src/plugins/expressions/common/expression_functions/specs/math_column.ts +++ b/src/plugins/expressions/common/expression_functions/specs/math_column.ts @@ -79,7 +79,7 @@ export const mathColumn: ExpressionFunctionDefinition< input.rows.map(async (row) => { const result = await math.fn( { - type: 'datatable', + ...input, columns: input.columns, rows: [row], }, @@ -128,7 +128,7 @@ export const mathColumn: ExpressionFunctionDefinition< columns.push(newColumn); return { - type: 'datatable', + ...input, columns, rows: newRows, } as Datatable; diff --git a/src/plugins/expressions/common/expression_types/specs/datatable.ts b/src/plugins/expressions/common/expression_types/specs/datatable.ts index a07f103d12e063..2a4820508210d8 100644 --- a/src/plugins/expressions/common/expression_types/specs/datatable.ts +++ b/src/plugins/expressions/common/expression_types/specs/datatable.ts @@ -91,12 +91,45 @@ export interface DatatableColumn { meta: DatatableColumnMeta; } +/** + * Metadata with statistics about the `Datatable` source. + */ +export interface DatatableMetaStatistics { + /** + * Total hits number returned for the request generated the `Datatable`. + */ + totalCount?: number; +} + +/** + * The `Datatable` meta information. + */ +export interface DatatableMeta { + /** + * Statistics about the `Datatable` source. + */ + statistics?: DatatableMetaStatistics; + + /** + * The `Datatable` type (e.g. `essql`, `eql`, `esdsl`, etc.). + */ + type?: string; + + /** + * The `Datatable` data source. + */ + source?: string; + + [key: string]: unknown; +} + /** * A `Datatable` in Canvas is a unique structure that represents tabulated data. */ export interface Datatable { type: typeof name; columns: DatatableColumn[]; + meta?: DatatableMeta; rows: DatatableRow[]; } diff --git a/test/interpreter_functional/snapshots/baseline/combined_test2.json b/test/interpreter_functional/snapshots/baseline/combined_test2.json index b4129ac898eed9..7e51931665a65f 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test2.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test2.json @@ -1 +1 @@ -{"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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"} \ No newline at end of file +{"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/combined_test3.json b/test/interpreter_functional/snapshots/baseline/combined_test3.json index dc1c037f45e950..5d22d728c0a4ad 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test3.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test3.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/final_output_test.json b/test/interpreter_functional/snapshots/baseline/final_output_test.json index dc1c037f45e950..5d22d728c0a4ad 100644 --- a/test/interpreter_functional/snapshots/baseline/final_output_test.json +++ b/test/interpreter_functional/snapshots/baseline/final_output_test.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_all_data.json b/test/interpreter_functional/snapshots/baseline/metric_all_data.json index 939e51b619928c..bd0c93b1de0579 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_all_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"bytes","params":null},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"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":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"bytes","params":null},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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 diff --git a/test/interpreter_functional/snapshots/baseline/metric_empty_data.json b/test/interpreter_functional/snapshots/baseline/metric_empty_data.json index 6adb4e117d2c7d..e38a14fe2b57e0 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_empty_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_empty_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"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 +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json index 4a324a133c057e..306c5f40b3d257 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"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":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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 diff --git a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json index 944820d0ed16d0..01fe67d1e6a15c 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":{"colors":["rgb(0,0,0,0)","rgb(100, 100, 100)"],"continuity":"none","gradient":false,"range":"number","rangeMax":10000,"rangeMin":0,"stops":[0,10000]},"percentageMode":true,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"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":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":{"colors":["rgb(0,0,0,0)","rgb(100, 100, 100)"],"continuity":"none","gradient":false,"range":"number","rangeMax":10000,"rangeMin":0,"stops":[0,10000]},"percentageMode":true,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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 diff --git a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json index 392649d410e158..bf2ddf5e6e1841 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"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":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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 diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_1.json b/test/interpreter_functional/snapshots/baseline/partial_test_1.json index 8ce0ee16a0b3b2..0e264569679629 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_1.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_1.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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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":{"ariaLabel":null,"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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":{"ariaLabel":null,"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_2.json b/test/interpreter_functional/snapshots/baseline/partial_test_2.json index dc1c037f45e950..5d22d728c0a4ad 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_2.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_2.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test2.json b/test/interpreter_functional/snapshots/baseline/step_output_test2.json index b4129ac898eed9..7e51931665a65f 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test2.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test2.json @@ -1 +1 @@ -{"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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"} \ No newline at end of file +{"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test3.json b/test/interpreter_functional/snapshots/baseline/step_output_test3.json index dc1c037f45e950..5d22d728c0a4ad 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test3.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test3.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json index 837251a438911a..d373194db261d5 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_all_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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json index 5c3ca14f4eab73..864aa3538477ed 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[],"type":"datatable"},"visParams":{"ariaLabel":null,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[],"type":"datatable"},"visParams":{"ariaLabel":null,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json index 5e99024d6e52bc..461bdae0e172c2 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json index e00233197bda38..4eb2297db5425a 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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":{"ariaLabel":null,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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":{"ariaLabel":null,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json index 759b2752f93289..d7892c9197b7f5 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_options.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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test2.json b/test/interpreter_functional/snapshots/session/combined_test2.json index b4129ac898eed9..7e51931665a65f 100644 --- a/test/interpreter_functional/snapshots/session/combined_test2.json +++ b/test/interpreter_functional/snapshots/session/combined_test2.json @@ -1 +1 @@ -{"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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"} \ No newline at end of file +{"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test3.json b/test/interpreter_functional/snapshots/session/combined_test3.json index dc1c037f45e950..5d22d728c0a4ad 100644 --- a/test/interpreter_functional/snapshots/session/combined_test3.json +++ b/test/interpreter_functional/snapshots/session/combined_test3.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/final_output_test.json b/test/interpreter_functional/snapshots/session/final_output_test.json index dc1c037f45e950..5d22d728c0a4ad 100644 --- a/test/interpreter_functional/snapshots/session/final_output_test.json +++ b/test/interpreter_functional/snapshots/session/final_output_test.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_all_data.json b/test/interpreter_functional/snapshots/session/metric_all_data.json index 939e51b619928c..bd0c93b1de0579 100644 --- a/test/interpreter_functional/snapshots/session/metric_all_data.json +++ b/test/interpreter_functional/snapshots/session/metric_all_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"bytes","params":null},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"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":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"bytes","params":null},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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 diff --git a/test/interpreter_functional/snapshots/session/metric_empty_data.json b/test/interpreter_functional/snapshots/session/metric_empty_data.json index 6adb4e117d2c7d..e38a14fe2b57e0 100644 --- a/test/interpreter_functional/snapshots/session/metric_empty_data.json +++ b/test/interpreter_functional/snapshots/session/metric_empty_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"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 +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json index 4a324a133c057e..306c5f40b3d257 100644 --- a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"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":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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 diff --git a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json index 944820d0ed16d0..01fe67d1e6a15c 100644 --- a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":{"colors":["rgb(0,0,0,0)","rgb(100, 100, 100)"],"continuity":"none","gradient":false,"range":"number","rangeMax":10000,"rangeMin":0,"stops":[0,10000]},"percentageMode":true,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"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":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":{"colors":["rgb(0,0,0,0)","rgb(100, 100, 100)"],"continuity":"none","gradient":false,"range":"number","rangeMax":10000,"rangeMin":0,"stops":[0,10000]},"percentageMode":true,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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 diff --git a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json index 392649d410e158..bf2ddf5e6e1841 100644 --- a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"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":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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 diff --git a/test/interpreter_functional/snapshots/session/partial_test_1.json b/test/interpreter_functional/snapshots/session/partial_test_1.json index 8ce0ee16a0b3b2..0e264569679629 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_1.json +++ b/test/interpreter_functional/snapshots/session/partial_test_1.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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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":{"ariaLabel":null,"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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":{"ariaLabel":null,"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_2.json b/test/interpreter_functional/snapshots/session/partial_test_2.json index dc1c037f45e950..5d22d728c0a4ad 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_2.json +++ b/test/interpreter_functional/snapshots/session/partial_test_2.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test2.json b/test/interpreter_functional/snapshots/session/step_output_test2.json index b4129ac898eed9..7e51931665a65f 100644 --- a/test/interpreter_functional/snapshots/session/step_output_test2.json +++ b/test/interpreter_functional/snapshots/session/step_output_test2.json @@ -1 +1 @@ -{"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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"} \ No newline at end of file +{"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test3.json b/test/interpreter_functional/snapshots/session/step_output_test3.json index dc1c037f45e950..5d22d728c0a4ad 100644 --- a/test/interpreter_functional/snapshots/session/step_output_test3.json +++ b/test/interpreter_functional/snapshots/session/step_output_test3.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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"},"visType":"metric"}} \ No newline at end of file +{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json index 837251a438911a..d373194db261d5 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_all_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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ 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 index 5c3ca14f4eab73..864aa3538477ed 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_empty_data.json +++ b/test/interpreter_functional/snapshots/session/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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[],"type":"datatable"},"visParams":{"ariaLabel":null,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[],"type":"datatable"},"visParams":{"ariaLabel":null,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json index 5e99024d6e52bc..461bdae0e172c2 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json index e00233197bda38..4eb2297db5425a 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_metric_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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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":{"ariaLabel":null,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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":{"ariaLabel":null,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_options.json b/test/interpreter_functional/snapshots/session/tagcloud_options.json index 759b2752f93289..d7892c9197b7f5 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_options.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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"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":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","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,"hasPrecisionError":false,"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,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"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":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.ts index b495eb170b5b6d..77bf11c71a35ae 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.ts @@ -101,7 +101,7 @@ export function alterColumn(): ExpressionFunctionDefinition< })); return { - type: 'datatable', + ...input, columns, rows, }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.ts index 5ab7b95f0d00ba..1a38dfa84fd7ec 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.ts @@ -85,7 +85,7 @@ export function ply(): ExpressionFunctionDefinition<'ply', Datatable, Arguments, ); return { - type: 'datatable', + ...input, rows, columns, } as Datatable; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.ts index 373a9504712d66..032e7298e02f12 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.ts @@ -58,7 +58,7 @@ export function staticColumn(): ExpressionFunctionDefinition< } return { - type: 'datatable', + ...input, columns, rows, }; diff --git a/x-pack/plugins/lens/common/expressions/rename_columns/rename_columns_fn.ts b/x-pack/plugins/lens/common/expressions/rename_columns/rename_columns_fn.ts index ee0c7ed1eebec5..c201c5f8f07e17 100644 --- a/x-pack/plugins/lens/common/expressions/rename_columns/rename_columns_fn.ts +++ b/x-pack/plugins/lens/common/expressions/rename_columns/rename_columns_fn.ts @@ -28,7 +28,7 @@ export const renameColumnFn: RenameColumnsExpressionFunction['fn'] = ( const idMap = JSON.parse(encodedIdMap) as Record; return { - type: 'datatable', + ...data, rows: data.rows.map((row) => { const mappedRow: Record = {}; Object.entries(idMap).forEach(([fromId, toId]) => { From 75f8ac424e5f58a3c7cdce4f4dcffdae559b10c2 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 23 Mar 2022 08:20:10 -0600 Subject: [PATCH 42/64] [Maps] Add support for geohex_grid aggregation (#127170) * [Maps] hex bin gridding * remove console.log * disable hexbins for license and geo_shape * fix jest tests * copy cleanup * label * update clusters SVG with hexbins * show as tooltip * documenation updates * copy updates * add API test for hex * test cleanup * eslint * eslint and functional test fixes * eslint, copy updates, and more doc updates * fix i18n error * consolidate isMvt logic * copy review feedback * use 3 stop scale for hexs * jest snapshot updates * Update x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx Co-authored-by: Nick Peihl Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Nick Peihl --- docs/maps/maps-aggregations.asciidoc | 18 +- docs/maps/maps-getting-started.asciidoc | 2 +- docs/maps/vector-layer.asciidoc | 2 +- x-pack/plugins/maps/common/constants.ts | 1 + x-pack/plugins/maps/kibana.json | 1 + .../wizards/icons/clusters_layer_icon.tsx | 35 ++-- .../resolution_editor.test.tsx.snap | 154 +++++++++++++++++- .../update_source_editor.test.tsx.snap | 6 +- .../clusters_layer_wizard.tsx | 106 ++++++------ .../create_source_editor.js | 17 +- .../es_geo_grid_source.test.ts | 2 +- .../es_geo_grid_source/es_geo_grid_source.tsx | 45 +++-- .../sources/es_geo_grid_source/is_mvt.ts | 23 +++ .../es_geo_grid_source/render_as_select.tsx | 68 -------- .../render_as_select/i18n_constants.ts | 23 +++ .../render_as_select/index.ts | 8 + .../render_as_select/render_as_select.tsx | 92 +++++++++++ .../render_as_select/show_as_label.tsx | 64 ++++++++ .../resolution_editor.test.tsx | 22 ++- .../es_geo_grid_source/resolution_editor.tsx | 127 ++++++++------- .../update_source_editor.test.tsx | 1 + .../update_source_editor.tsx | 56 +++++-- .../classes/sources/es_source/es_source.ts | 2 +- x-pack/plugins/maps/public/kibana_services.ts | 6 + x-pack/plugins/maps/public/plugin.ts | 6 +- .../plugins/maps/server/mvt/get_grid_tile.ts | 7 +- x-pack/plugins/maps/server/mvt/mvt_routes.ts | 4 +- x-pack/plugins/maps/tsconfig.json | 1 + .../apis/maps/get_grid_tile.js | 135 +++++++++------ .../functional/apps/maps/mvt_geotile_grid.js | 2 +- 30 files changed, 739 insertions(+), 297 deletions(-) create mode 100644 x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/is_mvt.ts delete mode 100644 x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select.tsx create mode 100644 x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select/i18n_constants.ts create mode 100644 x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select/index.ts create mode 100644 x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select/render_as_select.tsx create mode 100644 x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select/show_as_label.tsx diff --git a/docs/maps/maps-aggregations.asciidoc b/docs/maps/maps-aggregations.asciidoc index fced15771c3864..8ffd9087704555 100644 --- a/docs/maps/maps-aggregations.asciidoc +++ b/docs/maps/maps-aggregations.asciidoc @@ -42,22 +42,24 @@ image::maps/images/grid_to_docs.gif[] [role="xpack"] [[maps-grid-aggregation]] -=== Grid aggregation +=== Clusters -Grid aggregation layers use {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[GeoTile grid aggregation] to group your documents into grids. You can calculate metrics for each gridded cell. +Clusters use {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[Geotile grid aggregation] or {ref}/search-aggregations-bucket-geohexgrid-aggregation.html[Geohex grid aggregation] to group your documents into grids. You can calculate metrics for each gridded cell. -Symbolize grid aggregation metrics as: +Symbolize cluster metrics as: -*Clusters*:: Creates a <> with a cluster symbol for each gridded cell. +*Clusters*:: Uses {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[Geotile grid aggregation] to group your documents into grids. Creates a <> with a cluster symbol for each gridded cell. The cluster location is the weighted centroid for all documents in the gridded cell. -*Grid rectangles*:: Creates a <> with a bounding box polygon for each gridded cell. +*Grids*:: Uses {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[Geotile grid aggregation] to group your documents into grids. Creates a <> with a bounding box polygon for each gridded cell. -*Heat map*:: Creates a <> that clusters the weighted centroids for each gridded cell. +*Heat map*:: Uses {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[Geotile grid aggregation] to group your documents into grids. Creates a <> that clusters the weighted centroids for each gridded cell. -To enable a grid aggregation layer: +*Hexbins*:: Uses {ref}/search-aggregations-bucket-geohexgrid-aggregation.html[Geohex grid aggregation] to group your documents into H3 hexagon grids. Creates a <> with a hexagon polygon for each gridded cell. -. Click *Add layer*, then select the *Clusters and grids* or *Heat map* layer. +To enable a clusters layer: + +. Click *Add layer*, then select the *Clusters* or *Heat map* layer. To enable a blended layer that dynamically shows clusters or documents: diff --git a/docs/maps/maps-getting-started.asciidoc b/docs/maps/maps-getting-started.asciidoc index a85586fc431887..d4da7ef8aae2ed 100644 --- a/docs/maps/maps-getting-started.asciidoc +++ b/docs/maps/maps-getting-started.asciidoc @@ -128,7 +128,7 @@ traffic. Larger circles will symbolize grids with more total bytes transferred, and smaller circles will symbolize grids with less bytes transferred. -. Click **Add layer**, and select **Clusters and grids**. +. Click **Add layer**, and select **Clusters**. . Set **Data view** to **kibana_sample_data_logs**. . Click **Add layer**. . In **Layer settings**, set: diff --git a/docs/maps/vector-layer.asciidoc b/docs/maps/vector-layer.asciidoc index cf6dd5334b07e5..8ad2aaf4c9769e 100644 --- a/docs/maps/vector-layer.asciidoc +++ b/docs/maps/vector-layer.asciidoc @@ -11,7 +11,7 @@ To add a vector layer to your map, click *Add layer*, then select one of the fol *Choropleth*:: Shaded areas to compare statistics across boundaries. -*Clusters and grids*:: Geospatial data grouped in grids with metrics for each gridded cell. +*Clusters*:: Geospatial data grouped in grids with metrics for each gridded cell. The index must contain at least one field mapped as {ref}/geo-point.html[geo_point] or {ref}/geo-shape.html[geo_shape]. *Create index*:: Draw shapes on the map and index in Elasticsearch. diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 435b4e55b4cecb..e02fead277f60e 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -164,6 +164,7 @@ export enum RENDER_AS { HEATMAP = 'heatmap', POINT = 'point', GRID = 'grid', + HEX = 'hex', } export enum GRID_RESOLUTION { diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index e049a0870855a8..a3264a406b7597 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -27,6 +27,7 @@ "presentationUtil" ], "optionalPlugins": [ + "cloud", "customIntegrations", "home", "savedObjectsTagging", diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/icons/clusters_layer_icon.tsx b/x-pack/plugins/maps/public/classes/layers/wizards/icons/clusters_layer_icon.tsx index 4c54d5faca5c71..abdc3a4f61fece 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/icons/clusters_layer_icon.tsx +++ b/x-pack/plugins/maps/public/classes/layers/wizards/icons/clusters_layer_icon.tsx @@ -12,27 +12,24 @@ export const ClustersLayerIcon: FunctionComponent = () => ( xmlns="http://www.w3.org/2000/svg" width="49" height="25" - fill="none" viewBox="0 0 49 25" className="mapLayersWizardIcon" > - - - - - - - - - - - - - - - - - - + + + + ); diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/resolution_editor.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/resolution_editor.test.tsx.snap index 90a5bd6758bde8..7ef5e39ba96f01 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/resolution_editor.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/resolution_editor.test.tsx.snap @@ -1,6 +1,158 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`render 1`] = ` +exports[`should render 3 tick slider when renderAs is HEX 1`] = ` + + + + + +`; + +exports[`should render 4 tick slider when renderAs is GRID 1`] = ` + + + + + +`; + +exports[`should render 4 tick slider when renderAs is HEATMAP 1`] = ` + + + + + +`; + +exports[`should render 4 tick slider when renderAs is POINT 1`] = `
- Clusters and grids + Clusters
{ @@ -48,62 +49,71 @@ export const clustersLayerWizardConfig: LayerWizard = { return; } + const sourceDescriptor = ESGeoGridSource.createDescriptor({ + ...sourceConfig, + resolution: GRID_RESOLUTION.FINE, + }); + const defaultDynamicProperties = getDefaultDynamicProperties(); - const layerDescriptor = GeoJsonVectorLayer.createDescriptor({ - sourceDescriptor: ESGeoGridSource.createDescriptor({ - ...sourceConfig, - resolution: GRID_RESOLUTION.FINE, - }), - style: VectorStyle.createDescriptor({ - // @ts-ignore - [VECTOR_STYLES.FILL_COLOR]: { - type: STYLE_TYPE.DYNAMIC, - options: { - ...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR]! - .options as ColorDynamicOptions), - field: { - name: COUNT_PROP_NAME, - origin: FIELD_ORIGIN.SOURCE, - }, - color: NUMERICAL_COLOR_PALETTES[0].value, - type: COLOR_MAP_TYPE.ORDINAL, + const style = VectorStyle.createDescriptor({ + // @ts-ignore + [VECTOR_STYLES.FILL_COLOR]: { + type: STYLE_TYPE.DYNAMIC, + options: { + ...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR]!.options as ColorDynamicOptions), + field: { + name: COUNT_PROP_NAME, + origin: FIELD_ORIGIN.SOURCE, }, + color: NUMERICAL_COLOR_PALETTES[0].value, + type: COLOR_MAP_TYPE.ORDINAL, }, - [VECTOR_STYLES.LINE_COLOR]: { - type: STYLE_TYPE.STATIC, - options: { - color: '#FFF', - }, + }, + [VECTOR_STYLES.LINE_COLOR]: { + type: STYLE_TYPE.STATIC, + options: { + color: '#FFF', }, - [VECTOR_STYLES.LINE_WIDTH]: { - type: STYLE_TYPE.STATIC, - options: { - size: 0, - }, + }, + [VECTOR_STYLES.LINE_WIDTH]: { + type: STYLE_TYPE.STATIC, + options: { + size: 0, }, - [VECTOR_STYLES.ICON_SIZE]: { - type: STYLE_TYPE.DYNAMIC, - options: { - ...(defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE].options as SizeDynamicOptions), - maxSize: 24, - field: { - name: COUNT_PROP_NAME, - origin: FIELD_ORIGIN.SOURCE, - }, + }, + [VECTOR_STYLES.ICON_SIZE]: { + type: STYLE_TYPE.DYNAMIC, + options: { + ...(defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE].options as SizeDynamicOptions), + maxSize: 24, + field: { + name: COUNT_PROP_NAME, + origin: FIELD_ORIGIN.SOURCE, }, }, - [VECTOR_STYLES.LABEL_TEXT]: { - type: STYLE_TYPE.DYNAMIC, - options: { - ...defaultDynamicProperties[VECTOR_STYLES.LABEL_TEXT].options, - field: { - name: COUNT_PROP_NAME, - origin: FIELD_ORIGIN.SOURCE, - }, + }, + [VECTOR_STYLES.LABEL_TEXT]: { + type: STYLE_TYPE.DYNAMIC, + options: { + ...defaultDynamicProperties[VECTOR_STYLES.LABEL_TEXT].options, + field: { + name: COUNT_PROP_NAME, + origin: FIELD_ORIGIN.SOURCE, }, }, - }), + }, }); + + const layerDescriptor = + sourceDescriptor.requestType === RENDER_AS.HEX + ? MvtVectorLayer.createDescriptor({ + sourceDescriptor, + style, + }) + : GeoJsonVectorLayer.createDescriptor({ + sourceDescriptor, + style, + }); previewLayers([layerDescriptor]); }; diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/create_source_editor.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/create_source_editor.js index e829400c4bbef1..872c7b71c9f92b 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/create_source_editor.js +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/create_source_editor.js @@ -51,10 +51,15 @@ export class CreateSourceEditor extends Component { ); }; - _onGeoFieldSelect = (geoField) => { + _onGeoFieldSelect = (geoFieldName) => { + const geoField = + this.state.indexPattern && geoFieldName + ? this.state.indexPattern.fields.getByName(geoFieldName) + : undefined; this.setState( { - geoField, + geoField: geoFieldName, + geoFieldType: geoField ? geoField.type : undefined, }, this.previewLayer ); @@ -85,7 +90,7 @@ export class CreateSourceEditor extends Component { return ( + ); } diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts index a26bd341613b2d..cd93ccff99a4c8 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts @@ -316,7 +316,7 @@ describe('ESGeoGridSource', () => { const tileUrl = await mvtGeogridSource.getTileUrl(vectorSourceRequestMeta, '1234'); expect(tileUrl).toEqual( - "rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&gridPrecision=8&requestBody=(foobar%3AES_DSL_PLACEHOLDER%2Cparams%3A('0'%3A('0'%3Aindex%2C'1'%3A(fields%3A()))%2C'1'%3A('0'%3Asize%2C'1'%3A0)%2C'2'%3A('0'%3Afilter%2C'1'%3A!())%2C'3'%3A('0'%3Aquery)%2C'4'%3A('0'%3Aindex%2C'1'%3A(fields%3A()))%2C'5'%3A('0'%3Aquery%2C'1'%3A(language%3AKQL%2Cquery%3A''))%2C'6'%3A('0'%3Aaggs%2C'1'%3A())))&requestType=point&token=1234" + "rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&gridPrecision=8&requestBody=(foobar%3AES_DSL_PLACEHOLDER%2Cparams%3A('0'%3A('0'%3Aindex%2C'1'%3A(fields%3A()))%2C'1'%3A('0'%3Asize%2C'1'%3A0)%2C'2'%3A('0'%3Afilter%2C'1'%3A!())%2C'3'%3A('0'%3Aquery)%2C'4'%3A('0'%3Aindex%2C'1'%3A(fields%3A()))%2C'5'%3A('0'%3Aquery%2C'1'%3A(language%3AKQL%2Cquery%3A''))%2C'6'%3A('0'%3Aaggs%2C'1'%3A())))&renderAs=heatmap&token=1234" ); }); }); diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx index 2eff5ce712ad53..67529edf15b4cc 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx @@ -45,13 +45,14 @@ import { DataView } from '../../../../../../../src/plugins/data/common'; import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters'; import { isValidStringConfig } from '../../util/valid_string_config'; import { makePublicExecutionContext } from '../../../util'; +import { isMvt } from './is_mvt'; type ESGeoGridSourceSyncMeta = Pick; const MAX_GEOTILE_LEVEL = 29; export const clustersTitle = i18n.translate('xpack.maps.source.esGridClustersTitle', { - defaultMessage: 'Clusters and grids', + defaultMessage: 'Clusters', }); export const heatmapTitle = i18n.translate('xpack.maps.source.esGridHeatmapTitle', { @@ -87,6 +88,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements IMvtVectorSo return ( { - if (this._descriptor.requestType === RENDER_AS.GRID) { + if ( + this._descriptor.requestType === RENDER_AS.GRID || + this._descriptor.requestType === RENDER_AS.HEX + ) { return [VECTOR_SHAPE_TYPE.POLYGON]; } diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/is_mvt.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/is_mvt.ts new file mode 100644 index 00000000000000..98115e9dcd992d --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/is_mvt.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. + */ + +import { GRID_RESOLUTION, RENDER_AS } from '../../../../common/constants'; + +export function isMvt(renderAs: RENDER_AS, resolution: GRID_RESOLUTION): boolean { + // heatmap uses MVT regardless of resolution because heatmap only supports counting metrics + if (renderAs === RENDER_AS.HEATMAP) { + return true; + } + + // hex uses MVT regardless of resolution because hex never supported "top terms" metric + if (renderAs === RENDER_AS.HEX) { + return true; + } + + // point and grid only use mvt at high resolution because lower resolutions may contain mvt unsupported "top terms" metric + return resolution === GRID_RESOLUTION.SUPER_FINE; +} diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select.tsx deleted file mode 100644 index 17fec469fe4aed..00000000000000 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select.tsx +++ /dev/null @@ -1,68 +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 React from 'react'; -import { EuiFormRow, EuiButtonGroup } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { RENDER_AS } from '../../../../common/constants'; - -const options = [ - { - id: RENDER_AS.POINT, - label: i18n.translate('xpack.maps.source.esGeoGrid.pointsDropdownOption', { - defaultMessage: 'clusters', - }), - value: RENDER_AS.POINT, - }, - { - id: RENDER_AS.GRID, - label: i18n.translate('xpack.maps.source.esGeoGrid.gridRectangleDropdownOption', { - defaultMessage: 'grids', - }), - value: RENDER_AS.GRID, - }, -]; - -export function RenderAsSelect(props: { - renderAs: RENDER_AS; - onChange: (newValue: RENDER_AS) => void; - isColumnCompressed?: boolean; -}) { - const currentOption = options.find((option) => option.value === props.renderAs) || options[0]; - - if (props.renderAs === RENDER_AS.HEATMAP) { - return null; - } - - function onChange(id: string) { - const data = options.find((option) => option.id === id); - if (data) { - props.onChange(data.value as RENDER_AS); - } - } - - return ( - - - - ); -} diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select/i18n_constants.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select/i18n_constants.ts new file mode 100644 index 00000000000000..3e9d79d8cd4865 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select/i18n_constants.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. + */ + +import { i18n } from '@kbn/i18n'; + +export const CLUSTER_LABEL = i18n.translate('xpack.maps.source.esGeoGrid.pointsDropdownOption', { + defaultMessage: 'Clusters', +}); + +export const GRID_LABEL = i18n.translate( + 'xpack.maps.source.esGeoGrid.gridRectangleDropdownOption', + { + defaultMessage: 'Grids', + } +); + +export const HEX_LABEL = i18n.translate('xpack.maps.source.esGeoGrid.hexDropdownOption', { + defaultMessage: 'Hexagons', +}); diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select/index.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select/index.ts new file mode 100644 index 00000000000000..4930a8ebfc0a9d --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select/index.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 { RenderAsSelect } from './render_as_select'; diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select/render_as_select.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select/render_as_select.tsx new file mode 100644 index 00000000000000..e5baf65711d3f0 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select/render_as_select.tsx @@ -0,0 +1,92 @@ +/* + * 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 { EuiFormRow, EuiButtonGroup } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ES_GEO_FIELD_TYPE, RENDER_AS } from '../../../../../common/constants'; +import { getIsCloud } from '../../../../kibana_services'; +import { getIsGoldPlus } from '../../../../licensed_features'; +import { CLUSTER_LABEL, GRID_LABEL, HEX_LABEL } from './i18n_constants'; +import { ShowAsLabel } from './show_as_label'; + +interface Props { + geoFieldType?: ES_GEO_FIELD_TYPE; + renderAs: RENDER_AS; + onChange: (newValue: RENDER_AS) => void; + isColumnCompressed?: boolean; +} + +export function RenderAsSelect(props: Props) { + if (props.renderAs === RENDER_AS.HEATMAP) { + return null; + } + + let isHexDisabled = false; + let hexDisabledReason = ''; + if (!getIsCloud() && !getIsGoldPlus()) { + isHexDisabled = true; + hexDisabledReason = i18n.translate('xpack.maps.hexbin.license.disabledReason', { + defaultMessage: '{hexLabel} is a subscription feature.', + values: { hexLabel: HEX_LABEL }, + }); + } else if (props.geoFieldType !== ES_GEO_FIELD_TYPE.GEO_POINT) { + isHexDisabled = true; + hexDisabledReason = i18n.translate('xpack.maps.hexbin.geoShape.disabledReason', { + defaultMessage: `{hexLabel} requires a 'geo_point' cluster field.`, + values: { hexLabel: HEX_LABEL }, + }); + } + + const options = [ + { + id: RENDER_AS.POINT, + label: CLUSTER_LABEL, + value: RENDER_AS.POINT, + }, + { + id: RENDER_AS.GRID, + label: GRID_LABEL, + value: RENDER_AS.GRID, + }, + { + id: RENDER_AS.HEX, + label: HEX_LABEL, + value: RENDER_AS.HEX, + isDisabled: isHexDisabled, + }, + ]; + + function onChange(id: string) { + const data = options.find((option) => option.id === id); + if (data) { + props.onChange(data.value as RENDER_AS); + } + } + + const currentOption = options.find((option) => option.value === props.renderAs) || options[0]; + + const selectLabel = ( + + ); + + return ( + + + + ); +} diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select/show_as_label.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select/show_as_label.tsx new file mode 100644 index 00000000000000..e16bc1cb8bcffd --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select/show_as_label.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiIcon, EuiText, EuiToolTip } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { CLUSTER_LABEL, GRID_LABEL, HEX_LABEL } from './i18n_constants'; + +interface Props { + isHexDisabled: boolean; + hexDisabledReason: string; +} + +export function ShowAsLabel(props: Props) { + return ( + +
+
{CLUSTER_LABEL}
+
+

+ +

+
+ +
{GRID_LABEL}
+
+

+ +

+
+ +
{HEX_LABEL}
+
+

+ +

+ {props.isHexDisabled ? {props.hexDisabledReason} : null} +
+
+ + } + > + + {' '} + + +
+ ); +} diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.test.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.test.tsx index 9802b91b47cd6e..bb659d13a2bb7e 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.test.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.test.tsx @@ -9,16 +9,30 @@ import React from 'react'; import { shallow } from 'enzyme'; import { ResolutionEditor } from './resolution_editor'; -import { GRID_RESOLUTION } from '../../../../common/constants'; +import { GRID_RESOLUTION, RENDER_AS } from '../../../../common/constants'; const defaultProps = { - isHeatmap: false, resolution: GRID_RESOLUTION.COARSE, onChange: () => {}, metrics: [], }; -test('render', () => { - const component = shallow(); +test('should render 4 tick slider when renderAs is POINT', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); +}); + +test('should render 4 tick slider when renderAs is GRID', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); +}); + +test('should render 4 tick slider when renderAs is HEATMAP', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); +}); + +test('should render 3 tick slider when renderAs is HEX', () => { + const component = shallow(); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.tsx index 72dec662791645..d6f3758de1c0b4 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.tsx @@ -10,46 +10,15 @@ import { EuiConfirmModal, EuiFormRow, EuiRange } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { AggDescriptor } from '../../../../common/descriptor_types'; -import { AGG_TYPE, GRID_RESOLUTION } from '../../../../common/constants'; - -function resolutionToSliderValue(resolution: GRID_RESOLUTION) { - if (resolution === GRID_RESOLUTION.SUPER_FINE) { - return 4; - } - - if (resolution === GRID_RESOLUTION.MOST_FINE) { - return 3; - } - - if (resolution === GRID_RESOLUTION.FINE) { - return 2; - } - - return 1; -} - -function sliderValueToResolution(value: number) { - if (value === 4) { - return GRID_RESOLUTION.SUPER_FINE; - } - - if (value === 3) { - return GRID_RESOLUTION.MOST_FINE; - } - - if (value === 2) { - return GRID_RESOLUTION.FINE; - } - - return GRID_RESOLUTION.COARSE; -} +import { AGG_TYPE, GRID_RESOLUTION, RENDER_AS } from '../../../../common/constants'; +import { isMvt } from './is_mvt'; function isUnsupportedVectorTileMetric(metric: AggDescriptor) { return metric.type === AGG_TYPE.TERMS; } interface Props { - isHeatmap: boolean; + renderAs: RENDER_AS; resolution: GRID_RESOLUTION; onChange: (resolution: GRID_RESOLUTION, metrics: AggDescriptor[]) => void; metrics: AggDescriptor[]; @@ -64,9 +33,70 @@ export class ResolutionEditor extends Component { showModal: false, }; + _getScale() { + return this.props.renderAs === RENDER_AS.HEX + ? { + [GRID_RESOLUTION.SUPER_FINE]: 3, + [GRID_RESOLUTION.MOST_FINE]: 2, + [GRID_RESOLUTION.FINE]: 2, + [GRID_RESOLUTION.COARSE]: 1, + } + : { + [GRID_RESOLUTION.SUPER_FINE]: 4, + [GRID_RESOLUTION.MOST_FINE]: 3, + [GRID_RESOLUTION.FINE]: 2, + [GRID_RESOLUTION.COARSE]: 1, + }; + } + + _getTicks() { + const scale = this._getScale(); + const unlabeledTicks = [ + { + label: '', + value: scale[GRID_RESOLUTION.FINE], + }, + ]; + if (scale[GRID_RESOLUTION.FINE] !== scale[GRID_RESOLUTION.MOST_FINE]) { + unlabeledTicks.push({ + label: '', + value: scale[GRID_RESOLUTION.MOST_FINE], + }); + } + + return [ + { + label: i18n.translate('xpack.maps.source.esGrid.lowLabel', { + defaultMessage: `low`, + }), + value: scale[GRID_RESOLUTION.COARSE], + }, + ...unlabeledTicks, + { + label: i18n.translate('xpack.maps.source.esGrid.highLabel', { + defaultMessage: `high`, + }), + value: scale[GRID_RESOLUTION.SUPER_FINE], + }, + ]; + } + + _resolutionToSliderValue(resolution: GRID_RESOLUTION): number { + const scale = this._getScale(); + return scale[resolution]; + } + + _sliderValueToResolution(value: number): GRID_RESOLUTION { + const scale = this._getScale(); + const resolution = Object.keys(scale).find((key) => { + return scale[key as GRID_RESOLUTION] === value; + }); + return resolution ? (resolution as GRID_RESOLUTION) : GRID_RESOLUTION.COARSE; + } + _onResolutionChange = (event: ChangeEvent | MouseEvent) => { - const resolution = sliderValueToResolution(parseInt(event.currentTarget.value, 10)); - if (!this.props.isHeatmap && resolution === GRID_RESOLUTION.SUPER_FINE) { + const resolution = this._sliderValueToResolution(parseInt(event.currentTarget.value, 10)); + if (isMvt(this.props.renderAs, resolution)) { const hasUnsupportedMetrics = this.props.metrics.find(isUnsupportedVectorTileMetric); if (hasUnsupportedMetrics) { this.setState({ showModal: true }); @@ -129,11 +159,13 @@ export class ResolutionEditor extends Component { render() { const helpText = - !this.props.isHeatmap && this.props.resolution === GRID_RESOLUTION.SUPER_FINE + (this.props.renderAs === RENDER_AS.POINT || this.props.renderAs === RENDER_AS.GRID) && + this.props.resolution === GRID_RESOLUTION.SUPER_FINE ? i18n.translate('xpack.maps.source.esGrid.superFineHelpText', { defaultMessage: 'High resolution uses vector tiles.', }) : undefined; + const ticks = this._getTicks(); return ( <> {this._renderModal()} @@ -145,28 +177,13 @@ export class ResolutionEditor extends Component { display="columnCompressed" >
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.test.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.test.tsx index 3ddb804cac2131..0df4c492940b75 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.test.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.test.tsx @@ -19,6 +19,7 @@ jest.mock('uuid/v4', () => { const defaultProps = { currentLayerType: LAYER_TYPE.GEOJSON_VECTOR, + geoFieldName: 'myLocation', indexPatternId: 'foobar', onChange: async () => {}, metrics: [], diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.tsx index 4754d26702c9ce..1ef695e9dcfac9 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.tsx @@ -11,7 +11,13 @@ import uuid from 'uuid/v4'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiPanel, EuiSpacer, EuiComboBoxOptionOption, EuiTitle } from '@elastic/eui'; import { getDataViewNotFoundMessage } from '../../../../common/i18n_getters'; -import { AGG_TYPE, GRID_RESOLUTION, LAYER_TYPE, RENDER_AS } from '../../../../common/constants'; +import { + AGG_TYPE, + ES_GEO_FIELD_TYPE, + GRID_RESOLUTION, + LAYER_TYPE, + RENDER_AS, +} from '../../../../common/constants'; import { MetricsEditor } from '../../../components/metrics_editor'; import { getIndexPatternService } from '../../../kibana_services'; import { ResolutionEditor } from './resolution_editor'; @@ -21,9 +27,11 @@ import { RenderAsSelect } from './render_as_select'; import { AggDescriptor } from '../../../../common/descriptor_types'; import { OnSourceChangeArgs } from '../source'; import { clustersTitle, heatmapTitle } from './es_geo_grid_source'; +import { isMvt } from './is_mvt'; interface Props { currentLayerType?: string; + geoFieldName: string; indexPatternId: string; onChange: (...args: OnSourceChangeArgs[]) => Promise; metrics: AggDescriptor[]; @@ -32,6 +40,7 @@ interface Props { } interface State { + geoFieldType?: ES_GEO_FIELD_TYPE; metricsEditorKey: string; fields: IndexPatternField[]; loadError?: string; @@ -70,30 +79,42 @@ export class UpdateSourceEditor extends Component { return; } + const geoField = indexPattern.fields.getByName(this.props.geoFieldName); + this.setState({ fields: indexPattern.fields.filter((field) => !indexPatterns.isNestedField(field)), + geoFieldType: geoField ? (geoField.type as ES_GEO_FIELD_TYPE) : undefined, }); } + _getNewLayerType(renderAs: RENDER_AS, resolution: GRID_RESOLUTION): LAYER_TYPE | undefined { + let nextLayerType: LAYER_TYPE | undefined; + if (renderAs === RENDER_AS.HEATMAP) { + nextLayerType = LAYER_TYPE.HEATMAP; + } else if (isMvt(renderAs, resolution)) { + nextLayerType = LAYER_TYPE.MVT_VECTOR; + } else { + nextLayerType = LAYER_TYPE.GEOJSON_VECTOR; + } + + // only return newLayerType if there is a change from current layer type + return nextLayerType !== undefined && nextLayerType !== this.props.currentLayerType + ? nextLayerType + : undefined; + } + _onMetricsChange = (metrics: AggDescriptor[]) => { this.props.onChange({ propName: 'metrics', value: metrics }); }; _onResolutionChange = async (resolution: GRID_RESOLUTION, metrics: AggDescriptor[]) => { - let newLayerType; - if ( - this.props.currentLayerType === LAYER_TYPE.GEOJSON_VECTOR || - this.props.currentLayerType === LAYER_TYPE.MVT_VECTOR - ) { - newLayerType = - resolution === GRID_RESOLUTION.SUPER_FINE - ? LAYER_TYPE.MVT_VECTOR - : LAYER_TYPE.GEOJSON_VECTOR; - } - await this.props.onChange( { propName: 'metrics', value: metrics }, - { propName: 'resolution', value: resolution, newLayerType } + { + propName: 'resolution', + value: resolution, + newLayerType: this._getNewLayerType(this.props.renderAs, resolution), + } ); // Metrics editor persists metrics in state. @@ -102,7 +123,11 @@ export class UpdateSourceEditor extends Component { }; _onRequestTypeSelect = (requestType: RENDER_AS) => { - this.props.onChange({ propName: 'requestType', value: requestType }); + this.props.onChange({ + propName: 'requestType', + value: requestType, + newLayerType: this._getNewLayerType(requestType, this.props.resolution), + }); }; _getMetricsFilter() { @@ -155,13 +180,14 @@ export class UpdateSourceEditor extends Component { diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts index e1090a16ec6652..27c11d27673f2c 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts @@ -341,7 +341,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource getGeoFieldName(): string { if (!this._descriptor.geoField) { - throw new Error('Should not call'); + throw new Error(`Required field 'geoField' not provided in '_descriptor'`); } return this._descriptor.geoField; } diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index d8197902c73ace..88338dd508eecf 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -23,6 +23,12 @@ export function setStartServices(core: CoreStart, plugins: MapsPluginStartDepend emsSettings = mapsEms.createEMSSettings(); } +let isCloudEnabled = false; +export function setIsCloudEnabled(enabled: boolean) { + isCloudEnabled = enabled; +} +export const getIsCloud = () => isCloudEnabled; + export const getIndexNameFormComponent = () => pluginsStart.fileUpload.IndexNameFormComponent; export const getFileUploadComponent = () => pluginsStart.fileUpload.FileUploadComponent; export const getIndexPatternService = () => pluginsStart.data.indexPatterns; diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 21c33cdcb500ae..bef5cd7039f7e7 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -22,7 +22,7 @@ import type { } from '../../../../src/core/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { MapInspectorView } from './inspector/map_inspector_view'; -import { setMapAppConfig, setStartServices } from './kibana_services'; +import { setIsCloudEnabled, setMapAppConfig, setStartServices } from './kibana_services'; import { featureCatalogueEntry } from './feature_catalogue_entry'; import { getMapsVisTypeAlias } from './maps_vis_type_alias'; import type { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; @@ -74,11 +74,13 @@ import { } from './legacy_visualizations'; import type { SecurityPluginStart } from '../../security/public'; import type { SpacesPluginStart } from '../../spaces/public'; +import type { CloudSetup } from '../../cloud/public'; import type { LensPublicSetup } from '../../lens/public'; import { setupLensChoroplethChart } from './lens'; export interface MapsPluginSetupDependencies { + cloud?: CloudSetup; expressions: ReturnType; inspector: InspectorSetupContract; home?: HomePublicPluginSetup; @@ -193,6 +195,8 @@ export class MapsPlugin plugins.expressions.registerRenderer(tileMapRenderer); plugins.visualizations.createBaseVisualization(tileMapVisType); + setIsCloudEnabled(!!plugins.cloud?.isCloudEnabled); + return { registerLayerWizard: registerLayerWizardExternal, registerSource, diff --git a/x-pack/plugins/maps/server/mvt/get_grid_tile.ts b/x-pack/plugins/maps/server/mvt/get_grid_tile.ts index 28effa5eabfba8..754fdb9c1f4d2a 100644 --- a/x-pack/plugins/maps/server/mvt/get_grid_tile.ts +++ b/x-pack/plugins/maps/server/mvt/get_grid_tile.ts @@ -23,7 +23,7 @@ export async function getEsGridTile({ y, z, requestBody = {}, - requestType = RENDER_AS.POINT, + renderAs = RENDER_AS.POINT, gridPrecision, abortController, }: { @@ -37,7 +37,7 @@ export async function getEsGridTile({ context: DataRequestHandlerContext; logger: Logger; requestBody: any; - requestType: RENDER_AS.GRID | RENDER_AS.POINT; + renderAs: RENDER_AS; gridPrecision: number; abortController: AbortController; }): Promise { @@ -49,7 +49,8 @@ export async function getEsGridTile({ exact_bounds: false, extent: 4096, // full resolution, query: requestBody.query, - grid_type: requestType === RENDER_AS.GRID ? 'grid' : 'centroid', + grid_agg: renderAs === RENDER_AS.HEX ? 'geohex' : 'geotile', + grid_type: renderAs === RENDER_AS.GRID || renderAs === RENDER_AS.HEX ? 'grid' : 'centroid', aggs: requestBody.aggs, fields: requestBody.fields, runtime_mappings: requestBody.runtime_mappings, diff --git a/x-pack/plugins/maps/server/mvt/mvt_routes.ts b/x-pack/plugins/maps/server/mvt/mvt_routes.ts index 5fdaea9ab66dfc..dde68bd0d13359 100644 --- a/x-pack/plugins/maps/server/mvt/mvt_routes.ts +++ b/x-pack/plugins/maps/server/mvt/mvt_routes.ts @@ -88,7 +88,7 @@ export function initMVTRoutes({ geometryFieldName: schema.string(), requestBody: schema.string(), index: schema.string(), - requestType: schema.string(), + renderAs: schema.string(), token: schema.maybe(schema.string()), gridPrecision: schema.number(), }), @@ -114,7 +114,7 @@ export function initMVTRoutes({ z: parseInt((params as any).z, 10) as number, index: query.index as string, requestBody: decodeMvtResponseBody(query.requestBody as string) as any, - requestType: query.requestType as RENDER_AS.POINT | RENDER_AS.GRID, + renderAs: query.renderAs as RENDER_AS, gridPrecision: parseInt(query.gridPrecision, 10), abortController, }); diff --git a/x-pack/plugins/maps/tsconfig.json b/x-pack/plugins/maps/tsconfig.json index ed188c609c330f..fbbc9cae2e3c90 100644 --- a/x-pack/plugins/maps/tsconfig.json +++ b/x-pack/plugins/maps/tsconfig.json @@ -32,6 +32,7 @@ { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../cloud/tsconfig.json" }, { "path": "../features/tsconfig.json" }, { "path": "../lens/tsconfig.json" }, { "path": "../licensing/tsconfig.json" }, diff --git a/x-pack/test/api_integration/apis/maps/get_grid_tile.js b/x-pack/test/api_integration/apis/maps/get_grid_tile.js index a1b420755a31a1..ab8c86215a3a55 100644 --- a/x-pack/test/api_integration/apis/maps/get_grid_tile.js +++ b/x-pack/test/api_integration/apis/maps/get_grid_tile.js @@ -13,16 +13,15 @@ export default function ({ getService }) { const supertest = getService('supertest'); describe('getGridTile', () => { - it('should return vector tile containing cluster features', async () => { - const resp = await supertest - .get( - `/api/maps/mvt/getGridTile/3/2/3.pbf\ + const URL = `/api/maps/mvt/getGridTile/3/2/3.pbf\ ?geometryFieldName=geo.coordinates\ &index=logstash-*\ &gridPrecision=8\ -&requestBody=(_source:(excludes:!()),aggs:(avg_of_bytes:(avg:(field:bytes))),fields:!((field:%27@timestamp%27,format:date_time),(field:%27relatedContent.article:modified_time%27,format:date_time),(field:%27relatedContent.article:published_time%27,format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:%27doc[!%27@timestamp!%27].value.getHour()%27))),size:0,stored_fields:!(%27*%27))\ -&requestType=point` - ) +&requestBody=(_source:(excludes:!()),aggs:(avg_of_bytes:(avg:(field:bytes))),fields:!((field:%27@timestamp%27,format:date_time),(field:%27relatedContent.article:modified_time%27,format:date_time),(field:%27relatedContent.article:published_time%27,format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:%27doc[!%27@timestamp!%27].value.getHour()%27))),size:0,stored_fields:!(%27*%27))`; + + it('should return vector tile with expected headers', async () => { + const resp = await supertest + .get(URL + '&renderAs=point') .set('kbn-xsrf', 'kibana') .responseType('blob') .expect(200); @@ -31,6 +30,14 @@ export default function ({ getService }) { expect(resp.headers['content-disposition']).to.be('inline'); expect(resp.headers['content-type']).to.be('application/x-protobuf'); expect(resp.headers['cache-control']).to.be('public, max-age=3600'); + }); + + it('should return vector tile containing clusters when renderAs is "point"', async () => { + const resp = await supertest + .get(URL + '&renderAs=point') + .set('kbn-xsrf', 'kibana') + .responseType('blob') + .expect(200); const jsonTile = new VectorTile(new Protobuf(resp.body)); @@ -46,59 +53,44 @@ export default function ({ getService }) { _key: '11/517/809', 'avg_of_bytes.value': 9252, }); - expect(clusterFeature.loadGeometry()).to.eql([[{ x: 87, y: 667 }]]); - // Metadata feature - const metaDataLayer = jsonTile.layers.meta; - expect(metaDataLayer.length).to.be(1); - const metadataFeature = metaDataLayer.feature(0); - expect(metadataFeature.type).to.be(3); - expect(metadataFeature.extent).to.be(4096); + // assert feature geometry is weighted centroid + expect(clusterFeature.loadGeometry()).to.eql([[{ x: 87, y: 667 }]]); + }); - expect(metadataFeature.properties['aggregations._count.avg']).to.eql(1); - expect(metadataFeature.properties['aggregations._count.count']).to.eql(1); - expect(metadataFeature.properties['aggregations._count.min']).to.eql(1); - expect(metadataFeature.properties['aggregations._count.sum']).to.eql(1); + it('should return vector tile containing clusters with renderAs is "heatmap"', async () => { + const resp = await supertest + .get(URL + '&renderAs=heatmap') + .set('kbn-xsrf', 'kibana') + .responseType('blob') + .expect(200); - expect(metadataFeature.properties['aggregations.avg_of_bytes.avg']).to.eql(9252); - expect(metadataFeature.properties['aggregations.avg_of_bytes.count']).to.eql(1); - expect(metadataFeature.properties['aggregations.avg_of_bytes.max']).to.eql(9252); - expect(metadataFeature.properties['aggregations.avg_of_bytes.min']).to.eql(9252); - expect(metadataFeature.properties['aggregations.avg_of_bytes.sum']).to.eql(9252); + const jsonTile = new VectorTile(new Protobuf(resp.body)); - expect(metadataFeature.properties['hits.total.relation']).to.eql('eq'); - expect(metadataFeature.properties['hits.total.value']).to.eql(1); + // Cluster feature + const layer = jsonTile.layers.aggs; + expect(layer.length).to.be(1); + const clusterFeature = layer.feature(0); + expect(clusterFeature.type).to.be(1); + expect(clusterFeature.extent).to.be(4096); + expect(clusterFeature.id).to.be(undefined); + expect(clusterFeature.properties).to.eql({ + _count: 1, + _key: '11/517/809', + 'avg_of_bytes.value': 9252, + }); - expect(metadataFeature.loadGeometry()).to.eql([ - [ - { x: 0, y: 4096 }, - { x: 4096, y: 4096 }, - { x: 4096, y: 0 }, - { x: 0, y: 0 }, - { x: 0, y: 4096 }, - ], - ]); + // assert feature geometry is weighted centroid + expect(clusterFeature.loadGeometry()).to.eql([[{ x: 87, y: 667 }]]); }); - it('should return vector tile containing grid features', async () => { + it('should return vector tile containing grid features when renderAs is "grid"', async () => { const resp = await supertest - .get( - `/api/maps/mvt/getGridTile/3/2/3.pbf\ -?geometryFieldName=geo.coordinates\ -&index=logstash-*\ -&gridPrecision=8\ -&requestBody=(_source:(excludes:!()),aggs:(avg_of_bytes:(avg:(field:bytes))),fields:!((field:%27@timestamp%27,format:date_time),(field:%27relatedContent.article:modified_time%27,format:date_time),(field:%27relatedContent.article:published_time%27,format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:%27doc[!%27@timestamp!%27].value.getHour()%27))),size:0,stored_fields:!(%27*%27))\ -&requestType=grid` - ) + .get(URL + '&renderAs=grid') .set('kbn-xsrf', 'kibana') .responseType('blob') .expect(200); - expect(resp.headers['content-encoding']).to.be('gzip'); - expect(resp.headers['content-disposition']).to.be('inline'); - expect(resp.headers['content-type']).to.be('application/x-protobuf'); - expect(resp.headers['cache-control']).to.be('public, max-age=3600'); - const jsonTile = new VectorTile(new Protobuf(resp.body)); const layer = jsonTile.layers.aggs; expect(layer.length).to.be(1); @@ -112,6 +104,8 @@ export default function ({ getService }) { _key: '11/517/809', 'avg_of_bytes.value': 9252, }); + + // assert feature geometry is grid expect(gridFeature.loadGeometry()).to.eql([ [ { x: 80, y: 672 }, @@ -121,6 +115,51 @@ export default function ({ getService }) { { x: 80, y: 672 }, ], ]); + }); + + it('should return vector tile containing hexegon features when renderAs is "hex"', async () => { + const resp = await supertest + .get(URL + '&renderAs=hex') + .set('kbn-xsrf', 'kibana') + .responseType('blob') + .expect(200); + + const jsonTile = new VectorTile(new Protobuf(resp.body)); + const layer = jsonTile.layers.aggs; + expect(layer.length).to.be(1); + + const gridFeature = layer.feature(0); + expect(gridFeature.type).to.be(3); + expect(gridFeature.extent).to.be(4096); + expect(gridFeature.id).to.be(undefined); + expect(gridFeature.properties).to.eql({ + _count: 1, + _key: '85264a33fffffff', + 'avg_of_bytes.value': 9252, + }); + + // assert feature geometry is hex + expect(gridFeature.loadGeometry()).to.eql([ + [ + { x: 102, y: 669 }, + { x: 99, y: 659 }, + { x: 89, y: 657 }, + { x: 83, y: 664 }, + { x: 86, y: 674 }, + { x: 96, y: 676 }, + { x: 102, y: 669 }, + ], + ]); + }); + + it('should return vector tile with meta layer', async () => { + const resp = await supertest + .get(URL + '&renderAs=point') + .set('kbn-xsrf', 'kibana') + .responseType('blob') + .expect(200); + + const jsonTile = new VectorTile(new Protobuf(resp.body)); // Metadata feature const metaDataLayer = jsonTile.layers.meta; diff --git a/x-pack/test/functional/apps/maps/mvt_geotile_grid.js b/x-pack/test/functional/apps/maps/mvt_geotile_grid.js index d56b389b878f32..40dfa5ac8e5719 100644 --- a/x-pack/test/functional/apps/maps/mvt_geotile_grid.js +++ b/x-pack/test/functional/apps/maps/mvt_geotile_grid.js @@ -47,7 +47,7 @@ export default function ({ getPageObjects, getService }) { geometryFieldName: 'geo.coordinates', index: 'logstash-*', gridPrecision: 8, - requestType: 'grid', + renderAs: 'grid', requestBody: `(_source:(excludes:!()),aggs:(max_of_bytes:(max:(field:bytes))),fields:!((field:'@timestamp',format:date_time),(field:'relatedContent.article:modified_time',format:date_time),(field:'relatedContent.article:published_time',format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((range:('@timestamp':(format:strict_date_optional_time,gte:'2015-09-20T00:00:00.000Z',lte:'2015-09-20T01:00:00.000Z')))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:'doc[!'@timestamp!'].value.getHour()'))),size:0,stored_fields:!('*'))`, }); From d7e17d78ebeb653bd4b5cda59df05f370939ab04 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 23 Mar 2022 08:20:45 -0600 Subject: [PATCH 43/64] [lens] include number of values in default terms field label (#127222) * [lens] include number of values in default terms field label * remove size from rare values * revert changes to label with secondary terms * fix jest tests * i18n cleanup, fix lens smokescreen functional test * functional test expects * funcational test expect * handle single value * eslint * revert changes to expect * update functional test expect * functional test expect update * test expects * expects * expects * expects * expects * expects Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../apps/management/_scripted_fields.ts | 4 +- .../droppable/droppable.test.ts | 10 ++-- .../operations/definitions/terms/index.tsx | 59 ++++++++++++++----- .../definitions/terms/terms.test.tsx | 19 +++--- .../operations/layer_helpers.test.ts | 6 +- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../functional/apps/lens/drag_and_drop.ts | 26 ++++---- .../functional/apps/lens/runtime_fields.ts | 4 +- .../test/functional/apps/lens/smokescreen.ts | 2 +- 11 files changed, 80 insertions(+), 53 deletions(-) diff --git a/test/functional/apps/management/_scripted_fields.ts b/test/functional/apps/management/_scripted_fields.ts index c8c605ec7ed19e..a6bbe798cf56b0 100644 --- a/test/functional/apps/management/_scripted_fields.ts +++ b/test/functional/apps/management/_scripted_fields.ts @@ -276,7 +276,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); // verify Lens opens a visualization expect(await testSubjects.getVisibleTextAll('lns-dimensionTrigger')).to.contain( - 'Top values of painString' + 'Top 5 values of painString' ); }); }); @@ -363,7 +363,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); // verify Lens opens a visualization expect(await testSubjects.getVisibleTextAll('lns-dimensionTrigger')).to.contain( - 'Top values of painBool' + 'Top 5 values of painBool' ); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts index ea3978ce8ca94d..778b589d283e10 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts @@ -144,7 +144,7 @@ const multipleColumnsLayer: IndexPatternLayer = { columns: { col1: oneColumnLayer.columns.col1, col2: { - label: 'Top values of src', + label: 'Top 10 values of src', dataType: 'string', isBucketed: true, // Private @@ -157,7 +157,7 @@ const multipleColumnsLayer: IndexPatternLayer = { sourceField: 'src', } as TermsIndexPatternColumn, col3: { - label: 'Top values of dest', + label: 'Top 10 values of dest', dataType: 'string', isBucketed: true, @@ -1620,7 +1620,7 @@ describe('IndexPatternDimensionEditorPanel', () => { col1: testState.layers.first.columns.col1, col2: { - label: 'Top values of src', + label: 'Top 10 values of src', dataType: 'string', isBucketed: true, @@ -2192,7 +2192,7 @@ describe('IndexPatternDimensionEditorPanel', () => { col1: testState.layers.first.columns.col1, col2: { isBucketed: true, - label: 'Top values of bytes', + label: 'Top 10 values of bytes', operationType: 'terms', sourceField: 'bytes', dataType: 'number', @@ -2284,7 +2284,7 @@ describe('IndexPatternDimensionEditorPanel', () => { col1: testState.layers.first.columns.col1, col2: { isBucketed: true, - label: 'Top values of bytes', + label: 'Top 10 values of bytes', operationType: 'terms', sourceField: 'bytes', dataType: 'number', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index fbb990e1dab813..c881bc898e8ad8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -60,7 +60,12 @@ const missingFieldLabel = i18n.translate('xpack.lens.indexPattern.missingFieldLa defaultMessage: 'Missing field', }); -function ofName(name?: string, count: number = 0, rare: boolean = false) { +function ofName( + name?: string, + secondaryFieldsCount: number = 0, + rare: boolean = false, + termsSize: number = 0 +) { if (rare) { return i18n.translate('xpack.lens.indexPattern.rareTermsOf', { defaultMessage: 'Rare values of {name}', @@ -69,19 +74,22 @@ function ofName(name?: string, count: number = 0, rare: boolean = false) { }, }); } - if (count) { + if (secondaryFieldsCount) { return i18n.translate('xpack.lens.indexPattern.multipleTermsOf', { defaultMessage: 'Top values of {name} + {count} {count, plural, one {other} other {others}}', values: { name: name ?? missingFieldLabel, - count, + count: secondaryFieldsCount, }, }); } return i18n.translate('xpack.lens.indexPattern.termsOf', { - defaultMessage: 'Top values of {name}', + defaultMessage: + 'Top {numberOfTermsLabel}{termsCount, plural, one {value} other {values}} of {name}', values: { name: name ?? missingFieldLabel, + termsCount: termsSize, + numberOfTermsLabel: termsSize > 1 ? `${termsSize} ` : '', }, }); } @@ -270,7 +278,8 @@ export const termsOperation: OperationDefinition { const newParams = { @@ -285,6 +294,7 @@ export const termsOperation: OperationDefinition { - updateLayer( - updateColumnParam({ - layer, - columnId, - paramName: 'size', - value, - }) - ); + updateLayer({ + ...layer, + columns: { + ...layer.columns, + [columnId]: { + ...currentColumn, + label: currentColumn.customLabel + ? currentColumn.label + : ofName( + indexPattern.getFieldByName(currentColumn.sourceField)?.displayName, + secondaryFieldsCount, + currentColumn.params.orderBy.type === 'rare', + value + ), + params: { + ...currentColumn.params, + size: value, + }, + }, + } as Record, + }); }} /> {currentColumn.params.orderBy.type === 'rare' && ( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index 0da1f0977e4bcd..a72250c2265c48 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -79,7 +79,7 @@ describe('terms', () => { columnOrder: ['col1', 'col2'], columns: { col1: { - label: 'Top values of source', + label: 'Top 3 values of source', dataType: 'string', isBucketed: true, operationType: 'terms', @@ -199,7 +199,7 @@ describe('terms', () => { const oldColumn: TermsIndexPatternColumn = { operationType: 'terms', sourceField: 'source', - label: 'Top values of source', + label: 'Top 5 values of source', isBucketed: true, dataType: 'string', params: { @@ -226,7 +226,7 @@ describe('terms', () => { const oldColumn: TermsIndexPatternColumn = { operationType: 'terms', sourceField: 'bytes', - label: 'Top values of bytes', + label: 'Top 5 values of bytes', isBucketed: true, dataType: 'number', params: { @@ -254,7 +254,7 @@ describe('terms', () => { const oldColumn: TermsIndexPatternColumn = { operationType: 'terms', sourceField: 'bytes', - label: 'Top values of bytes', + label: 'Top 5 values of bytes', isBucketed: true, dataType: 'number', params: { @@ -278,7 +278,7 @@ describe('terms', () => { const oldColumn: TermsIndexPatternColumn = { operationType: 'terms', sourceField: 'bytes', - label: 'Top values of bytes', + label: 'Top 5 values of bytes', isBucketed: true, dataType: 'number', params: { @@ -304,7 +304,7 @@ describe('terms', () => { const oldColumn: TermsIndexPatternColumn = { operationType: 'terms', sourceField: 'bytes', - label: 'Top values of bytes', + label: 'Top 5 values of bytes', isBucketed: true, dataType: 'number', params: { @@ -327,7 +327,7 @@ describe('terms', () => { const oldColumn: TermsIndexPatternColumn = { operationType: 'terms', sourceField: 'bytes', - label: 'Top values of bytes', + label: 'Top 5 values of bytes', isBucketed: true, dataType: 'number', params: { @@ -929,7 +929,7 @@ describe('terms', () => { createMockedIndexPattern(), {} ) - ).toBe('Top values of source'); + ).toBe('Top 3 values of source'); }); it('should return main value with single counter for two fields', () => { @@ -2083,6 +2083,7 @@ describe('terms', () => { ...layer.columns, col1: { ...layer.columns.col1, + label: 'Top 7 values of source', params: { ...(layer.columns.col1 as TermsIndexPatternColumn).params, size: 7, @@ -2101,7 +2102,7 @@ describe('terms', () => { col1: { dataType: 'boolean', isBucketed: true, - label: 'Top values of bytes', + label: 'Top 5 values of bytes', operationType: 'terms', params: { missingBucket: false, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index b6398970056e28..15cfcda26d9178 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -767,7 +767,7 @@ describe('state_helpers', () => { }).columns.col2 ).toEqual( expect.objectContaining({ - label: 'Top values of bytes', + label: 'Top 3 values of bytes', }) ); }); @@ -1079,7 +1079,7 @@ describe('state_helpers', () => { }).columns.col1 ).toEqual( expect.objectContaining({ - label: 'Top values of source', + label: 'Top 3 values of source', }) ); }); @@ -2251,7 +2251,7 @@ describe('state_helpers', () => { it('should remove column and any incomplete state', () => { const termsColumn: TermsIndexPatternColumn = { - label: 'Top values of source', + label: 'Top 5 values of source', dataType: 'string', isBucketed: true, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index ac95693301e545..8914efcf12ded4 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -553,7 +553,6 @@ "xpack.lens.indexPattern.terms.otherBucketDescription": "Regrouper les autres valeurs sous \"Autre\"", "xpack.lens.indexPattern.terms.otherLabel": "Autre", "xpack.lens.indexPattern.terms.size": "Nombre de valeurs", - "xpack.lens.indexPattern.termsOf": "Valeurs les plus élevées de {name}", "xpack.lens.indexPattern.termsWithMultipleShifts": "Dans un seul calque, il est impossible de combiner des indicateurs avec des décalages temporels différents et des valeurs dynamiques les plus élevées. Utilisez la même valeur de décalage pour tous les indicateurs, ou utilisez des filtres à la place des valeurs les plus élevées.", "xpack.lens.indexPattern.termsWithMultipleShiftsFixActionLabel": "Utiliser des filtres", "xpack.lens.indexPattern.timeScale.enableTimeScale": "Normaliser par unité", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2337f25502da36..48f0d74d73765c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -654,7 +654,6 @@ "xpack.lens.indexPattern.terms.size": "値の数", "xpack.lens.indexPattern.terms.sizeLimitMax": "値が最大値{max}を超えています。最大値が使用されます。", "xpack.lens.indexPattern.terms.sizeLimitMin": "値が最小値{min}未満です。最小値が使用されます。", - "xpack.lens.indexPattern.termsOf": "{name} のトップの値", "xpack.lens.indexPattern.termsWithMultipleShifts": "単一のレイヤーでは、メトリックを異なる時間シフトと動的な上位の値と組み合わせることができません。すべてのメトリックで同じ時間シフト値を使用するか、上位の値ではなくフィルターを使用します。", "xpack.lens.indexPattern.termsWithMultipleShiftsFixActionLabel": "フィルターを使用", "xpack.lens.indexPattern.termsWithMultipleTermsAndScriptedFields": "複数のフィールドを使用するときには、スクリプトフィールドがサポートされていません。{fields}が見つかりました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 158534694943a4..bbc00d8d205f7a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -660,7 +660,6 @@ "xpack.lens.indexPattern.terms.size": "值数目", "xpack.lens.indexPattern.terms.sizeLimitMax": "值大于最大值 {max},将改为使用最大值。", "xpack.lens.indexPattern.terms.sizeLimitMin": "值小于最小值 {min},将改为使用最小值。", - "xpack.lens.indexPattern.termsOf": "{name} 排名最前值", "xpack.lens.indexPattern.termsWithMultipleShifts": "在单个图层中,无法将指标与不同时间偏移和动态排名最前值组合。将相同的时间偏移值用于所有指标或使用筛选,而非排名最前值。", "xpack.lens.indexPattern.termsWithMultipleShiftsFixActionLabel": "使用筛选", "xpack.lens.indexPattern.termsWithMultipleTermsAndScriptedFields": "使用多个字段时不支持脚本字段,找到 {fields}", diff --git a/x-pack/test/functional/apps/lens/drag_and_drop.ts b/x-pack/test/functional/apps/lens/drag_and_drop.ts index 27e336a1cbc127..1a7b8e96d68026 100644 --- a/x-pack/test/functional/apps/lens/drag_and_drop.ts +++ b/x-pack/test/functional/apps/lens/drag_and_drop.ts @@ -33,7 +33,7 @@ export default function ({ getPageObjects }: FtrProviderContext) { 'lnsDatatable_rows > lns-dimensionTrigger' ); expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_rows')).to.eql( - 'Top values of clientip' + 'Top 3 values of clientip' ); await PageObjects.lens.dragFieldToDimensionTrigger( @@ -48,7 +48,7 @@ export default function ({ getPageObjects }: FtrProviderContext) { 'lnsDatatable_rows > lns-empty-dimension' ); expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_rows', 2)).to.eql( - 'Top values of @message.raw' + 'Top 3 values of @message.raw' ); }); @@ -56,8 +56,8 @@ export default function ({ getPageObjects }: FtrProviderContext) { await PageObjects.lens.reorderDimensions('lnsDatatable_rows', 3, 1); await PageObjects.lens.waitForVisualization(); expect(await PageObjects.lens.getDimensionTriggersTexts('lnsDatatable_rows')).to.eql([ - 'Top values of @message.raw', - 'Top values of clientip', + 'Top 3 values of @message.raw', + 'Top 3 values of clientip', 'bytes', ]); }); @@ -65,11 +65,11 @@ export default function ({ getPageObjects }: FtrProviderContext) { it('should move the column to compatible dimension group', async () => { await PageObjects.lens.switchToVisualization('bar'); expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql([ - 'Top values of @message.raw', + 'Top 3 values of @message.raw', ]); expect( await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') - ).to.eql(['Top values of clientip']); + ).to.eql(['Top 3 values of clientip']); await PageObjects.lens.dragDimensionToDimension( 'lnsXY_xDimensionPanel > lns-dimensionTrigger', @@ -81,13 +81,13 @@ export default function ({ getPageObjects }: FtrProviderContext) { ); expect( await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') - ).to.eql(['Top values of @message.raw']); + ).to.eql(['Top 3 values of @message.raw']); }); it('should move the column to non-compatible dimension group', async () => { expect( await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') - ).to.eql(['Top values of @message.raw']); + ).to.eql(['Top 3 values of @message.raw']); await PageObjects.lens.dragDimensionToDimension( 'lnsXY_splitDimensionPanel > lns-dimensionTrigger', @@ -129,7 +129,7 @@ export default function ({ getPageObjects }: FtrProviderContext) { 'Unique count of @message.raw [1]', ]); expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql([ - 'Top values of @message.raw', + 'Top 5 values of @message.raw', ]); }); @@ -159,7 +159,7 @@ export default function ({ getPageObjects }: FtrProviderContext) { 'Unique count of @timestamp' ); expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_splitDimensionPanel')).to.eql( - 'Top values of @message.raw' + 'Top 3 values of @message.raw' ); }); @@ -244,14 +244,14 @@ export default function ({ getPageObjects }: FtrProviderContext) { await PageObjects.lens.dragFieldWithKeyboard('@message.raw', 1, true); expect( await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') - ).to.eql(['Top values of @message.raw']); + ).to.eql(['Top 3 values of @message.raw']); await PageObjects.lens.assertFocusedField('@message.raw'); }); it('should drop a field to an existing dimension replacing the old one', async () => { await PageObjects.lens.dragFieldWithKeyboard('clientip', 1, true); expect( await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') - ).to.eql(['Top values of clientip']); + ).to.eql(['Top 3 values of clientip']); await PageObjects.lens.assertFocusedField('clientip'); }); @@ -319,7 +319,7 @@ export default function ({ getPageObjects }: FtrProviderContext) { await PageObjects.lens.waitForVisualization(); expect( await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') - ).to.eql(['Top values of clientip']); + ).to.eql(['Top 3 values of clientip']); await PageObjects.lens.openDimensionEditor( 'lnsXY_splitDimensionPanel > lns-dimensionTrigger' ); diff --git a/x-pack/test/functional/apps/lens/runtime_fields.ts b/x-pack/test/functional/apps/lens/runtime_fields.ts index 1353bcaea2c848..252951cba4bd09 100644 --- a/x-pack/test/functional/apps/lens/runtime_fields.ts +++ b/x-pack/test/functional/apps/lens/runtime_fields.ts @@ -33,7 +33,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); await PageObjects.lens.waitForVisualization(); expect(await PageObjects.lens.getDatatableHeaderText(0)).to.equal( - 'Top values of runtimefield' + 'Top 5 values of runtimefield' ); expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('abc'); }); @@ -62,7 +62,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); await PageObjects.lens.waitForVisualization(); expect(await PageObjects.lens.getDatatableHeaderText(0)).to.equal( - 'Top values of runtimefield2' + 'Top 5 values of runtimefield2' ); expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('abc'); }); diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 4887f96c6870a5..f0cc3b0da72014 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -337,7 +337,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await PageObjects.lens.getLayerCount()).to.eql(1); expect(await PageObjects.lens.getDimensionTriggerText('lnsPie_sliceByDimensionPanel')).to.eql( - 'Top values of geo.dest' + 'Top 5 values of geo.dest' ); expect(await PageObjects.lens.getDimensionTriggerText('lnsPie_sizeByDimensionPanel')).to.eql( 'Average of bytes' From b028cf97ed9a554fef723be1f38ffabbfc41bac1 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Wed, 23 Mar 2022 10:28:43 -0400 Subject: [PATCH 44/64] [ResponseOps][task manager] log event loop delay for tasks when over configured limit (#126300) resolves https://github.com/elastic/kibana/issues/124366 Adds new task manager configuration keys. - `xpack.task_manager.event_loop_delay.monitor` - whether to monitor event loop delay or not; added in case this specific monitoring causes other issues and we'd want to disable it. We don't know of any cases where we'd need this today - `xpack.task_manager.event_loop_delay.warn_threshold` - the number of milliseconds of event loop delay before logging a warning This code uses the `perf_hooks.monitorEventLoopDelay()` API[1] to collect the event loop delay while a task is running. [1] https://nodejs.org/api/perf_hooks.html#perf_hooksmonitoreventloopdelayoptions When a significant event loop delay is encountered, it's very likely that other tasks running at the same time will be affected, and so will also end up having a long event loop delay value, and warnings will be logged on those. Over time, though, tasks which have consistently long event loop delays will outnumber those unfortunate peer tasks, and be obvious from the volume in the logs. To make it a bit easier to find these when viewing Kibana logs in Discover, tags are added to the logged messages to make it easier to find them. One tag is `event-loop-blocked`, second is the task type, and the third is a string consisting of the task type and task id. --- docs/settings/task-manager-settings.asciidoc | 5 ++ .../resources/base/bin/kibana-docker | 2 + .../task_manager/server/config.test.ts | 12 +++ x-pack/plugins/task_manager/server/config.ts | 10 +++ .../server/ephemeral_task_lifecycle.test.ts | 4 + .../managed_configuration.test.ts | 4 + .../configuration_statistics.test.ts | 4 + .../monitoring_stats_stream.test.ts | 4 + .../task_manager/server/plugin.test.ts | 12 +++ .../server/polling_lifecycle.test.ts | 4 + .../task_manager/server/polling_lifecycle.ts | 3 + .../task_manager/server/task_events.test.ts | 82 +++++++++++++++++++ .../task_manager/server/task_events.ts | 20 +++++ .../server/task_running/task_runner.test.ts | 10 ++- .../server/task_running/task_runner.ts | 21 ++++- 15 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/task_manager/server/task_events.test.ts diff --git a/docs/settings/task-manager-settings.asciidoc b/docs/settings/task-manager-settings.asciidoc index b7423d7c37b310..5f31c9adc879d2 100644 --- a/docs/settings/task-manager-settings.asciidoc +++ b/docs/settings/task-manager-settings.asciidoc @@ -40,6 +40,11 @@ These non-persisted action tasks have a risk that they won't be run at all if th `xpack.task_manager.ephemeral_tasks.request_capacity`:: Sets the size of the ephemeral queue defined above. Defaults to 10. +`xpack.task_manager.event_loop_delay.monitor`:: +Enables event loop delay monitoring, which will log a warning when a task causes an event loop delay which exceeds the `warn_threshold` setting. Defaults to true. + +`xpack.task_manager.event_loop_delay.warn_threshold`:: +Sets the amount of event loop delay during a task execution which will cause a warning to be logged. Defaults to 5000 milliseconds (5 seconds). [float] [[task-manager-health-settings]] 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 01d27a345378bf..83a542c93d12b9 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 @@ -379,6 +379,8 @@ kibana_vars=( xpack.task_manager.poll_interval xpack.task_manager.request_capacity xpack.task_manager.version_conflict_threshold + xpack.task_manager.event_loop_delay.monitor + xpack.task_manager.event_loop_delay.warn_threshold xpack.uptime.index ) diff --git a/x-pack/plugins/task_manager/server/config.test.ts b/x-pack/plugins/task_manager/server/config.test.ts index 4c4db2aba71285..f5ba0a3bcee0a7 100644 --- a/x-pack/plugins/task_manager/server/config.test.ts +++ b/x-pack/plugins/task_manager/server/config.test.ts @@ -16,6 +16,10 @@ describe('config validation', () => { "enabled": false, "request_capacity": 10, }, + "event_loop_delay": Object { + "monitor": true, + "warn_threshold": 5000, + }, "max_attempts": 3, "max_poll_inactivity_cycles": 10, "max_workers": 10, @@ -62,6 +66,10 @@ describe('config validation', () => { "enabled": false, "request_capacity": 10, }, + "event_loop_delay": Object { + "monitor": true, + "warn_threshold": 5000, + }, "max_attempts": 3, "max_poll_inactivity_cycles": 10, "max_workers": 10, @@ -106,6 +114,10 @@ describe('config validation', () => { "enabled": false, "request_capacity": 10, }, + "event_loop_delay": Object { + "monitor": true, + "warn_threshold": 5000, + }, "max_attempts": 3, "max_poll_inactivity_cycles": 10, "max_workers": 10, diff --git a/x-pack/plugins/task_manager/server/config.ts b/x-pack/plugins/task_manager/server/config.ts index 5a58e45a70d96d..f650ed093cee0c 100644 --- a/x-pack/plugins/task_manager/server/config.ts +++ b/x-pack/plugins/task_manager/server/config.ts @@ -41,6 +41,14 @@ export const taskExecutionFailureThresholdSchema = schema.object( } ); +const eventLoopDelaySchema = schema.object({ + monitor: schema.boolean({ defaultValue: true }), + warn_threshold: schema.number({ + defaultValue: 5000, + min: 10, + }), +}); + export const configSchema = schema.object( { /* The maximum number of times a task will be attempted before being abandoned as failed */ @@ -118,6 +126,7 @@ export const configSchema = schema.object( max: DEFAULT_MAX_EPHEMERAL_REQUEST_CAPACITY, }), }), + event_loop_delay: eventLoopDelaySchema, /* These are not designed to be used by most users. Please use caution when changing these */ unsafe: schema.object({ exclude_task_types: schema.arrayOf(schema.string(), { defaultValue: [] }), @@ -138,3 +147,4 @@ export const configSchema = schema.object( export type TaskManagerConfig = TypeOf; export type TaskExecutionFailureThreshold = TypeOf; +export type EventLoopDelayConfig = TypeOf; diff --git a/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts b/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts index 639bb834eeb4c7..1d98e37a06a551 100644 --- a/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts +++ b/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts @@ -69,6 +69,10 @@ describe('EphemeralTaskLifecycle', () => { unsafe: { exclude_task_types: [], }, + event_loop_delay: { + monitor: true, + warn_threshold: 5000, + }, ...config, }, elasticsearchAndSOAvailability$, diff --git a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts index 3442e69aab44a6..c5f03b17693857 100644 --- a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts +++ b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts @@ -57,6 +57,10 @@ describe.skip('managed configuration', () => { unsafe: { exclude_task_types: [], }, + event_loop_delay: { + monitor: true, + warn_threshold: 5000, + }, }); logger = context.logger.get('taskManager'); diff --git a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts index 77fd9a8f11fabe..776f5bc9388f7b 100644 --- a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts @@ -40,6 +40,10 @@ describe('Configuration Statistics Aggregator', () => { unsafe: { exclude_task_types: [], }, + event_loop_delay: { + monitor: true, + warn_threshold: 5000, + }, }; const managedConfig = { diff --git a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts index 8aa2d54d896238..a6ef665966ddd7 100644 --- a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts @@ -44,6 +44,10 @@ describe('createMonitoringStatsStream', () => { unsafe: { exclude_task_types: [], }, + event_loop_delay: { + monitor: true, + warn_threshold: 5000, + }, }; it('returns the initial config used to configure Task Manager', async () => { diff --git a/x-pack/plugins/task_manager/server/plugin.test.ts b/x-pack/plugins/task_manager/server/plugin.test.ts index 20e5f211a5b4ed..aa91533eabadf9 100644 --- a/x-pack/plugins/task_manager/server/plugin.test.ts +++ b/x-pack/plugins/task_manager/server/plugin.test.ts @@ -43,6 +43,10 @@ describe('TaskManagerPlugin', () => { unsafe: { exclude_task_types: [], }, + event_loop_delay: { + monitor: true, + warn_threshold: 5000, + }, }); pluginInitializerContext.env.instanceUuid = ''; @@ -84,6 +88,10 @@ describe('TaskManagerPlugin', () => { unsafe: { exclude_task_types: [], }, + event_loop_delay: { + monitor: true, + warn_threshold: 5000, + }, }); const taskManagerPlugin = new TaskManagerPlugin(pluginInitializerContext); @@ -154,6 +162,10 @@ describe('TaskManagerPlugin', () => { unsafe: { exclude_task_types: ['*'], }, + event_loop_delay: { + monitor: true, + warn_threshold: 5000, + }, }); const logger = pluginInitializerContext.logger.get(); diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts index cf29d1f475c6ce..7cbaa5a1655449 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts @@ -67,6 +67,10 @@ describe('TaskPollingLifecycle', () => { unsafe: { exclude_task_types: [], }, + event_loop_delay: { + monitor: true, + warn_threshold: 5000, + }, }, taskStore: mockTaskStore, logger: taskManagerLogger, diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.ts index a452c8a3f82fbe..ee7e2ec32932ee 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.ts @@ -91,6 +91,7 @@ export class TaskPollingLifecycle { private middleware: Middleware; private usageCounter?: UsageCounter; + private config: TaskManagerConfig; /** * Initializes the task manager, preventing any further addition of middleware, @@ -117,6 +118,7 @@ export class TaskPollingLifecycle { this.store = taskStore; this.executionContext = executionContext; this.usageCounter = usageCounter; + this.config = config; const emitEvent = (event: TaskLifecycleEvent) => this.events$.next(event); @@ -240,6 +242,7 @@ export class TaskPollingLifecycle { defaultMaxAttempts: this.taskClaiming.maxAttempts, executionContext: this.executionContext, usageCounter: this.usageCounter, + eventLoopDelayConfig: { ...this.config.event_loop_delay }, }); }; diff --git a/x-pack/plugins/task_manager/server/task_events.test.ts b/x-pack/plugins/task_manager/server/task_events.test.ts new file mode 100644 index 00000000000000..5d72120da725ca --- /dev/null +++ b/x-pack/plugins/task_manager/server/task_events.test.ts @@ -0,0 +1,82 @@ +/* + * 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 { startTaskTimer, startTaskTimerWithEventLoopMonitoring } from './task_events'; + +const DelayIterations = 4; +const DelayMillis = 250; +const DelayTotal = DelayIterations * DelayMillis; + +async function nonBlockingDelay(millis: number) { + await new Promise((resolve) => setTimeout(resolve, millis)); +} + +async function blockingDelay(millis: number) { + // get task in async queue + await nonBlockingDelay(0); + + const end = Date.now() + millis; + // eslint-disable-next-line no-empty + while (Date.now() < end) {} +} + +async function nonBlockingTask() { + for (let i = 0; i < DelayIterations; i++) { + await nonBlockingDelay(DelayMillis); + } +} + +async function blockingTask() { + for (let i = 0; i < DelayIterations; i++) { + await blockingDelay(DelayMillis); + } +} + +describe('task_events', () => { + test('startTaskTimer', async () => { + const stopTaskTimer = startTaskTimer(); + await nonBlockingTask(); + const result = stopTaskTimer(); + expect(result.stop - result.start).not.toBeLessThan(DelayTotal); + expect(result.eventLoopBlockMs).toBe(undefined); + }); + + describe('startTaskTimerWithEventLoopMonitoring', () => { + test('non-blocking', async () => { + const stopTaskTimer = startTaskTimerWithEventLoopMonitoring({ + monitor: true, + warn_threshold: 5000, + }); + await nonBlockingTask(); + const result = stopTaskTimer(); + expect(result.stop - result.start).not.toBeLessThan(DelayTotal); + expect(result.eventLoopBlockMs).toBeLessThan(DelayMillis); + }); + + test('blocking', async () => { + const stopTaskTimer = startTaskTimerWithEventLoopMonitoring({ + monitor: true, + warn_threshold: 5000, + }); + await blockingTask(); + const result = stopTaskTimer(); + expect(result.stop - result.start).not.toBeLessThan(DelayTotal); + expect(result.eventLoopBlockMs).not.toBeLessThan(DelayMillis); + }); + + test('not monitoring', async () => { + const stopTaskTimer = startTaskTimerWithEventLoopMonitoring({ + monitor: false, + warn_threshold: 5000, + }); + await blockingTask(); + const result = stopTaskTimer(); + expect(result.stop - result.start).not.toBeLessThan(DelayTotal); + expect(result.eventLoopBlockMs).toBe(0); + }); + }); +}); diff --git a/x-pack/plugins/task_manager/server/task_events.ts b/x-pack/plugins/task_manager/server/task_events.ts index 7c7845569a10b5..de2c9dc04acd27 100644 --- a/x-pack/plugins/task_manager/server/task_events.ts +++ b/x-pack/plugins/task_manager/server/task_events.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { monitorEventLoopDelay } from 'perf_hooks'; + import { Option } from 'fp-ts/lib/Option'; import { ConcreteTaskInstance } from './task'; @@ -14,6 +16,7 @@ import { ClaimAndFillPoolResult } from './lib/fill_pool'; import { PollingError } from './polling'; import { TaskRunResult } from './task_running'; import { EphemeralTaskInstanceRequest } from './ephemeral_task_lifecycle'; +import type { EventLoopDelayConfig } from './config'; export enum TaskPersistence { Recurring = 'recurring', @@ -40,6 +43,7 @@ export enum TaskClaimErrorType { export interface TaskTiming { start: number; stop: number; + eventLoopBlockMs?: number; } export type WithTaskTiming = T & { timing: TaskTiming }; @@ -48,6 +52,22 @@ export function startTaskTimer(): () => TaskTiming { return () => ({ start, stop: Date.now() }); } +export function startTaskTimerWithEventLoopMonitoring( + eventLoopDelayConfig: EventLoopDelayConfig +): () => TaskTiming { + const stopTaskTimer = startTaskTimer(); + const eldHistogram = eventLoopDelayConfig.monitor ? monitorEventLoopDelay() : null; + eldHistogram?.enable(); + + return () => { + const { start, stop } = stopTaskTimer(); + eldHistogram?.disable(); + const eldMax = eldHistogram?.max ?? 0; + const eventLoopBlockMs = Math.round(eldMax / 1000 / 1000); // original in nanoseconds + return { start, stop, eventLoopBlockMs }; + }; +} + export interface TaskEvent { id?: ID; timing?: TaskTiming; diff --git a/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts b/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts index 09af125884fe92..ece82099728e3b 100644 --- a/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts +++ b/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts @@ -1528,7 +1528,11 @@ describe('TaskManagerRunner', () => { function withAnyTiming(taskRun: TaskRun) { return { ...taskRun, - timing: { start: expect.any(Number), stop: expect.any(Number) }, + timing: { + start: expect.any(Number), + stop: expect.any(Number), + eventLoopBlockMs: expect.any(Number), + }, }; } @@ -1590,6 +1594,10 @@ describe('TaskManagerRunner', () => { onTaskEvent: opts.onTaskEvent, executionContext, usageCounter, + eventLoopDelayConfig: { + monitor: true, + warn_threshold: 5000, + }, }); if (stage === TaskRunningStage.READY_TO_RUN) { diff --git a/x-pack/plugins/task_manager/server/task_running/task_runner.ts b/x-pack/plugins/task_manager/server/task_running/task_runner.ts index 48927435c4bdf9..778a834c168a14 100644 --- a/x-pack/plugins/task_manager/server/task_running/task_runner.ts +++ b/x-pack/plugins/task_manager/server/task_running/task_runner.ts @@ -38,7 +38,7 @@ import { TaskMarkRunning, asTaskRunEvent, asTaskMarkRunningEvent, - startTaskTimer, + startTaskTimerWithEventLoopMonitoring, TaskTiming, TaskPersistence, } from '../task_events'; @@ -56,6 +56,7 @@ import { } from '../task'; import { TaskTypeDictionary } from '../task_type_dictionary'; import { isUnrecoverableError } from './errors'; +import type { EventLoopDelayConfig } from '../config'; const defaultBackoffPerFailure = 5 * 60 * 1000; export const EMPTY_RUN_RESULT: SuccessfulRunResult = { state: {} }; @@ -105,6 +106,7 @@ type Opts = { defaultMaxAttempts: number; executionContext: ExecutionContextStart; usageCounter?: UsageCounter; + eventLoopDelayConfig: EventLoopDelayConfig; } & Pick; export enum TaskRunResult { @@ -152,6 +154,7 @@ export class TaskManagerRunner implements TaskRunner { private uuid: string; private readonly executionContext: ExecutionContextStart; private usageCounter?: UsageCounter; + private eventLoopDelayConfig: EventLoopDelayConfig; /** * Creates an instance of TaskManagerRunner. @@ -174,6 +177,7 @@ export class TaskManagerRunner implements TaskRunner { onTaskEvent = identity, executionContext, usageCounter, + eventLoopDelayConfig, }: Opts) { this.instance = asPending(sanitizeInstance(instance)); this.definitions = definitions; @@ -186,6 +190,7 @@ export class TaskManagerRunner implements TaskRunner { this.executionContext = executionContext; this.usageCounter = usageCounter; this.uuid = uuid.v4(); + this.eventLoopDelayConfig = eventLoopDelayConfig; } /** @@ -292,7 +297,7 @@ export class TaskManagerRunner implements TaskRunner { taskInstance: this.instance.task, }); - const stopTaskTimer = startTaskTimer(); + const stopTaskTimer = startTaskTimerWithEventLoopMonitoring(this.eventLoopDelayConfig); try { this.task = this.definition.createTaskRunner(modifiedContext); @@ -617,6 +622,18 @@ export class TaskManagerRunner implements TaskRunner { ); } ); + + const { eventLoopBlockMs = 0 } = taskTiming; + const taskLabel = `${this.taskType} ${this.instance.task.id}`; + if (eventLoopBlockMs > this.eventLoopDelayConfig.warn_threshold) { + this.logger.warn( + `event loop blocked for at least ${eventLoopBlockMs} ms while running task ${taskLabel}`, + { + tags: [this.taskType, taskLabel, 'event-loop-blocked'], + } + ); + } + return result; } From e6f225bc56890c3bfd4992549c09f92a638ffb5c Mon Sep 17 00:00:00 2001 From: Giorgos Bamparopoulos Date: Wed, 23 Mar 2022 14:32:39 +0000 Subject: [PATCH 45/64] Add loading state to the SparkPlot (#128196) --- .../error_group_list/index.tsx | 11 +- .../app/error_group_overview/index.tsx | 7 +- .../app/service_inventory/index.tsx | 4 + .../service_inventory/service_list/index.tsx | 9 + .../service_list/service_list.test.tsx | 10 ++ .../app/service_map/popover/stats_list.tsx | 1 + .../get_columns.tsx | 3 + .../service_overview_errors_table/index.tsx | 5 + ...ice_overview_instances_chart_and_table.tsx | 90 +++++----- .../get_columns.tsx | 7 + .../index.tsx | 3 + .../shared/charts/spark_plot/index.tsx | 161 ++++++++++++------ .../shared/dependencies_table/index.tsx | 12 ++ .../shared/transactions_table/get_columns.tsx | 5 + .../shared/transactions_table/index.tsx | 8 +- 15 files changed, 236 insertions(+), 100 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/error_group_list/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/error_group_list/index.tsx index 7a54a633e7f154..6e86c8c9517103 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/error_group_list/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/error_group_list/index.tsx @@ -59,6 +59,7 @@ type ErrorGroupDetailedStatistics = interface Props { mainStatistics: ErrorGroupItem[]; serviceName: string; + detailedStatisticsLoading: boolean; detailedStatistics: ErrorGroupDetailedStatistics; comparisonEnabled?: boolean; } @@ -66,6 +67,7 @@ interface Props { function ErrorGroupList({ mainStatistics, serviceName, + detailedStatisticsLoading, detailedStatistics, comparisonEnabled, }: Props) { @@ -210,6 +212,7 @@ function ErrorGroupList({ return ( >; - }, [serviceName, query, detailedStatistics, comparisonEnabled]); + }, [ + serviceName, + query, + detailedStatistics, + comparisonEnabled, + detailedStatisticsLoading, + ]); return ( { if (requestId && errorGroupMainStatistics.length && start && end) { @@ -189,6 +190,10 @@ export function ErrorGroupOverview() { diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx index c26ae5a273b4ee..807a848d649ea5 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx @@ -191,6 +191,10 @@ export function ServiceInventory() { isLoading={isLoading} isFailure={isFailure} items={items} + comparisonDataLoading={ + comparisonFetch.status === FETCH_STATUS.LOADING || + comparisonFetch.status === FETCH_STATUS.NOT_INITIATED + } comparisonData={comparisonFetch?.data} noItemsMessage={noItemsMessage} /> diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx index 2d01a11d92186f..cc43be6a790ea8 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx @@ -64,6 +64,7 @@ const SERVICE_HEALTH_STATUS_ORDER = [ export function getServiceColumns({ query, showTransactionTypeColumn, + comparisonDataLoading, comparisonData, breakpoints, showHealthStatusColumn, @@ -71,6 +72,7 @@ export function getServiceColumns({ query: TypeOf['query']; showTransactionTypeColumn: boolean; showHealthStatusColumn: boolean; + comparisonDataLoading: boolean; breakpoints: Breakpoints; comparisonData?: ServicesDetailedStatisticsAPIResponse; }): Array> { @@ -162,6 +164,7 @@ export function getServiceColumns({ ); return ( { describe('when small', () => { it('shows environment, transaction type and sparklines', () => { const renderedColumns = getServiceColumns({ + comparisonDataLoading: false, showHealthStatusColumn: true, query, showTransactionTypeColumn: true, @@ -96,6 +97,7 @@ describe('ServiceList', () => { color="green" comparisonSeriesColor="black" hideSeries={false} + isLoading={false} valueLabel="0 ms" /> `); @@ -105,6 +107,7 @@ describe('ServiceList', () => { describe('when Large', () => { it('hides environment, transaction type and sparklines', () => { const renderedColumns = getServiceColumns({ + comparisonDataLoading: false, showHealthStatusColumn: true, query, showTransactionTypeColumn: true, @@ -122,6 +125,7 @@ describe('ServiceList', () => { color="green" comparisonSeriesColor="black" hideSeries={true} + isLoading={false} valueLabel="0 ms" /> `); @@ -130,6 +134,7 @@ describe('ServiceList', () => { describe('when XL', () => { it('hides transaction type', () => { const renderedColumns = getServiceColumns({ + comparisonDataLoading: false, showHealthStatusColumn: true, query, showTransactionTypeColumn: true, @@ -156,6 +161,7 @@ describe('ServiceList', () => { color="green" comparisonSeriesColor="black" hideSeries={false} + isLoading={false} valueLabel="0 ms" /> `); @@ -165,6 +171,7 @@ describe('ServiceList', () => { describe('when XXL', () => { it('hides transaction type', () => { const renderedColumns = getServiceColumns({ + comparisonDataLoading: false, showHealthStatusColumn: true, query, showTransactionTypeColumn: true, @@ -192,6 +199,7 @@ describe('ServiceList', () => { color="green" comparisonSeriesColor="black" hideSeries={false} + isLoading={false} valueLabel="0 ms" /> `); @@ -203,6 +211,7 @@ describe('ServiceList', () => { describe('without ML data', () => { it('hides healthStatus column', () => { const renderedColumns = getServiceColumns({ + comparisonDataLoading: false, showHealthStatusColumn: false, query, showTransactionTypeColumn: true, @@ -219,6 +228,7 @@ describe('ServiceList', () => { describe('with ML data', () => { it('shows healthStatus column', () => { const renderedColumns = getServiceColumns({ + comparisonDataLoading: false, showHealthStatusColumn: true, query, showTransactionTypeColumn: true, diff --git a/x-pack/plugins/apm/public/components/app/service_map/popover/stats_list.tsx b/x-pack/plugins/apm/public/components/app/service_map/popover/stats_list.tsx index 7cc0e158fe52d1..e5ed89571165e0 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/popover/stats_list.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/popover/stats_list.tsx @@ -198,6 +198,7 @@ export function StatsList({ data, isLoading }: StatsListProps) { {timeseries ? ( ['query']; @@ -129,6 +131,7 @@ export function getColumns({ return ( { if (requestId && items.length && start && end) { @@ -176,8 +177,12 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { { preservePreviousData: false } ); + const errorGroupDetailedStatisticsLoading = + errorGroupDetailedStatisticsStatus === FETCH_STATUS.LOADING; + const columns = getColumns({ serviceName, + errorGroupDetailedStatisticsLoading, errorGroupDetailedStatistics, comparisonEnabled, query, diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx index dfea13eaaf476b..bbe94f8e8aae86 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx @@ -172,50 +172,52 @@ export function ServiceOverviewInstancesChartAndTable({ direction ).slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE); - const { data: detailedStatsData = INITIAL_STATE_DETAILED_STATISTICS } = - useFetcher( - (callApmApi) => { - if ( - !start || - !end || - !transactionType || - !latencyAggregationType || - !currentPeriodItemsCount - ) { - return; - } + const { + data: detailedStatsData = INITIAL_STATE_DETAILED_STATISTICS, + status: detailedStatsStatus, + } = useFetcher( + (callApmApi) => { + if ( + !start || + !end || + !transactionType || + !latencyAggregationType || + !currentPeriodItemsCount + ) { + return; + } - return callApmApi( - 'GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics', - { - params: { - path: { - serviceName, - }, - query: { - environment, - kuery, - latencyAggregationType: - latencyAggregationType as LatencyAggregationType, - start, - end, - numBuckets: 20, - transactionType, - serviceNodeIds: JSON.stringify( - currentPeriodOrderedItems.map((item) => item.serviceNodeName) - ), - comparisonStart, - comparisonEnd, - }, + return callApmApi( + 'GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics', + { + params: { + path: { + serviceName, }, - } - ); - }, - // only fetches detailed statistics when requestId is invalidated by main statistics api call - // eslint-disable-next-line react-hooks/exhaustive-deps - [requestId], - { preservePreviousData: false } - ); + query: { + environment, + kuery, + latencyAggregationType: + latencyAggregationType as LatencyAggregationType, + start, + end, + numBuckets: 20, + transactionType, + serviceNodeIds: JSON.stringify( + currentPeriodOrderedItems.map((item) => item.serviceNodeName) + ), + comparisonStart, + comparisonEnd, + }, + }, + } + ); + }, + // only fetches detailed statistics when requestId is invalidated by main statistics api call + // eslint-disable-next-line react-hooks/exhaustive-deps + [requestId], + { preservePreviousData: false } + ); return ( <> @@ -233,6 +235,10 @@ export function ServiceOverviewInstancesChartAndTable({ mainStatsItems={currentPeriodOrderedItems} mainStatsStatus={mainStatsStatus} mainStatsItemCount={currentPeriodItemsCount} + detailedStatsLoading={ + detailedStatsStatus === FETCH_STATUS.LOADING || + detailedStatsStatus === FETCH_STATUS.NOT_INITIATED + } detailedStatsData={detailedStatsData} serviceName={serviceName} tableOptions={tableOptions} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx index 0b6e846f952399..26a117224ace12 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx @@ -48,6 +48,7 @@ export function getColumns({ kuery, agentName, latencyAggregationType, + detailedStatsLoading, detailedStatsData, comparisonEnabled, toggleRowDetails, @@ -60,6 +61,7 @@ export function getColumns({ kuery: string; agentName?: string; latencyAggregationType?: LatencyAggregationType; + detailedStatsLoading: boolean; detailedStatsData?: ServiceInstanceDetailedStatistics; comparisonEnabled?: boolean; toggleRowDetails: (selectedServiceNodeName: string) => void; @@ -125,6 +127,7 @@ export function getColumns({ color={currentPeriodColor} valueLabel={asMillisecondDuration(latency)} hideSeries={!shouldShowSparkPlots} + isLoading={detailedStatsLoading} series={currentPeriodTimestamp} comparisonSeries={ comparisonEnabled ? previousPeriodTimestamp : undefined @@ -158,6 +161,7 @@ export function getColumns({ color={currentPeriodColor} hideSeries={!shouldShowSparkPlots} valueLabel={asTransactionRate(throughput)} + isLoading={detailedStatsLoading} series={currentPeriodTimestamp} comparisonSeries={ comparisonEnabled ? previousPeriodTimestamp : undefined @@ -191,6 +195,7 @@ export function getColumns({ color={currentPeriodColor} hideSeries={!shouldShowSparkPlots} valueLabel={asPercent(errorRate, 1)} + isLoading={detailedStatsLoading} series={currentPeriodTimestamp} comparisonSeries={ comparisonEnabled ? previousPeriodTimestamp : undefined @@ -224,6 +229,7 @@ export function getColumns({ color={currentPeriodColor} hideSeries={!shouldShowSparkPlots} valueLabel={asPercent(cpuUsage, 1)} + isLoading={detailedStatsLoading} series={currentPeriodTimestamp} comparisonSeries={ comparisonEnabled ? previousPeriodTimestamp : undefined @@ -257,6 +263,7 @@ export function getColumns({ color={currentPeriodColor} hideSeries={!shouldShowSparkPlots} valueLabel={asPercent(memoryUsage, 1)} + isLoading={detailedStatsLoading} series={currentPeriodTimestamp} comparisonSeries={ comparisonEnabled ? previousPeriodTimestamp : undefined diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx index c12b0a19644f50..b49208e2cdde72 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx @@ -53,6 +53,7 @@ interface Props { page?: { index: number }; sort?: { field: string; direction: SortDirection }; }) => void; + detailedStatsLoading: boolean; detailedStatsData?: ServiceInstanceDetailedStatistics; isLoading: boolean; isNotInitiated: boolean; @@ -64,6 +65,7 @@ export function ServiceOverviewInstancesTable({ mainStatsStatus: status, tableOptions, onChangeTableOptions, + detailedStatsLoading, detailedStatsData: detailedStatsData, isLoading, isNotInitiated, @@ -124,6 +126,7 @@ export function ServiceOverviewInstancesTable({ serviceName, kuery, latencyAggregationType: latencyAggregationType as LatencyAggregationType, + detailedStatsLoading, detailedStatsData, comparisonEnabled, toggleRowDetails, diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx index 325eb3d12f8994..c497d35ed2cf64 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx @@ -14,7 +14,12 @@ import { ScaleType, Settings, } from '@elastic/charts'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLoadingChart, +} from '@elastic/eui'; import React from 'react'; import { useChartTheme } from '../../../../../../observability/public'; import { Coordinate } from '../../../../../typings/timeseries'; @@ -32,6 +37,7 @@ const flexGroupStyle = { overflow: 'hidden' }; export function SparkPlot({ color, + isLoading, series, comparisonSeries = [], valueLabel, @@ -39,11 +45,52 @@ export function SparkPlot({ comparisonSeriesColor, }: { color: string; + isLoading: boolean; series?: Coordinate[] | null; valueLabel: React.ReactNode; compact?: boolean; comparisonSeries?: Coordinate[]; comparisonSeriesColor: string; +}) { + return ( + + + {valueLabel} + + + + + + ); +} + +function SparkPlotItem({ + color, + isLoading, + series, + comparisonSeries, + comparisonSeriesColor, + compact, +}: { + color: string; + isLoading: boolean; + series?: Coordinate[] | null; + compact?: boolean; + comparisonSeries?: Coordinate[]; + comparisonSeriesColor: string; }) { const theme = useTheme(); const defaultChartTheme = useChartTheme(); @@ -68,61 +115,65 @@ export function SparkPlot({ const Sparkline = hasComparisonSeries ? LineSeries : AreaSeries; + if (isLoading) { + return ( +
+ +
+ ); + } + + if (hasValidTimeseries(series)) { + return ( + + + + {hasComparisonSeries && ( + + )} + + ); + } + return ( - - - {valueLabel} - - - {hasValidTimeseries(series) ? ( - - - - {hasComparisonSeries && ( - - )} - - ) : ( -
- -
- )} -
-
+ +
); } diff --git a/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx index 0986c8fe587de8..a0dba6f5b870b4 100644 --- a/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx @@ -97,6 +97,10 @@ export function DependenciesTable(props: Props) { compact color={currentPeriodColor} hideSeries={!shouldShowSparkPlots} + isLoading={ + status === FETCH_STATUS.LOADING || + status === FETCH_STATUS.NOT_INITIATED + } series={currentStats.latency.timeseries} comparisonSeries={previousStats?.latency.timeseries} valueLabel={asMillisecondDuration(currentStats.latency.value)} @@ -122,6 +126,10 @@ export function DependenciesTable(props: Props) { compact color={currentPeriodColor} hideSeries={!shouldShowSparkPlots} + isLoading={ + status === FETCH_STATUS.LOADING || + status === FETCH_STATUS.NOT_INITIATED + } series={currentStats.throughput.timeseries} comparisonSeries={previousStats?.throughput.timeseries} valueLabel={asTransactionRate(currentStats.throughput.value)} @@ -168,6 +176,10 @@ export function DependenciesTable(props: Props) { compact color={currentPeriodColor} hideSeries={!shouldShowSparkPlots} + isLoading={ + status === FETCH_STATUS.LOADING || + status === FETCH_STATUS.NOT_INITIATED + } series={currentStats.errorRate.timeseries} comparisonSeries={previousStats?.errorRate.timeseries} valueLabel={asPercent(currentStats.errorRate.value, 1)} diff --git a/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx b/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx index ecfe277247d4c4..054514f430a077 100644 --- a/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx +++ b/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx @@ -46,6 +46,7 @@ type TransactionGroupDetailedStatistics = export function getColumns({ serviceName, latencyAggregationType, + transactionGroupDetailedStatisticsLoading, transactionGroupDetailedStatistics, comparisonEnabled, shouldShowSparkPlots = true, @@ -53,6 +54,7 @@ export function getColumns({ }: { serviceName: string; latencyAggregationType?: LatencyAggregationType; + transactionGroupDetailedStatisticsLoading: boolean; transactionGroupDetailedStatistics?: TransactionGroupDetailedStatistics; comparisonEnabled?: boolean; shouldShowSparkPlots?: boolean; @@ -106,6 +108,7 @@ export function getColumns({ color={currentPeriodColor} compact hideSeries={!shouldShowSparkPlots} + isLoading={transactionGroupDetailedStatisticsLoading} series={currentTimeseries} comparisonSeries={ comparisonEnabled ? previousTimeseries : undefined @@ -140,6 +143,7 @@ export function getColumns({ color={currentPeriodColor} compact hideSeries={!shouldShowSparkPlots} + isLoading={transactionGroupDetailedStatisticsLoading} series={currentTimeseries} comparisonSeries={ comparisonEnabled ? previousTimeseries : undefined @@ -196,6 +200,7 @@ export function getColumns({ color={currentPeriodColor} compact hideSeries={!shouldShowSparkPlots} + isLoading={transactionGroupDetailedStatisticsLoading} series={currentTimeseries} comparisonSeries={ comparisonEnabled ? previousTimeseries : undefined diff --git a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx index 6134f9c3cdcb13..66f068f6cb05c0 100644 --- a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx @@ -182,7 +182,10 @@ export function TransactionsTable({ }, } = data; - const { data: transactionGroupDetailedStatistics } = useFetcher( + const { + data: transactionGroupDetailedStatistics, + status: transactionGroupDetailedStatisticsStatus, + } = useFetcher( (callApmApi) => { if ( transactionGroupsTotalItems && @@ -225,6 +228,9 @@ export function TransactionsTable({ const columns = getColumns({ serviceName, latencyAggregationType: latencyAggregationType as LatencyAggregationType, + transactionGroupDetailedStatisticsLoading: + transactionGroupDetailedStatisticsStatus === FETCH_STATUS.LOADING || + transactionGroupDetailedStatisticsStatus === FETCH_STATUS.NOT_INITIATED, transactionGroupDetailedStatistics, comparisonEnabled, shouldShowSparkPlots, From d48b82ad5b2f156f0c9c4740f6caaa7cdeff3f29 Mon Sep 17 00:00:00 2001 From: Faisal Kanout Date: Wed, 23 Mar 2022 17:49:05 +0300 Subject: [PATCH 46/64] [RAC][APM] Add "View in App URL" {{context.viewInAppUrl}} variable to the rule templating language (#128243) * Add viewInAppUrl variable * export getAlertUrl as common function * add getAlertUrl to the error rule type * Add viewInAppUrl for Error rule type * Fix tests mocking * Add viewInAppUrl to tracation duration * Add viewInAppUrl for transaction duration * Add viewInAppUrl to anomolay alert * Fix funxtion arg * Add viewInAppUrl to TransactionDurationAnomaly * Get all related code to use the shared functions * Add/Fix tests * Update file name to snack case * Add comment * Fix lint * Remove join * Fix basePath mock * Code Review - refactor foramtting functions * Remove comment * fix typo --- .../apm/common/utils/formatters/alert_url.ts | 42 ++++++++++ .../apm/common/utils/formatters/index.ts | 1 + .../alerting/register_apm_alerts.ts | 81 +++++++------------ x-pack/plugins/apm/server/plugin.ts | 1 + .../server/routes/alerts/action_variables.ts | 10 +++ .../routes/alerts/register_apm_alerts.ts | 3 +- .../register_error_count_alert_type.test.ts | 6 ++ .../alerts/register_error_count_alert_type.ts | 19 ++++- ...er_transaction_duration_alert_type.test.ts | 2 + ...egister_transaction_duration_alert_type.ts | 18 +++++ ...action_duration_anomaly_alert_type.test.ts | 2 + ...transaction_duration_anomaly_alert_type.ts | 18 +++++ ..._transaction_error_rate_alert_type.test.ts | 2 + ...ister_transaction_error_rate_alert_type.ts | 16 ++++ .../server/routes/alerts/test_utils/index.ts | 7 +- 15 files changed, 172 insertions(+), 56 deletions(-) create mode 100644 x-pack/plugins/apm/common/utils/formatters/alert_url.ts diff --git a/x-pack/plugins/apm/common/utils/formatters/alert_url.ts b/x-pack/plugins/apm/common/utils/formatters/alert_url.ts new file mode 100644 index 00000000000000..a88f69b4ef5c73 --- /dev/null +++ b/x-pack/plugins/apm/common/utils/formatters/alert_url.ts @@ -0,0 +1,42 @@ +/* + * 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 { stringify } from 'querystring'; +import { ENVIRONMENT_ALL } from '../../environment_filter_values'; + +const format = ({ + pathname, + query, +}: { + pathname: string; + query: Record; +}): string => { + return `${pathname}?${stringify(query)}`; +}; + +export const getAlertUrlErrorCount = ( + serviceName: string, + serviceEnv: string | undefined +) => + format({ + pathname: `/app/apm/services/${serviceName}/errors`, + query: { + environment: serviceEnv ?? ENVIRONMENT_ALL.value, + }, + }); +// This formatter is for TransactionDuration, TransactionErrorRate, and TransactionDurationAnomaly. +export const getAlertUrlTransaction = ( + serviceName: string, + serviceEnv: string | undefined, + transactionType: string +) => + format({ + pathname: `/app/apm/services/${serviceName}`, + query: { + transactionType, + environment: serviceEnv ?? ENVIRONMENT_ALL.value, + }, + }); diff --git a/x-pack/plugins/apm/common/utils/formatters/index.ts b/x-pack/plugins/apm/common/utils/formatters/index.ts index 1a431867308b6b..f510a54b37102d 100644 --- a/x-pack/plugins/apm/common/utils/formatters/index.ts +++ b/x-pack/plugins/apm/common/utils/formatters/index.ts @@ -9,3 +9,4 @@ export * from './formatters'; export * from './datetime'; export * from './duration'; export * from './size'; +export * from './alert_url'; diff --git a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts index 3be124573728b1..692165f2b2ff50 100644 --- a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts +++ b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts @@ -7,10 +7,12 @@ import { i18n } from '@kbn/i18n'; import { lazy } from 'react'; -import { stringify } from 'querystring'; import { ALERT_REASON } from '@kbn/rule-data-utils'; import type { ObservabilityRuleTypeRegistry } from '../../../../observability/public'; -import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; +import { + getAlertUrlErrorCount, + getAlertUrlTransaction, +} from '../../../common/utils/formatters'; import { AlertType } from '../../../common/alert_types'; // copied from elasticsearch_fieldnames.ts to limit page load bundle size @@ -18,16 +20,6 @@ const SERVICE_ENVIRONMENT = 'service.environment'; const SERVICE_NAME = 'service.name'; const TRANSACTION_TYPE = 'transaction.type'; -const format = ({ - pathname, - query, -}: { - pathname: string; - query: Record; -}): string => { - return `${pathname}?${stringify(query)}`; -}; - export function registerApmAlerts( observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry ) { @@ -40,16 +32,10 @@ export function registerApmAlerts( format: ({ fields }) => { return { reason: fields[ALERT_REASON]!, - link: format({ - pathname: `/app/apm/services/${String( - fields[SERVICE_NAME][0] - )}/errors`, - query: { - ...(fields[SERVICE_ENVIRONMENT]?.[0] - ? { environment: String(fields[SERVICE_ENVIRONMENT][0]) } - : { environment: ENVIRONMENT_ALL.value }), - }, - }), + link: getAlertUrlErrorCount( + String(fields[SERVICE_NAME][0]!), + fields[SERVICE_ENVIRONMENT] && String(fields[SERVICE_ENVIRONMENT][0]) + ), }; }, iconClass: 'bell', @@ -83,19 +69,16 @@ export function registerApmAlerts( 'Alert when the latency of a specific transaction type in a service exceeds a defined threshold.', } ), - format: ({ fields, formatters: { asDuration } }) => ({ - reason: fields[ALERT_REASON]!, - - link: format({ - pathname: `/app/apm/services/${fields[SERVICE_NAME][0]!}`, - query: { - transactionType: fields[TRANSACTION_TYPE][0]!, - ...(fields[SERVICE_ENVIRONMENT]?.[0] - ? { environment: String(fields[SERVICE_ENVIRONMENT][0]) } - : { environment: ENVIRONMENT_ALL.value }), - }, - }), - }), + format: ({ fields, formatters: { asDuration } }) => { + return { + reason: fields[ALERT_REASON]!, + link: getAlertUrlTransaction( + String(fields[SERVICE_NAME][0]!), + fields[SERVICE_ENVIRONMENT] && String(fields[SERVICE_ENVIRONMENT][0]), + String(fields[TRANSACTION_TYPE][0]!) + ), + }; + }, iconClass: 'bell', documentationUrl(docLinks) { return `${docLinks.links.alerting.apmRules}`; @@ -132,15 +115,11 @@ export function registerApmAlerts( ), format: ({ fields, formatters: { asPercent } }) => ({ reason: fields[ALERT_REASON]!, - link: format({ - pathname: `/app/apm/services/${String(fields[SERVICE_NAME][0]!)}`, - query: { - transactionType: String(fields[TRANSACTION_TYPE][0]!), - ...(fields[SERVICE_ENVIRONMENT]?.[0] - ? { environment: String(fields[SERVICE_ENVIRONMENT][0]) } - : { environment: ENVIRONMENT_ALL.value }), - }, - }), + link: getAlertUrlTransaction( + String(fields[SERVICE_NAME][0]!), + fields[SERVICE_ENVIRONMENT] && String(fields[SERVICE_ENVIRONMENT][0]), + String(fields[TRANSACTION_TYPE][0]!) + ), }), iconClass: 'bell', documentationUrl(docLinks) { @@ -177,15 +156,11 @@ export function registerApmAlerts( ), format: ({ fields }) => ({ reason: fields[ALERT_REASON]!, - link: format({ - pathname: `/app/apm/services/${String(fields[SERVICE_NAME][0])}`, - query: { - transactionType: String(fields[TRANSACTION_TYPE][0]), - ...(fields[SERVICE_ENVIRONMENT]?.[0] - ? { environment: String(fields[SERVICE_ENVIRONMENT][0]) } - : { environment: ENVIRONMENT_ALL.value }), - }, - }), + link: getAlertUrlTransaction( + String(fields[SERVICE_NAME][0]!), + fields[SERVICE_ENVIRONMENT] && String(fields[SERVICE_ENVIRONMENT][0]), + String(fields[TRANSACTION_TYPE][0]!) + ), }), iconClass: 'bell', documentationUrl(docLinks) { diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index 2dda29019239ac..4e603741ea2a51 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -194,6 +194,7 @@ export class APMPlugin ml: plugins.ml, config$, logger: this.logger!.get('rule'), + basePath: core.http.basePath, }); } diff --git a/x-pack/plugins/apm/server/routes/alerts/action_variables.ts b/x-pack/plugins/apm/server/routes/alerts/action_variables.ts index 540cd9ffd49464..ce78dbc7bee6d8 100644 --- a/x-pack/plugins/apm/server/routes/alerts/action_variables.ts +++ b/x-pack/plugins/apm/server/routes/alerts/action_variables.ts @@ -65,4 +65,14 @@ export const apmActionVariables = { ), name: 'reason' as const, }, + viewInAppUrl: { + description: i18n.translate( + 'xpack.apm.alerts.action_variables.viewInAppUrl', + { + defaultMessage: + 'Link to the view or feature within Elastic that can be used to investigate the alert and its context further', + } + ), + name: 'viewInAppUrl' as const, + }, }; diff --git a/x-pack/plugins/apm/server/routes/alerts/register_apm_alerts.ts b/x-pack/plugins/apm/server/routes/alerts/register_apm_alerts.ts index db79b4f11df29e..4556abfea1ee5e 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_apm_alerts.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_apm_alerts.ts @@ -6,7 +6,7 @@ */ import { Observable } from 'rxjs'; -import { Logger } from 'kibana/server'; +import { IBasePath, Logger } from 'kibana/server'; import { PluginSetupContract as AlertingPluginSetupContract } from '../../../../alerting/server'; import { IRuleDataClient } from '../../../../rule_registry/server'; import { registerTransactionDurationAlertType } from './register_transaction_duration_alert_type'; @@ -22,6 +22,7 @@ export interface RegisterRuleDependencies { alerting: AlertingPluginSetupContract; config$: Observable; logger: Logger; + basePath: IBasePath; } export function registerApmAlerts(dependencies: RegisterRuleDependencies) { diff --git a/x-pack/plugins/apm/server/routes/alerts/register_error_count_alert_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/register_error_count_alert_type.test.ts index 175b87f7943b0e..3125791e7853b4 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_error_count_alert_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_error_count_alert_type.test.ts @@ -144,6 +144,8 @@ describe('Error count alert', () => { triggerValue: 5, reason: 'Error count is 5 in the last 5 mins for foo. Alert when > 2.', interval: '5m', + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo', }); expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { serviceName: 'foo', @@ -152,6 +154,8 @@ describe('Error count alert', () => { triggerValue: 4, reason: 'Error count is 4 in the last 5 mins for foo. Alert when > 2.', interval: '5m', + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/foo/errors?environment=env-foo-2', }); expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { serviceName: 'bar', @@ -160,6 +164,8 @@ describe('Error count alert', () => { threshold: 2, triggerValue: 3, interval: '5m', + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/bar/errors?environment=env-bar', }); }); }); diff --git a/x-pack/plugins/apm/server/routes/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/routes/alerts/register_error_count_alert_type.ts index f5df3c946f46e0..5fc32ea363bc6d 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_error_count_alert_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_error_count_alert_type.ts @@ -18,6 +18,7 @@ import { getEnvironmentEsField, getEnvironmentLabel, } from '../../../common/environment_filter_values'; +import { getAlertUrlErrorCount } from '../../../common/utils/formatters'; import { AlertType, APM_SERVER_FEATURE_ID, @@ -52,6 +53,7 @@ export function registerErrorCountAlertType({ logger, ruleDataClient, config$, + basePath, }: RegisterRuleDependencies) { const createLifecycleRuleType = createLifecycleRuleTypeFactory({ ruleDataClient, @@ -75,6 +77,7 @@ export function registerErrorCountAlertType({ apmActionVariables.triggerValue, apmActionVariables.interval, apmActionVariables.reason, + apmActionVariables.viewInAppUrl, ], }, producer: APM_SERVER_FEATURE_ID, @@ -83,11 +86,11 @@ export function registerErrorCountAlertType({ executor: async ({ services, params }) => { const config = await config$.pipe(take(1)).toPromise(); const ruleParams = params; + const indices = await getApmIndices({ config, savedObjectsClient: services.savedObjectsClient, }); - const searchParams = { index: indices.error, size: 0, @@ -147,6 +150,19 @@ export function registerErrorCountAlertType({ windowSize: ruleParams.windowSize, windowUnit: ruleParams.windowUnit, }); + + const relativeViewInAppUrl = getAlertUrlErrorCount( + serviceName, + getEnvironmentEsField(environment)?.[SERVICE_ENVIRONMENT] + ); + + const viewInAppUrl = basePath.publicBaseUrl + ? new URL( + basePath.prepend(relativeViewInAppUrl), + basePath.publicBaseUrl + ).toString() + : relativeViewInAppUrl; + services .alertWithLifecycle({ id: [AlertType.ErrorCount, serviceName, environment] @@ -168,6 +184,7 @@ export function registerErrorCountAlertType({ triggerValue: errorCount, interval: `${ruleParams.windowSize}${ruleParams.windowUnit}`, reason: alertReason, + viewInAppUrl, }); }); diff --git a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_alert_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_alert_type.test.ts index 6a3feed69c19a1..57b596bf94087f 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_alert_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_alert_type.test.ts @@ -57,6 +57,8 @@ describe('registerTransactionDurationAlertType', () => { interval: `5m`, reason: 'Avg. latency is 5,500 ms in the last 5 mins for opbeans-java. Alert when > 3,000 ms.', + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/opbeans-java?transactionType=request&environment=ENVIRONMENT_ALL', }); }); }); diff --git a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_alert_type.ts index 4567670129720b..bfbb2a99c662cc 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_alert_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_alert_type.ts @@ -13,6 +13,7 @@ import { ALERT_REASON, } from '@kbn/rule-data-utils'; import { take } from 'rxjs/operators'; +import { getAlertUrlTransaction } from '../../../common/utils/formatters'; import { asDuration } from '../../../../observability/common/utils/formatters'; import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; import { SearchAggregatedTransactionSetting } from '../../../common/aggregated_transactions'; @@ -26,6 +27,7 @@ import { PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_TYPE, + SERVICE_ENVIRONMENT, } from '../../../common/elasticsearch_fieldnames'; import { getEnvironmentEsField, @@ -64,6 +66,7 @@ export function registerTransactionDurationAlertType({ ruleDataClient, config$, logger, + basePath, }: RegisterRuleDependencies) { const createLifecycleRuleType = createLifecycleRuleTypeFactory({ ruleDataClient, @@ -87,6 +90,7 @@ export function registerTransactionDurationAlertType({ apmActionVariables.triggerValue, apmActionVariables.interval, apmActionVariables.reason, + apmActionVariables.viewInAppUrl, ], }, producer: APM_SERVER_FEATURE_ID, @@ -188,6 +192,19 @@ export function registerTransactionDurationAlertType({ windowSize: ruleParams.windowSize, windowUnit: ruleParams.windowUnit, }); + + const relativeViewInAppUrl = getAlertUrlTransaction( + ruleParams.serviceName, + getEnvironmentEsField(ruleParams.environment)?.[SERVICE_ENVIRONMENT], + ruleParams.transactionType + ); + + const viewInAppUrl = basePath.publicBaseUrl + ? new URL( + basePath.prepend(relativeViewInAppUrl), + basePath.publicBaseUrl + ).toString() + : relativeViewInAppUrl; services .alertWithLifecycle({ id: `${AlertType.TransactionDuration}_${getEnvironmentLabel( @@ -211,6 +228,7 @@ export function registerTransactionDurationAlertType({ triggerValue: transactionDurationFormatted, interval: `${ruleParams.windowSize}${ruleParams.windowUnit}`, reason: reasonMessage, + viewInAppUrl, }); } diff --git a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts index 585fadc348700d..2bb8530ca03f6e 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts @@ -201,6 +201,8 @@ describe('Transaction duration anomaly alert', () => { triggerValue: 'critical', reason: 'critical anomaly with a score of 80 was detected in the last 5 mins for foo.', + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/foo?transactionType=type-foo&environment=development', }); }); }); diff --git a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.ts index fbdd7f5e33f0a5..64f06c9f638f18 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -22,7 +22,9 @@ import { PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_TYPE, + SERVICE_ENVIRONMENT, } from '../../../common/elasticsearch_fieldnames'; +import { getAlertUrlTransaction } from '../../../common/utils/formatters'; import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { ANOMALY_SEVERITY } from '../../../common/ml_constants'; import { KibanaRequest } from '../../../../../../src/core/server'; @@ -63,6 +65,7 @@ export function registerTransactionDurationAnomalyAlertType({ ruleDataClient, alerting, ml, + basePath, }: RegisterRuleDependencies) { const createLifecycleRuleType = createLifecycleRuleTypeFactory({ logger, @@ -86,6 +89,7 @@ export function registerTransactionDurationAnomalyAlertType({ apmActionVariables.threshold, apmActionVariables.triggerValue, apmActionVariables.reason, + apmActionVariables.viewInAppUrl, ], }, producer: 'apm', @@ -218,6 +222,19 @@ export function registerTransactionDurationAnomalyAlertType({ windowSize: params.windowSize, windowUnit: params.windowUnit, }); + + const relativeViewInAppUrl = getAlertUrlTransaction( + serviceName, + getEnvironmentEsField(environment)?.[SERVICE_ENVIRONMENT], + transactionType + ); + + const viewInAppUrl = basePath.publicBaseUrl + ? new URL( + basePath.prepend(relativeViewInAppUrl), + basePath.publicBaseUrl + ).toString() + : relativeViewInAppUrl; services .alertWithLifecycle({ id: [ @@ -246,6 +263,7 @@ export function registerTransactionDurationAnomalyAlertType({ threshold: selectedOption?.label, triggerValue: severityLevel, reason: reasonMessage, + viewInAppUrl, }); }); diff --git a/x-pack/plugins/apm/server/routes/alerts/register_transaction_error_rate_alert_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/register_transaction_error_rate_alert_type.test.ts index 36ec8e6ce205fe..d3a024ec92a736 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_transaction_error_rate_alert_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_transaction_error_rate_alert_type.test.ts @@ -129,6 +129,8 @@ describe('Transaction error rate alert', () => { threshold: 10, triggerValue: '10', interval: '5m', + viewInAppUrl: + 'http://localhost:5601/eyr/app/apm/services/foo?transactionType=type-foo&environment=env-foo', }); }); }); diff --git a/x-pack/plugins/apm/server/routes/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/routes/alerts/register_transaction_error_rate_alert_type.ts index 0f68e74a2a9bce..219f992ad15be2 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_transaction_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_transaction_error_rate_alert_type.ts @@ -17,6 +17,7 @@ import { getEnvironmentEsField, getEnvironmentLabel, } from '../../../common/environment_filter_values'; +import { getAlertUrlTransaction } from '../../../common/utils/formatters'; import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; import { AlertType, @@ -60,6 +61,7 @@ export function registerTransactionErrorRateAlertType({ ruleDataClient, logger, config$, + basePath, }: RegisterRuleDependencies) { const createLifecycleRuleType = createLifecycleRuleTypeFactory({ ruleDataClient, @@ -84,6 +86,7 @@ export function registerTransactionErrorRateAlertType({ apmActionVariables.triggerValue, apmActionVariables.interval, apmActionVariables.reason, + apmActionVariables.viewInAppUrl, ], }, producer: APM_SERVER_FEATURE_ID, @@ -207,6 +210,18 @@ export function registerTransactionErrorRateAlertType({ windowSize: ruleParams.windowSize, windowUnit: ruleParams.windowUnit, }); + + const relativeViewInAppUrl = getAlertUrlTransaction( + serviceName, + getEnvironmentEsField(environment)?.[SERVICE_ENVIRONMENT], + transactionType + ); + const viewInAppUrl = basePath.publicBaseUrl + ? new URL( + basePath.prepend(relativeViewInAppUrl), + basePath.publicBaseUrl + ).toString() + : relativeViewInAppUrl; services .alertWithLifecycle({ id: [ @@ -235,6 +250,7 @@ export function registerTransactionErrorRateAlertType({ triggerValue: asDecimalOrInteger(errorRate), interval: `${ruleParams.windowSize}${ruleParams.windowUnit}`, reason: reasonMessage, + viewInAppUrl, }); }); diff --git a/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts b/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts index a34b3cdb1334df..71a4e0d3d111ec 100644 --- a/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts +++ b/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Logger } from 'kibana/server'; +import { IBasePath, Logger } from 'kibana/server'; import { of } from 'rxjs'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { IRuleDataClient } from '../../../../../rule_registry/server'; @@ -56,6 +56,11 @@ export const createRuleTypeMocks = () => { ruleDataClient: ruleRegistryMocks.createRuleDataClient( '.alerts-observability.apm.alerts' ) as IRuleDataClient, + basePath: { + serverBasePath: '/eyr', + publicBaseUrl: 'http://localhost:5601/eyr', + prepend: (path: string) => `http://localhost:5601/eyr${path}`, + } as IBasePath, }, services, scheduleActions, From c55bb917fab509df7922ac727d4246514dbd7a59 Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Wed, 23 Mar 2022 08:23:22 -0700 Subject: [PATCH 47/64] [Security team: AWP] Session view: Alert details tab (#127500) * alerts tab work. list view done * View mode toggle + group view implemented * tests written * clean up * addressed @opauloh comments * fixed weird bug due to importing assests from a test into its component * empty state added for alerts tab * react-query caching keys updated to include sessionEntityId * rule_registry added as a dependency in order to use AlertsClient in alerts_route.ts * fixed build/test errors due to merge. events route now orders by process.start then @timestamp * plumbing for the alert details tie in done. * removed rule_registry ecs mappings. kqualters PR will add this. * alerts index merge conflict fix Co-authored-by: mitodrummer --- .../plugins/session_view/common/constants.ts | 3 +- x-pack/plugins/session_view/kibana.json | 7 +- .../detail_panel_alert_actions/index.test.tsx | 88 ++++++ .../detail_panel_alert_actions/index.tsx | 105 ++++++++ .../detail_panel_alert_actions/styles.ts | 107 ++++++++ .../detail_panel_alert_group_item/index.tsx | 84 ++++++ .../detail_panel_alert_list_item/index.tsx | 137 ++++++++++ .../detail_panel_alert_list_item/styles.ts | 112 ++++++++ .../detail_panel_alert_tab/index.test.tsx | 251 ++++++++++++++++++ .../detail_panel_alert_tab/index.tsx | 146 ++++++++++ .../detail_panel_alert_tab/styles.ts | 43 +++ .../public/components/process_tree/hooks.ts | 13 + .../components/process_tree/index.test.tsx | 5 +- .../public/components/process_tree/index.tsx | 12 +- .../process_tree_alert/index.test.tsx | 10 +- .../components/process_tree_alert/index.tsx | 12 +- .../process_tree_alerts/index.test.tsx | 2 +- .../components/process_tree_alerts/index.tsx | 9 +- .../process_tree_node/index.test.tsx | 2 +- .../components/process_tree_node/index.tsx | 48 +++- .../public/components/session_view/hooks.ts | 35 ++- .../public/components/session_view/index.tsx | 34 ++- .../public/components/session_view/styles.ts | 11 + .../session_view_detail_panel/index.test.tsx | 48 +++- .../session_view_detail_panel/index.tsx | 71 +++-- x-pack/plugins/session_view/server/plugin.ts | 12 +- .../server/routes/alerts_route.test.ts | 133 ++++++++++ .../server/routes/alerts_route.ts | 66 +++++ .../session_view/server/routes/index.ts | 5 +- .../server/routes/process_events_route.ts | 28 +- x-pack/plugins/session_view/server/types.ts | 15 +- x-pack/plugins/session_view/tsconfig.json | 3 +- 32 files changed, 1555 insertions(+), 102 deletions(-) create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.test.tsx create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_actions/styles.ts create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/styles.ts create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.test.tsx create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_tab/styles.ts create mode 100644 x-pack/plugins/session_view/server/routes/alerts_route.test.ts create mode 100644 x-pack/plugins/session_view/server/routes/alerts_route.ts diff --git a/x-pack/plugins/session_view/common/constants.ts b/x-pack/plugins/session_view/common/constants.ts index 42e1d33ab6dba5..9e8e1ae0d5e047 100644 --- a/x-pack/plugins/session_view/common/constants.ts +++ b/x-pack/plugins/session_view/common/constants.ts @@ -6,10 +6,11 @@ */ export const PROCESS_EVENTS_ROUTE = '/internal/session_view/process_events_route'; +export const ALERTS_ROUTE = '/internal/session_view/alerts_route'; export const ALERT_STATUS_ROUTE = '/internal/session_view/alert_status_route'; export const SESSION_ENTRY_LEADERS_ROUTE = '/internal/session_view/session_entry_leaders_route'; export const PROCESS_EVENTS_INDEX = 'logs-endpoint.events.process-default'; -export const ALERTS_INDEX = '.siem-signals-default'; +export const ALERTS_INDEX = '.alerts-security.alerts-default'; // TODO: changes to remove this and use AlertsClient instead to get indices. export const ENTRY_SESSION_ENTITY_ID_PROPERTY = 'process.entry_leader.entity_id'; export const ALERT_UUID_PROPERTY = 'kibana.alert.uuid'; export const KIBANA_DATE_FORMAT = 'MMM DD, YYYY @ hh:mm:ss.SSS'; diff --git a/x-pack/plugins/session_view/kibana.json b/x-pack/plugins/session_view/kibana.json index ff9d849016c555..4807315569d341 100644 --- a/x-pack/plugins/session_view/kibana.json +++ b/x-pack/plugins/session_view/kibana.json @@ -1,6 +1,6 @@ { "id": "sessionView", - "version": "8.0.0", + "version": "1.0.0", "kibanaVersion": "kibana", "owner": { "name": "Security Team", @@ -8,10 +8,11 @@ }, "requiredPlugins": [ "data", - "timelines" + "timelines", + "ruleRegistry" ], "requiredBundles": [ - "kibanaReact", + "kibanaReact", "esUiShared" ], "server": true, diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.test.tsx new file mode 100644 index 00000000000000..1d0c9d0227699e --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.test.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { + DetailPanelAlertActions, + BUTTON_TEST_ID, + SHOW_DETAILS_TEST_ID, + JUMP_TO_PROCESS_TEST_ID, +} from './index'; +import { mockAlerts } from '../../../common/mocks/constants/session_view_process.mock'; +import userEvent from '@testing-library/user-event'; +import { ProcessImpl } from '../process_tree/hooks'; + +describe('DetailPanelAlertActions component', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + let mockShowAlertDetails = jest.fn((uuid) => uuid); + let mockOnProcessSelected = jest.fn((process) => process); + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + mockShowAlertDetails = jest.fn((uuid) => uuid); + mockOnProcessSelected = jest.fn((process) => process); + }); + + describe('When DetailPanelAlertActions is mounted', () => { + it('renders a popover when button is clicked', async () => { + const mockEvent = mockAlerts[0]; + + renderResult = mockedContext.render( + + ); + + userEvent.click(renderResult.getByTestId(BUTTON_TEST_ID)); + expect(renderResult.queryByTestId(SHOW_DETAILS_TEST_ID)).toBeTruthy(); + expect(renderResult.queryByTestId(JUMP_TO_PROCESS_TEST_ID)).toBeTruthy(); + expect(mockShowAlertDetails.mock.calls.length).toBe(0); + expect(mockOnProcessSelected.mock.calls.length).toBe(0); + }); + + it('calls alert flyout callback when View details clicked', async () => { + const mockEvent = mockAlerts[0]; + + renderResult = mockedContext.render( + + ); + + userEvent.click(renderResult.getByTestId(BUTTON_TEST_ID)); + userEvent.click(renderResult.getByTestId(SHOW_DETAILS_TEST_ID)); + expect(mockShowAlertDetails.mock.calls.length).toBe(1); + expect(mockShowAlertDetails.mock.results[0].value).toBe(mockEvent.kibana?.alert.uuid); + expect(mockOnProcessSelected.mock.calls.length).toBe(0); + }); + + it('calls onProcessSelected when Jump to process clicked', async () => { + const mockEvent = mockAlerts[0]; + + renderResult = mockedContext.render( + + ); + + userEvent.click(renderResult.getByTestId(BUTTON_TEST_ID)); + userEvent.click(renderResult.getByTestId(JUMP_TO_PROCESS_TEST_ID)); + expect(mockOnProcessSelected.mock.calls.length).toBe(1); + expect(mockOnProcessSelected.mock.results[0].value).toBeInstanceOf(ProcessImpl); + expect(mockShowAlertDetails.mock.calls.length).toBe(0); + }); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx new file mode 100644 index 00000000000000..4c7e3fdfaa9614 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiPopover, EuiContextMenuPanel, EuiButtonIcon, EuiContextMenuItem } from '@elastic/eui'; +import { Process, ProcessEvent } from '../../../common/types/process_tree'; +import { ProcessImpl } from '../process_tree/hooks'; + +export const BUTTON_TEST_ID = 'sessionView:detailPanelAlertActionsBtn'; +export const SHOW_DETAILS_TEST_ID = 'sessionView:detailPanelAlertActionShowDetails'; +export const JUMP_TO_PROCESS_TEST_ID = 'sessionView:detailPanelAlertActionJumpToProcess'; + +interface DetailPanelAlertActionsDeps { + event: ProcessEvent; + onShowAlertDetails: (alertId: string) => void; + onProcessSelected: (process: Process) => void; +} + +/** + * Detail panel alert context menu actions + */ +export const DetailPanelAlertActions = ({ + event, + onShowAlertDetails, + onProcessSelected, +}: DetailPanelAlertActionsDeps) => { + const [isPopoverOpen, setPopover] = useState(false); + + const onClosePopover = useCallback(() => { + setPopover(false); + }, []); + + const onToggleMenu = useCallback(() => { + setPopover(!isPopoverOpen); + }, [isPopoverOpen]); + + const onJumpToAlert = useCallback(() => { + const process = new ProcessImpl(event.process.entity_id); + process.addEvent(event); + + onProcessSelected(process); + setPopover(false); + }, [event, onProcessSelected]); + + const onShowDetails = useCallback(() => { + if (event.kibana) { + onShowAlertDetails(event.kibana.alert.uuid); + setPopover(false); + } + }, [event, onShowAlertDetails]); + + if (!event.kibana) { + return null; + } + + const { uuid } = event.kibana.alert; + + const menuItems = [ + + + , + + + , + ]; + + return ( + + } + isOpen={isPopoverOpen} + closePopover={onClosePopover} + panelPaddingSize="none" + anchorPosition="leftCenter" + > + + + ); +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/styles.ts new file mode 100644 index 00000000000000..14d0be374b5d15 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/styles.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 { useMemo } from 'react'; +import { useEuiTheme, transparentize } from '@elastic/eui'; +import { CSSObject, css } from '@emotion/react'; + +interface StylesDeps { + minimal?: boolean; + isInvestigated?: boolean; +} + +export const useStyles = ({ minimal = false, isInvestigated = false }: StylesDeps) => { + const { euiTheme } = useEuiTheme(); + + const cached = useMemo(() => { + const { colors, font, size, border } = euiTheme; + + const dangerBorder = transparentize(colors.danger, 0.2); + const dangerBackground = transparentize(colors.danger, 0.08); + const borderThickness = border.width.thin; + const mediumPadding = size.m; + + let alertTitleColor = colors.text; + let borderColor = colors.lightShade; + + if (isInvestigated) { + alertTitleColor = colors.primaryText; + borderColor = dangerBorder; + } + + const alertItem = css` + border: ${borderThickness} solid ${borderColor}; + padding: ${mediumPadding}; + border-radius: ${border.radius.medium}; + + margin: 0 ${mediumPadding} ${mediumPadding} ${mediumPadding}; + background-color: ${colors.emptyShade}; + + & .euiAccordion__buttonContent { + width: 100%; + } + + & .euiAccordion__button { + min-width: 0; + width: calc(100% - ${size.l}); + } + + & .euiAccordion__childWrapper { + overflow: visible; + } + `; + + const alertTitle: CSSObject = { + display: minimal ? 'none' : 'initial', + color: alertTitleColor, + fontWeight: font.weight.semiBold, + textOverflow: 'ellipsis', + overflow: 'hidden', + whiteSpace: 'nowrap', + }; + + const alertIcon: CSSObject = { + marginRight: size.s, + }; + + const alertAccordionButton: CSSObject = { + width: `calc(100% - ${size.l})`, + minWidth: 0, + }; + + const processPanel: CSSObject = { + border: `${borderThickness} solid ${colors.lightShade}`, + fontFamily: font.familyCode, + marginTop: mediumPadding, + padding: `${size.xs} ${size.s}`, + }; + + const investigatedLabel: CSSObject = { + position: 'relative', + zIndex: 1, + bottom: `-${mediumPadding}`, + left: `-${mediumPadding}`, + width: `calc(100% + ${mediumPadding} * 2)`, + borderTop: `${borderThickness} solid ${dangerBorder}`, + borderBottomLeftRadius: border.radius.medium, + borderBottomRightRadius: border.radius.medium, + backgroundColor: dangerBackground, + textAlign: 'center', + }; + + return { + alertItem, + alertTitle, + alertIcon, + alertAccordionButton, + processPanel, + investigatedLabel, + }; + }, [euiTheme, isInvestigated, minimal]); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx new file mode 100644 index 00000000000000..daa472cd6e5b4b --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx @@ -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 React, { useMemo } from 'react'; +import { EuiIcon, EuiText, EuiAccordion, EuiNotificationBadge } from '@elastic/eui'; +import { Process, ProcessEvent } from '../../../common/types/process_tree'; +import { useStyles } from '../detail_panel_alert_list_item/styles'; +import { DetailPanelAlertListItem } from '../detail_panel_alert_list_item'; +import { ALERT_COUNT_THRESHOLD } from '../../../common/constants'; + +export const ALERT_GROUP_ITEM_TEST_ID = 'sessionView:detailPanelAlertGroupItem'; +export const ALERT_GROUP_ITEM_COUNT_TEST_ID = 'sessionView:detailPanelAlertGroupCount'; +export const ALERT_GROUP_ITEM_TITLE_TEST_ID = 'sessionView:detailPanelAlertGroupTitle'; + +interface DetailPanelAlertsGroupItemDeps { + alerts: ProcessEvent[]; + onProcessSelected: (process: Process) => void; + onShowAlertDetails: (alertId: string) => void; +} + +/** + * Detail panel description list item. + */ +export const DetailPanelAlertGroupItem = ({ + alerts, + onProcessSelected, + onShowAlertDetails, +}: DetailPanelAlertsGroupItemDeps) => { + const styles = useStyles(); + + const alertsCount = useMemo(() => { + return alerts.length >= ALERT_COUNT_THRESHOLD ? ALERT_COUNT_THRESHOLD + '+' : alerts.length; + }, [alerts]); + + if (!alerts[0].kibana) { + return null; + } + + const { rule } = alerts[0].kibana.alert; + + return ( + +

+ + {rule.name} +

+ + } + css={styles.alertItem} + extraAction={ + + {alertsCount} + + } + > + {alerts.map((event) => { + const key = 'minimal_' + event.kibana?.alert.uuid; + + return ( + + ); + })} +
+ ); +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx new file mode 100644 index 00000000000000..516d04539432ef --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx @@ -0,0 +1,137 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiFlexItem, + EuiFlexGroup, + EuiSpacer, + EuiIcon, + EuiText, + EuiAccordion, + EuiPanel, + EuiHorizontalRule, +} from '@elastic/eui'; +import { Process, ProcessEvent } from '../../../common/types/process_tree'; +import { useStyles } from './styles'; +import { DetailPanelAlertActions } from '../detail_panel_alert_actions'; + +export const ALERT_LIST_ITEM_TEST_ID = 'sessionView:detailPanelAlertListItem'; +export const ALERT_LIST_ITEM_ARGS_TEST_ID = 'sessionView:detailPanelAlertListItemArgs'; +export const ALERT_LIST_ITEM_TIMESTAMP_TEST_ID = 'sessionView:detailPanelAlertListItemTimestamp'; + +interface DetailPanelAlertsListItemDeps { + event: ProcessEvent; + onShowAlertDetails: (alertId: string) => void; + onProcessSelected: (process: Process) => void; + isInvestigated?: boolean; + minimal?: boolean; +} + +/** + * Detail panel description list item. + */ +export const DetailPanelAlertListItem = ({ + event, + onProcessSelected, + onShowAlertDetails, + isInvestigated, + minimal, +}: DetailPanelAlertsListItemDeps) => { + const styles = useStyles(minimal, isInvestigated); + + if (!event.kibana) { + return null; + } + + const timestamp = event['@timestamp']; + const { uuid, name } = event.kibana.alert.rule; + const { args } = event.process; + + const forceState = !isInvestigated ? 'open' : undefined; + + return minimal ? ( +
+ + + + + {timestamp} + + + + + + + + {args.join(' ')} + + +
+ ) : ( + +

+ + {name} +

+ + } + initialIsOpen={true} + forceState={forceState} + css={styles.alertItem} + extraAction={ + + } + > + + + {timestamp} + + + + {args.join(' ')} + + + {isInvestigated && ( +
+ + + +
+ )} +
+ ); +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/styles.ts new file mode 100644 index 00000000000000..7672bb942ff329 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/styles.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { useEuiTheme, transparentize } from '@elastic/eui'; +import { CSSObject, css } from '@emotion/react'; + +export const useStyles = (minimal = false, isInvestigated = false) => { + const { euiTheme } = useEuiTheme(); + + const cached = useMemo(() => { + const { colors, font, size, border } = euiTheme; + + const dangerBorder = transparentize(colors.danger, 0.2); + const dangerBackground = transparentize(colors.danger, 0.08); + const borderThickness = border.width.thin; + const mediumPadding = size.m; + + let alertTitleColor = colors.text; + let borderColor = colors.lightShade; + + if (isInvestigated) { + alertTitleColor = colors.primaryText; + borderColor = dangerBorder; + } + + const alertItem = css` + border: ${borderThickness} solid ${borderColor}; + padding: ${mediumPadding}; + border-radius: ${border.radius.medium}; + + margin: 0 ${mediumPadding} ${mediumPadding} ${mediumPadding}; + background-color: ${colors.emptyShade}; + + & .euiAccordion__buttonContent { + width: 100%; + } + + & .euiAccordion__button { + min-width: 0; + width: calc(100% - ${size.l}); + } + + & .euiAccordion__childWrapper { + overflow: visible; + } + `; + + const alertTitle: CSSObject = { + display: minimal ? 'none' : 'initial', + color: alertTitleColor, + fontWeight: font.weight.semiBold, + textOverflow: 'ellipsis', + overflow: 'hidden', + whiteSpace: 'nowrap', + }; + + const alertIcon: CSSObject = { + marginRight: size.s, + }; + + const alertAccordionButton: CSSObject = { + width: `calc(100% - ${size.l})`, + minWidth: 0, + }; + + const processPanel: CSSObject = { + border: `${borderThickness} solid ${colors.lightShade}`, + fontFamily: font.familyCode, + marginTop: minimal ? size.s : size.m, + padding: `${size.xs} ${size.s}`, + }; + + const investigatedLabel: CSSObject = { + position: 'relative', + zIndex: 1, + bottom: `-${mediumPadding}`, + left: `-${mediumPadding}`, + width: `calc(100% + ${mediumPadding} * 2)`, + borderTop: `${borderThickness} solid ${dangerBorder}`, + borderBottomLeftRadius: border.radius.medium, + borderBottomRightRadius: border.radius.medium, + backgroundColor: dangerBackground, + textAlign: 'center', + }; + + const minimalContextMenu: CSSObject = { + float: 'right', + }; + + const minimalHR: CSSObject = { + marginBottom: 0, + }; + + return { + alertItem, + alertTitle, + alertIcon, + alertAccordionButton, + processPanel, + investigatedLabel, + minimalContextMenu, + minimalHR, + }; + }, [euiTheme, isInvestigated, minimal]); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.test.tsx new file mode 100644 index 00000000000000..a915f8e285ad13 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.test.tsx @@ -0,0 +1,251 @@ +/* + * 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 { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { DetailPanelAlertTab } from './index'; +import { mockAlerts } from '../../../common/mocks/constants/session_view_process.mock'; +import { fireEvent } from '@testing-library/dom'; +import { + INVESTIGATED_ALERT_TEST_ID, + VIEW_MODE_TOGGLE, + ALERTS_TAB_EMPTY_STATE_TEST_ID, +} from './index'; +import { + ALERT_LIST_ITEM_TEST_ID, + ALERT_LIST_ITEM_ARGS_TEST_ID, + ALERT_LIST_ITEM_TIMESTAMP_TEST_ID, +} from '../detail_panel_alert_list_item/index'; +import { + ALERT_GROUP_ITEM_TEST_ID, + ALERT_GROUP_ITEM_COUNT_TEST_ID, + ALERT_GROUP_ITEM_TITLE_TEST_ID, +} from '../detail_panel_alert_group_item/index'; + +const ACCORDION_BUTTON_CLASS = '.euiAccordion__button'; +const VIEW_MODE_GROUP = 'groupView'; +const ARIA_EXPANDED_ATTR = 'aria-expanded'; + +describe('DetailPanelAlertTab component', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + let mockOnProcessSelected = jest.fn((process) => process); + let mockShowAlertDetails = jest.fn((alertId) => alertId); + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + mockOnProcessSelected = jest.fn((process) => process); + mockShowAlertDetails = jest.fn((alertId) => alertId); + }); + + describe('When DetailPanelAlertTab is mounted', () => { + it('renders a list of alerts for the session (defaulting to list view mode)', async () => { + renderResult = mockedContext.render( + + ); + + expect(renderResult.queryAllByTestId(ALERT_LIST_ITEM_TEST_ID).length).toBe(mockAlerts.length); + expect(renderResult.queryByTestId(ALERT_GROUP_ITEM_TEST_ID)).toBeFalsy(); + expect(renderResult.queryByTestId(INVESTIGATED_ALERT_TEST_ID)).toBeFalsy(); + expect( + renderResult + .queryByTestId(VIEW_MODE_TOGGLE) + ?.querySelector('.euiButtonGroupButton-isSelected')?.textContent + ).toBe('List view'); + }); + + it('renders a list of alerts grouped by rule when group-view clicked', async () => { + renderResult = mockedContext.render( + + ); + + fireEvent.click(renderResult.getByTestId(VIEW_MODE_GROUP)); + + expect(renderResult.queryAllByTestId(ALERT_LIST_ITEM_TEST_ID).length).toBe(mockAlerts.length); + expect(renderResult.queryByTestId(ALERT_GROUP_ITEM_TEST_ID)).toBeTruthy(); + expect(renderResult.queryByTestId(INVESTIGATED_ALERT_TEST_ID)).toBeFalsy(); + expect( + renderResult + .queryByTestId(VIEW_MODE_TOGGLE) + ?.querySelector('.euiButtonGroupButton-isSelected')?.textContent + ).toBe('Group view'); + }); + + it('renders a sticky investigated alert (outside of main list) if one is set', async () => { + renderResult = mockedContext.render( + + ); + + expect(renderResult.queryByTestId(INVESTIGATED_ALERT_TEST_ID)).toBeTruthy(); + + fireEvent.click(renderResult.getByTestId(VIEW_MODE_GROUP)); + + expect(renderResult.queryByTestId(INVESTIGATED_ALERT_TEST_ID)).toBeTruthy(); + }); + + it('investigated alert should be collapsible', async () => { + renderResult = mockedContext.render( + + ); + + expect( + renderResult + .queryByTestId(INVESTIGATED_ALERT_TEST_ID) + ?.querySelector(ACCORDION_BUTTON_CLASS) + ?.attributes.getNamedItem(ARIA_EXPANDED_ATTR)?.value + ).toBe('true'); + + const expandButton = renderResult + .queryByTestId(INVESTIGATED_ALERT_TEST_ID) + ?.querySelector(ACCORDION_BUTTON_CLASS); + + if (expandButton) { + fireEvent.click(expandButton); + } + + expect( + renderResult + .queryByTestId(INVESTIGATED_ALERT_TEST_ID) + ?.querySelector(ACCORDION_BUTTON_CLASS) + ?.attributes.getNamedItem(ARIA_EXPANDED_ATTR)?.value + ).toBe('false'); + }); + + it('non investigated alert should NOT be collapsible', async () => { + renderResult = mockedContext.render( + + ); + + expect( + renderResult + .queryAllByTestId(ALERT_LIST_ITEM_TEST_ID)[0] + ?.querySelector(ACCORDION_BUTTON_CLASS) + ?.attributes.getNamedItem(ARIA_EXPANDED_ATTR)?.value + ).toBe('true'); + + const expandButton = renderResult + .queryAllByTestId(ALERT_LIST_ITEM_TEST_ID)[0] + ?.querySelector(ACCORDION_BUTTON_CLASS); + + if (expandButton) { + fireEvent.click(expandButton); + } + + expect( + renderResult + .queryAllByTestId(ALERT_LIST_ITEM_TEST_ID)[0] + ?.querySelector(ACCORDION_BUTTON_CLASS) + ?.attributes.getNamedItem(ARIA_EXPANDED_ATTR)?.value + ).toBe('true'); + }); + + it('grouped alerts should be expandable/collapsible (default to collapsed)', async () => { + renderResult = mockedContext.render( + + ); + + fireEvent.click(renderResult.getByTestId(VIEW_MODE_GROUP)); + + expect( + renderResult + .queryAllByTestId(ALERT_GROUP_ITEM_TEST_ID)[0] + ?.querySelector(ACCORDION_BUTTON_CLASS) + ?.attributes.getNamedItem(ARIA_EXPANDED_ATTR)?.value + ).toBe('false'); + + const expandButton = renderResult + .queryAllByTestId(ALERT_GROUP_ITEM_TEST_ID)[0] + ?.querySelector(ACCORDION_BUTTON_CLASS); + + if (expandButton) { + fireEvent.click(expandButton); + } + + expect( + renderResult + .queryAllByTestId(ALERT_GROUP_ITEM_TEST_ID)[0] + ?.querySelector(ACCORDION_BUTTON_CLASS) + ?.attributes.getNamedItem(ARIA_EXPANDED_ATTR)?.value + ).toBe('true'); + }); + + it('each alert list item should show a timestamp and process arguments', async () => { + renderResult = mockedContext.render( + + ); + + expect(renderResult.queryAllByTestId(ALERT_LIST_ITEM_TIMESTAMP_TEST_ID)[0]).toHaveTextContent( + mockAlerts[0]['@timestamp'] + ); + + expect(renderResult.queryAllByTestId(ALERT_LIST_ITEM_ARGS_TEST_ID)[0]).toHaveTextContent( + mockAlerts[0].process.args.join(' ') + ); + }); + + it('each alert group should show a rule title and alert count', async () => { + renderResult = mockedContext.render( + + ); + + fireEvent.click(renderResult.getByTestId(VIEW_MODE_GROUP)); + + expect(renderResult.queryByTestId(ALERT_GROUP_ITEM_COUNT_TEST_ID)).toHaveTextContent('2'); + expect(renderResult.queryByTestId(ALERT_GROUP_ITEM_TITLE_TEST_ID)).toHaveTextContent( + mockAlerts[0].kibana?.alert.rule.name || '' + ); + }); + + it('renders an empty state when there are no alerts', async () => { + renderResult = mockedContext.render( + + ); + + expect(renderResult.queryByTestId(ALERTS_TAB_EMPTY_STATE_TEST_ID)).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx new file mode 100644 index 00000000000000..7fa47f4f5daf7c --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useState, useMemo } from 'react'; +import { EuiEmptyPrompt, EuiButtonGroup, EuiHorizontalRule } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { groupBy } from 'lodash'; +import { ProcessEvent, Process } from '../../../common/types/process_tree'; +import { useStyles } from './styles'; +import { DetailPanelAlertListItem } from '../detail_panel_alert_list_item'; +import { DetailPanelAlertGroupItem } from '../detail_panel_alert_group_item'; + +export const ALERTS_TAB_EMPTY_STATE_TEST_ID = 'sessionView:detailPanelAlertsEmptyState'; +export const INVESTIGATED_ALERT_TEST_ID = 'sessionView:detailPanelInvestigatedAlert'; +export const VIEW_MODE_TOGGLE = 'sessionView:detailPanelAlertsViewMode'; + +interface DetailPanelAlertTabDeps { + alerts: ProcessEvent[]; + onProcessSelected: (process: Process) => void; + onShowAlertDetails: (alertId: string) => void; + investigatedAlert?: ProcessEvent; +} + +const VIEW_MODE_LIST = 'listView'; +const VIEW_MODE_GROUP = 'groupView'; + +/** + * Host Panel of session view detail panel. + */ +export const DetailPanelAlertTab = ({ + alerts, + onProcessSelected, + onShowAlertDetails, + investigatedAlert, +}: DetailPanelAlertTabDeps) => { + const styles = useStyles(); + const [viewMode, setViewMode] = useState(VIEW_MODE_LIST); + const viewModes = [ + { + id: VIEW_MODE_LIST, + label: i18n.translate('xpack.sessionView.alertDetailsTab.listView', { + defaultMessage: 'List view', + }), + }, + { + id: VIEW_MODE_GROUP, + label: i18n.translate('xpack.sessionView.alertDetailsTab.groupView', { + defaultMessage: 'Group view', + }), + }, + ]; + + const filteredAlerts = useMemo(() => { + return alerts.filter((event) => { + const isInvestigatedAlert = + event.kibana?.alert.uuid === investigatedAlert?.kibana?.alert.uuid; + return !isInvestigatedAlert; + }); + }, [investigatedAlert, alerts]); + + const groupedAlerts = useMemo(() => { + return groupBy(filteredAlerts, (event) => event.kibana?.alert.rule.uuid); + }, [filteredAlerts]); + + if (alerts.length === 0) { + return ( + + + + } + body={ +

+ +

+ } + /> + ); + } + + return ( +
+ + {investigatedAlert && ( +
+ + +
+ )} + + {viewMode === VIEW_MODE_LIST + ? filteredAlerts.map((event) => { + const key = event.kibana?.alert.uuid; + + return ( + + ); + }) + : Object.keys(groupedAlerts).map((ruleId: string) => { + const alertsByRule = groupedAlerts[ruleId]; + + return ( + + ); + })} +
+ ); +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/styles.ts new file mode 100644 index 00000000000000..a906744cdafb2e --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/styles.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { useEuiTheme } from '@elastic/eui'; +import { CSSObject } from '@emotion/react'; + +export const useStyles = () => { + const { euiTheme } = useEuiTheme(); + + const cached = useMemo(() => { + const { colors, size } = euiTheme; + + const container: CSSObject = { + position: 'relative', + }; + + const stickyItem: CSSObject = { + position: 'sticky', + top: 0, + zIndex: 1, + backgroundColor: colors.emptyShade, + paddingTop: size.base, + }; + + const viewMode: CSSObject = { + margin: size.base, + marginBottom: 0, + }; + + return { + container, + stickyItem, + viewMode, + }; + }, [euiTheme]); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/components/process_tree/hooks.ts b/x-pack/plugins/session_view/public/components/process_tree/hooks.ts index fb00344d5e280e..2b7f78e88fafb1 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/hooks.ts +++ b/x-pack/plugins/session_view/public/components/process_tree/hooks.ts @@ -21,12 +21,14 @@ import { processNewEvents, searchProcessTree, autoExpandProcessTree, + updateProcessMap, } from './helpers'; import { sortProcesses } from '../../../common/utils/sort_processes'; interface UseProcessTreeDeps { sessionEntityId: string; data: ProcessEventsPage[]; + alerts: ProcessEvent[]; searchQuery?: string; updatedAlertsStatus: AlertStatusEventEntityIdMap; } @@ -196,6 +198,7 @@ export class ProcessImpl implements Process { export const useProcessTree = ({ sessionEntityId, data, + alerts, searchQuery, updatedAlertsStatus, }: UseProcessTreeDeps) => { @@ -221,6 +224,7 @@ export const useProcessTree = ({ const [processMap, setProcessMap] = useState(initializedProcessMap); const [processedPages, setProcessedPages] = useState([]); + const [alertsProcessed, setAlertsProcessed] = useState(false); const [searchResults, setSearchResults] = useState([]); const [orphans, setOrphans] = useState([]); @@ -257,6 +261,15 @@ export const useProcessTree = ({ } }, [data, processMap, orphans, processedPages, sessionEntityId]); + useEffect(() => { + // currently we are loading a single page of alerts, with no pagination + // so we only need to add these alert events to processMap once. + if (!alertsProcessed) { + updateProcessMap(processMap, alerts); + setAlertsProcessed(true); + } + }, [processMap, alerts, alertsProcessed]); + useEffect(() => { setSearchResults(searchProcessTree(processMap, searchQuery)); autoExpandProcessTree(processMap); diff --git a/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx index 9fa7900d04b0dc..3c0b9c5d0d4d9f 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { mockData } from '../../../common/mocks/constants/session_view_process.mock'; +import { mockData, mockAlerts } from '../../../common/mocks/constants/session_view_process.mock'; import { Process } from '../../../common/types/process_tree'; import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; import { ProcessImpl } from './hooks'; @@ -21,6 +21,7 @@ describe('ProcessTree component', () => { const props: ProcessTreeDeps = { sessionEntityId: sessionLeader.process.entity_id, data: mockData, + alerts: mockAlerts, isFetching: false, fetchNextPage: jest.fn(), hasNextPage: false, @@ -28,7 +29,7 @@ describe('ProcessTree component', () => { hasPreviousPage: false, onProcessSelected: jest.fn(), updatedAlertsStatus: {}, - handleOnAlertDetailsClosed: jest.fn(), + onShowAlertDetails: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/session_view/public/components/process_tree/index.tsx b/x-pack/plugins/session_view/public/components/process_tree/index.tsx index 4b489797c7e267..1e10e58d1cca05 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree/index.tsx @@ -26,6 +26,7 @@ export interface ProcessTreeDeps { sessionEntityId: string; data: ProcessEventsPage[]; + alerts: ProcessEvent[]; jumpToEvent?: ProcessEvent; isFetching: boolean; @@ -44,8 +45,7 @@ export interface ProcessTreeDeps { // a map for alerts with updated status and process.entity_id updatedAlertsStatus: AlertStatusEventEntityIdMap; - loadAlertDetails?: (alertUuid: string, handleOnAlertDetailsClosed: () => void) => void; - handleOnAlertDetailsClosed: (alertUuid: string) => void; + onShowAlertDetails: (alertUuid: string) => void; timeStampOn?: boolean; verboseModeOn?: boolean; } @@ -53,6 +53,7 @@ export interface ProcessTreeDeps { export const ProcessTree = ({ sessionEntityId, data, + alerts, jumpToEvent, isFetching, hasNextPage, @@ -64,8 +65,7 @@ export const ProcessTree = ({ onProcessSelected, setSearchResults, updatedAlertsStatus, - loadAlertDetails, - handleOnAlertDetailsClosed, + onShowAlertDetails, timeStampOn, verboseModeOn, }: ProcessTreeDeps) => { @@ -76,6 +76,7 @@ export const ProcessTree = ({ const { sessionLeader, processMap, searchResults } = useProcessTree({ sessionEntityId, data, + alerts, searchQuery, updatedAlertsStatus, }); @@ -203,8 +204,7 @@ export const ProcessTree = ({ selectedProcessId={selectedProcess?.id} scrollerRef={scrollerRef} onChangeJumpToEventVisibility={onChangeJumpToEventVisibility} - loadAlertDetails={loadAlertDetails} - handleOnAlertDetailsClosed={handleOnAlertDetailsClosed} + onShowAlertDetails={onShowAlertDetails} timeStampOn={timeStampOn} verboseModeOn={verboseModeOn} /> diff --git a/x-pack/plugins/session_view/public/components/process_tree_alert/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree_alert/index.test.tsx index 2a56a0ae2be672..c1b0c807528eca 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_alert/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_alert/index.test.tsx @@ -26,7 +26,7 @@ describe('ProcessTreeAlerts component', () => { isSelected: false, onClick: jest.fn(), selectAlert: jest.fn(), - handleOnAlertDetailsClosed: jest.fn(), + onShowAlertDetails: jest.fn(), }; beforeEach(() => { @@ -61,16 +61,16 @@ describe('ProcessTreeAlerts component', () => { expect(selectAlert).toHaveBeenCalledTimes(1); }); - it('should execute loadAlertDetails callback when clicking on expand button', async () => { - const loadAlertDetails = jest.fn(); + it('should execute onShowAlertDetails callback when clicking on expand button', async () => { + const onShowAlertDetails = jest.fn(); renderResult = mockedContext.render( - + ); const expandButton = renderResult.queryByTestId(EXPAND_BUTTON_TEST_ID); expect(expandButton).toBeTruthy(); expandButton?.click(); - expect(loadAlertDetails).toHaveBeenCalledTimes(1); + expect(onShowAlertDetails).toHaveBeenCalledTimes(1); }); }); }); diff --git a/x-pack/plugins/session_view/public/components/process_tree_alert/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_alert/index.tsx index 5ec1c4a7693c34..30892d02c54284 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_alert/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_alert/index.tsx @@ -17,8 +17,7 @@ export interface ProcessTreeAlertDeps { isSelected: boolean; onClick: (alert: ProcessEventAlert | null) => void; selectAlert: (alertUuid: string) => void; - loadAlertDetails?: (alertUuid: string, handleOnAlertDetailsClosed: () => void) => void; - handleOnAlertDetailsClosed: (alertUuid: string, status?: string) => void; + onShowAlertDetails: (alertUuid: string) => void; } export const ProcessTreeAlert = ({ @@ -27,8 +26,7 @@ export const ProcessTreeAlert = ({ isSelected, onClick, selectAlert, - loadAlertDetails, - handleOnAlertDetailsClosed, + onShowAlertDetails, }: ProcessTreeAlertDeps) => { const styles = useStyles({ isInvestigated, isSelected }); @@ -41,10 +39,10 @@ export const ProcessTreeAlert = ({ }, [isInvestigated, uuid, selectAlert]); const handleExpandClick = useCallback(() => { - if (loadAlertDetails && uuid) { - loadAlertDetails(uuid, () => handleOnAlertDetailsClosed(uuid)); + if (uuid) { + onShowAlertDetails(uuid); } - }, [handleOnAlertDetailsClosed, loadAlertDetails, uuid]); + }, [onShowAlertDetails, uuid]); const handleClick = useCallback(() => { if (alert.kibana?.alert) { diff --git a/x-pack/plugins/session_view/public/components/process_tree_alerts/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.test.tsx index 2333c71d36a510..ee6866f6a8a604 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_alerts/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.test.tsx @@ -17,7 +17,7 @@ describe('ProcessTreeAlerts component', () => { const props: ProcessTreeAlertsDeps = { alerts: mockAlerts, onAlertSelected: jest.fn(), - handleOnAlertDetailsClosed: jest.fn(), + onShowAlertDetails: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/session_view/public/components/process_tree_alerts/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.tsx index c97ccfe253605b..b51d58bf825ec3 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_alerts/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.tsx @@ -16,8 +16,7 @@ export interface ProcessTreeAlertsDeps { jumpToAlertID?: string; isProcessSelected?: boolean; onAlertSelected: (e: MouseEvent) => void; - loadAlertDetails?: (alertUuid: string, handleOnAlertDetailsClosed: () => void) => void; - handleOnAlertDetailsClosed: (alertUuid: string) => void; + onShowAlertDetails: (alertUuid: string) => void; } export function ProcessTreeAlerts({ @@ -25,8 +24,7 @@ export function ProcessTreeAlerts({ jumpToAlertID, isProcessSelected = false, onAlertSelected, - loadAlertDetails, - handleOnAlertDetailsClosed, + onShowAlertDetails, }: ProcessTreeAlertsDeps) { const [selectedAlert, setSelectedAlert] = useState(null); const styles = useStyles(); @@ -90,8 +88,7 @@ export function ProcessTreeAlerts({ isSelected={isProcessSelected && selectedAlert?.uuid === alertUuid} onClick={handleAlertClick} selectAlert={selectAlert} - loadAlertDetails={loadAlertDetails} - handleOnAlertDetailsClosed={handleOnAlertDetailsClosed} + onShowAlertDetails={onShowAlertDetails} /> ); })} diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx index 2e82e822f0c82c..5c3b790ad04309 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx @@ -36,7 +36,7 @@ describe('ProcessTreeNode component', () => { }, } as unknown as RefObject, onChangeJumpToEventVisibility: jest.fn(), - handleOnAlertDetailsClosed: (_alertUuid: string) => {}, + onShowAlertDetails: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx index b1c42dd95efb91..387e7a5074699f 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx @@ -21,7 +21,8 @@ import React, { useMemo, RefObject, } from 'react'; -import { EuiButton, EuiIcon, formatDate } from '@elastic/eui'; +import { EuiButton, EuiIcon, EuiToolTip, formatDate } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { Process } from '../../../common/types/process_tree'; import { useVisible } from '../../hooks/use_visible'; @@ -43,8 +44,7 @@ export interface ProcessDeps { verboseModeOn?: boolean; scrollerRef: RefObject; onChangeJumpToEventVisibility: (isVisible: boolean, isAbove: boolean) => void; - loadAlertDetails?: (alertUuid: string, handleOnAlertDetailsClosed: () => void) => void; - handleOnAlertDetailsClosed: (alertUuid: string) => void; + onShowAlertDetails: (alertUuid: string) => void; } /** @@ -62,8 +62,7 @@ export function ProcessTreeNode({ verboseModeOn = true, scrollerRef, onChangeJumpToEventVisibility, - loadAlertDetails, - handleOnAlertDetailsClosed, + onShowAlertDetails, }: ProcessDeps) { const textRef = useRef(null); @@ -144,6 +143,33 @@ export function ProcessTreeNode({ ); const processDetails = process.getDetails(); + const hasExec = process.hasExec(); + + const processIcon = useMemo(() => { + if (!process.parent) { + return 'unlink'; + } else if (hasExec) { + return 'console'; + } else { + return 'branch'; + } + }, [hasExec, process.parent]); + + const iconTooltip = useMemo(() => { + if (!process.parent) { + return i18n.translate('xpack.sessionView.processNode.tooltipOrphan', { + defaultMessage: 'Process missing parent (orphan)', + }); + } else if (hasExec) { + return i18n.translate('xpack.sessionView.processNode.tooltipExec', { + defaultMessage: "Process exec'd", + }); + } else { + return i18n.translate('xpack.sessionView.processNode.tooltipFork', { + defaultMessage: 'Process forked (no exec)', + }); + } + }, [hasExec, process.parent]); if (!processDetails?.process) { return null; @@ -169,11 +195,9 @@ export function ProcessTreeNode({ const showUserEscalation = user.id !== parent.user.id; const interactiveSession = !!tty; const sessionIcon = interactiveSession ? 'consoleApp' : 'compute'; - const hasExec = process.hasExec(); const iconTestSubj = hasExec ? 'sessionView:processTreeNodeExecIcon' : 'sessionView:processTreeNodeForkIcon'; - const processIcon = hasExec ? 'console' : 'branch'; const timeStampsNormal = formatDate(start, KIBANA_DATE_FORMAT); @@ -200,7 +224,9 @@ export function ProcessTreeNode({ ) : ( - + + + {' '} {workingDirectory}  {args[0]}  @@ -255,8 +281,7 @@ export function ProcessTreeNode({ jumpToAlertID={jumpToAlertID} isProcessSelected={selectedProcessId === process.id} onAlertSelected={onProcessClicked} - loadAlertDetails={loadAlertDetails} - handleOnAlertDetailsClosed={handleOnAlertDetailsClosed} + onShowAlertDetails={onShowAlertDetails} /> )} @@ -276,8 +301,7 @@ export function ProcessTreeNode({ verboseModeOn={verboseModeOn} scrollerRef={scrollerRef} onChangeJumpToEventVisibility={onChangeJumpToEventVisibility} - loadAlertDetails={loadAlertDetails} - handleOnAlertDetailsClosed={handleOnAlertDetailsClosed} + onShowAlertDetails={onShowAlertDetails} /> ); })} diff --git a/x-pack/plugins/session_view/public/components/session_view/hooks.ts b/x-pack/plugins/session_view/public/components/session_view/hooks.ts index a134a366c41685..bf8796336602d3 100644 --- a/x-pack/plugins/session_view/public/components/session_view/hooks.ts +++ b/x-pack/plugins/session_view/public/components/session_view/hooks.ts @@ -15,9 +15,11 @@ import { ProcessEventResults, } from '../../../common/types/process_tree'; import { + ALERTS_ROUTE, PROCESS_EVENTS_ROUTE, PROCESS_EVENTS_PER_PAGE, ALERT_STATUS_ROUTE, + QUERY_KEY_PROCESS_EVENTS, QUERY_KEY_ALERTS, } from '../../../common/constants'; @@ -28,9 +30,10 @@ export const useFetchSessionViewProcessEvents = ( const { http } = useKibana().services; const [isJumpToFirstPage, setIsJumpToFirstPage] = useState(false); const jumpToCursor = jumpToEvent && jumpToEvent.process.start; + const cachingKeys = [QUERY_KEY_PROCESS_EVENTS, sessionEntityId]; const query = useInfiniteQuery( - 'sessionViewProcessEvents', + cachingKeys, async ({ pageParam = {} }) => { let { cursor } = pageParam; const { forward } = pageParam; @@ -52,7 +55,7 @@ export const useFetchSessionViewProcessEvents = ( return { events, cursor }; }, { - getNextPageParam: (lastPage, pages) => { + getNextPageParam: (lastPage) => { if (lastPage.events.length === PROCESS_EVENTS_PER_PAGE) { return { cursor: lastPage.events[lastPage.events.length - 1]['@timestamp'], @@ -60,7 +63,7 @@ export const useFetchSessionViewProcessEvents = ( }; } }, - getPreviousPageParam: (firstPage, pages) => { + getPreviousPageParam: (firstPage) => { if (jumpToEvent && firstPage.events.length === PROCESS_EVENTS_PER_PAGE) { return { cursor: firstPage.events[0]['@timestamp'], @@ -84,6 +87,32 @@ export const useFetchSessionViewProcessEvents = ( return query; }; +export const useFetchSessionViewAlerts = (sessionEntityId: string) => { + const { http } = useKibana().services; + const cachingKeys = [QUERY_KEY_ALERTS, sessionEntityId]; + const query = useQuery( + cachingKeys, + async () => { + const res = await http.get(ALERTS_ROUTE, { + query: { + sessionEntityId, + }, + }); + + const events = res.events.map((event: any) => event._source as ProcessEvent); + + return events; + }, + { + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchOnReconnect: false, + } + ); + + return query; +}; + export const useFetchAlertStatus = ( updatedAlertsStatus: AlertStatusEventEntityIdMap, alertUuid: string diff --git a/x-pack/plugins/session_view/public/components/session_view/index.tsx b/x-pack/plugins/session_view/public/components/session_view/index.tsx index af4eb6114a0a26..ee481c42041082 100644 --- a/x-pack/plugins/session_view/public/components/session_view/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view/index.tsx @@ -23,7 +23,11 @@ import { SessionViewDetailPanel } from '../session_view_detail_panel'; import { SessionViewSearchBar } from '../session_view_search_bar'; import { SessionViewDisplayOptions } from '../session_view_display_options'; import { useStyles } from './styles'; -import { useFetchAlertStatus, useFetchSessionViewProcessEvents } from './hooks'; +import { + useFetchAlertStatus, + useFetchSessionViewProcessEvents, + useFetchSessionViewAlerts, +} from './hooks'; /** * The main wrapper component for the session view. @@ -61,8 +65,12 @@ export const SessionView = ({ hasPreviousPage, } = useFetchSessionViewProcessEvents(sessionEntityId, jumpToEvent); - const hasData = data && data.pages.length > 0 && data.pages[0].events.length > 0; - const renderIsLoading = isFetching && !data; + const alertsQuery = useFetchSessionViewAlerts(sessionEntityId); + const { data: alerts, error: alertsError, isFetching: alertsFetching } = alertsQuery; + + const hasData = alerts && data && data.pages?.[0].events.length > 0; + const hasError = error || alertsError; + const renderIsLoading = (isFetching || alertsFetching) && !data; const renderDetails = isDetailOpen && selectedProcess; const { data: newUpdatedAlertsStatus } = useFetchAlertStatus( updatedAlertsStatus, @@ -83,6 +91,15 @@ export const SessionView = ({ setIsDetailOpen(!isDetailOpen); }, [isDetailOpen]); + const onShowAlertDetails = useCallback( + (alertUuid: string) => { + if (loadAlertDetails) { + loadAlertDetails(alertUuid, () => handleOnAlertDetailsClosed(alertUuid)); + } + }, + [loadAlertDetails, handleOnAlertDetailsClosed] + ); + const handleOptionChange = useCallback((checkedOptions: DisplayOptionsState) => { setDisplayOptions(checkedOptions); }, []); @@ -165,7 +182,7 @@ export const SessionView = ({ )} - {error && ( + {hasError && ( @@ -215,7 +232,7 @@ export const SessionView = ({ {renderDetails ? ( <> - + diff --git a/x-pack/plugins/session_view/public/components/session_view/styles.ts b/x-pack/plugins/session_view/public/components/session_view/styles.ts index d2c87130bfa4b7..edfe2356d5aa20 100644 --- a/x-pack/plugins/session_view/public/components/session_view/styles.ts +++ b/x-pack/plugins/session_view/public/components/session_view/styles.ts @@ -17,6 +17,10 @@ export const useStyles = ({ height = 500 }: StylesDeps) => { const { euiTheme } = useEuiTheme(); const cached = useMemo(() => { + const { border, colors } = euiTheme; + + const thinBorder = `${border.width.thin} solid ${colors.lightShade}!important`; + const processTree: CSSObject = { height: `${height}px`, position: 'relative', @@ -24,6 +28,12 @@ export const useStyles = ({ height = 500 }: StylesDeps) => { const detailPanel: CSSObject = { height: `${height}px`, + borderLeft: thinBorder, + borderRight: thinBorder, + }; + + const resizeHandle: CSSObject = { + zIndex: 2, }; const searchBar: CSSObject = { @@ -38,6 +48,7 @@ export const useStyles = ({ height = 500 }: StylesDeps) => { return { processTree, detailPanel, + resizeHandle, searchBar, buttonsEyeDetail, }; diff --git a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.test.tsx b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.test.tsx index f754086fe5fab7..40e71efd8a6cfe 100644 --- a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.test.tsx @@ -6,7 +6,10 @@ */ import React from 'react'; -import { sessionViewBasicProcessMock } from '../../../common/mocks/constants/session_view_process.mock'; +import { + mockAlerts, + sessionViewBasicProcessMock, +} from '../../../common/mocks/constants/session_view_process.mock'; import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; import { SessionViewDetailPanel } from './index'; @@ -14,27 +17,66 @@ describe('SessionView component', () => { let render: () => ReturnType; let renderResult: ReturnType; let mockedContext: AppContextTestRender; + let mockOnProcessSelected = jest.fn((process) => process); + let mockShowAlertDetails = jest.fn((alertId) => alertId); beforeEach(() => { mockedContext = createAppRootMockRenderer(); + mockOnProcessSelected = jest.fn((process) => process); + mockShowAlertDetails = jest.fn((alertId) => alertId); }); describe('When SessionViewDetailPanel is mounted', () => { it('shows process detail by default', async () => { renderResult = mockedContext.render( - + ); expect(renderResult.queryByText('8e4daeb2-4a4e-56c4-980e-f0dcfdbc3726')).toBeVisible(); }); it('can switch tabs to show host details', async () => { renderResult = mockedContext.render( - + ); renderResult.queryByText('Host')?.click(); expect(renderResult.queryByText('hostname')).toBeVisible(); expect(renderResult.queryAllByText('james-fleet-714-2')).toHaveLength(2); }); + + it('can switch tabs to show alert details', async () => { + renderResult = mockedContext.render( + + ); + + renderResult.queryByText('Alerts')?.click(); + expect(renderResult.queryByText('List view')).toBeVisible(); + }); + it('alert tab disabled when no alerts', async () => { + renderResult = mockedContext.render( + + ); + + renderResult.queryByText('Alerts')?.click(); + expect(renderResult.queryByText('List view')).toBeFalsy(); + }); }); }); diff --git a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx index a47ce1d91ac973..51eb65a38f835e 100644 --- a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx @@ -6,50 +6,91 @@ */ import React, { useState, useMemo, useCallback } from 'react'; import { EuiTabs, EuiTab, EuiNotificationBadge } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { EuiTabProps } from '../../types'; -import { Process } from '../../../common/types/process_tree'; +import { Process, ProcessEvent } from '../../../common/types/process_tree'; import { getDetailPanelProcess, getSelectedTabContent } from './helpers'; import { DetailPanelProcessTab } from '../detail_panel_process_tab'; import { DetailPanelHostTab } from '../detail_panel_host_tab'; +import { DetailPanelAlertTab } from '../detail_panel_alert_tab'; +import { ALERT_COUNT_THRESHOLD } from '../../../common/constants'; interface SessionViewDetailPanelDeps { selectedProcess: Process; - onProcessSelected?: (process: Process) => void; + alerts?: ProcessEvent[]; + investigatedAlert?: ProcessEvent; + onProcessSelected: (process: Process) => void; + onShowAlertDetails: (alertId: string) => void; } /** * Detail panel in the session view. */ -export const SessionViewDetailPanel = ({ selectedProcess }: SessionViewDetailPanelDeps) => { +export const SessionViewDetailPanel = ({ + alerts, + selectedProcess, + investigatedAlert, + onProcessSelected, + onShowAlertDetails, +}: SessionViewDetailPanelDeps) => { const [selectedTabId, setSelectedTabId] = useState('process'); const processDetail = useMemo(() => getDetailPanelProcess(selectedProcess), [selectedProcess]); - const tabs: EuiTabProps[] = useMemo( - () => [ + const alertsCount = useMemo(() => { + if (!alerts) { + return 0; + } + + return alerts.length >= ALERT_COUNT_THRESHOLD ? ALERT_COUNT_THRESHOLD + '+' : alerts.length; + }, [alerts]); + + const tabs: EuiTabProps[] = useMemo(() => { + const hasAlerts = !!alerts?.length; + + return [ { id: 'process', - name: 'Process', + name: i18n.translate('xpack.sessionView.detailsPanel.process', { + defaultMessage: 'Process', + }), content: , }, { id: 'host', - name: 'Host', + name: i18n.translate('xpack.sessionView.detailsPanel.host', { + defaultMessage: 'Host', + }), content: , }, { id: 'alerts', - disabled: true, - name: 'Alerts', - append: ( + name: i18n.translate('xpack.sessionView.detailsPanel.alerts', { + defaultMessage: 'Alerts', + }), + append: hasAlerts && ( - 10 + {alertsCount} ), - content: null, + content: alerts && ( + + ), }, - ], - [processDetail, selectedProcess.events] - ); + ]; + }, [ + alerts, + alertsCount, + processDetail, + selectedProcess.events, + onProcessSelected, + onShowAlertDetails, + investigatedAlert, + ]); const onSelectedTabChanged = useCallback((id: string) => { setSelectedTabId(id); diff --git a/x-pack/plugins/session_view/server/plugin.ts b/x-pack/plugins/session_view/server/plugin.ts index c7fd511b3de050..7347f7676af626 100644 --- a/x-pack/plugins/session_view/server/plugin.ts +++ b/x-pack/plugins/session_view/server/plugin.ts @@ -11,12 +11,14 @@ import { Plugin, Logger, PluginInitializerContext, + IRouter, } from '../../../../src/core/server'; import { SessionViewSetupPlugins, SessionViewStartPlugins } from './types'; import { registerRoutes } from './routes'; export class SessionViewPlugin implements Plugin { private logger: Logger; + private router: IRouter | undefined; /** * Initialize SessionViewPlugin class properties (logger, etc) that is accessible @@ -28,14 +30,16 @@ export class SessionViewPlugin implements Plugin { public setup(core: CoreSetup, plugins: SessionViewSetupPlugins) { this.logger.debug('session view: Setup'); - const router = core.http.createRouter(); - - // Register server routes - registerRoutes(router); + this.router = core.http.createRouter(); } public start(core: CoreStart, plugins: SessionViewStartPlugins) { this.logger.debug('session view: Start'); + + // Register server routes + if (this.router) { + registerRoutes(this.router, plugins.ruleRegistry); + } } public stop() { diff --git a/x-pack/plugins/session_view/server/routes/alerts_route.test.ts b/x-pack/plugins/session_view/server/routes/alerts_route.test.ts new file mode 100644 index 00000000000000..4c8ee6fb2c83eb --- /dev/null +++ b/x-pack/plugins/session_view/server/routes/alerts_route.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + ALERT_RULE_CONSUMER, + ALERT_RULE_TYPE_ID, + SPACE_IDS, + ALERT_WORKFLOW_STATUS, +} from '@kbn/rule-data-utils'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import { doSearch } from './alerts_route'; +import { mockEvents } from '../../common/mocks/constants/session_view_process.mock'; + +import { + AlertsClient, + ConstructorOptions, +} from '../../../rule_registry/server/alert_data_client/alerts_client'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { alertingAuthorizationMock } from '../../../alerting/server/authorization/alerting_authorization.mock'; +import { auditLoggerMock } from '../../../security/server/audit/mocks'; +import { AlertingAuthorizationEntity } from '../../../alerting/server'; +import { ruleDataServiceMock } from '../../../rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock'; + +const alertingAuthMock = alertingAuthorizationMock.create(); +const auditLogger = auditLoggerMock.create(); + +const DEFAULT_SPACE = 'test_default_space_id'; + +const getEmptyResponse = async () => { + return { + hits: { + total: 0, + hits: [], + }, + }; +}; + +const getResponse = async () => { + return { + hits: { + total: mockEvents.length, + hits: mockEvents.map((event) => { + return { + found: true, + _type: 'alert', + _index: '.alerts-security', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 1, + _seq_no: 362, + _primary_term: 2, + _source: { + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', + message: 'hello world 1', + [ALERT_RULE_CONSUMER]: 'apm', + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: ['test_default_space_id'], + ...event, + }, + }; + }), + }, + }; +}; + +const esClientMock = elasticsearchServiceMock.createElasticsearchClient(getResponse()); + +const alertsClientParams: jest.Mocked = { + logger: loggingSystemMock.create().get(), + authorization: alertingAuthMock, + auditLogger, + ruleDataService: ruleDataServiceMock.create(), + esClient: esClientMock, +}; + +describe('alerts_route.ts', () => { + beforeEach(() => { + jest.resetAllMocks(); + + alertingAuthMock.getSpaceId.mockImplementation(() => DEFAULT_SPACE); + // @ts-expect-error + alertingAuthMock.getAuthorizationFilter.mockImplementation(async () => + Promise.resolve({ filter: [] }) + ); + // @ts-expect-error + alertingAuthMock.getAugmentedRuleTypesWithAuthorization.mockImplementation(async () => { + const authorizedRuleTypes = new Set(); + authorizedRuleTypes.add({ producer: 'apm' }); + return Promise.resolve({ authorizedRuleTypes }); + }); + + alertingAuthMock.ensureAuthorized.mockImplementation( + // @ts-expect-error + async ({ + ruleTypeId, + consumer, + operation, + entity, + }: { + ruleTypeId: string; + consumer: string; + operation: string; + entity: typeof AlertingAuthorizationEntity.Alert; + }) => { + if (ruleTypeId === 'apm.error_rate' && consumer === 'apm') { + return Promise.resolve(); + } + return Promise.reject(new Error(`Unauthorized for ${ruleTypeId} and ${consumer}`)); + } + ); + }); + + describe('doSearch(client, sessionEntityId)', () => { + it('should return an empty events array for a non existant entity_id', async () => { + const esClient = elasticsearchServiceMock.createElasticsearchClient(getEmptyResponse()); + const alertsClient = new AlertsClient({ ...alertsClientParams, esClient }); + const body = await doSearch(alertsClient, 'asdf'); + + expect(body.events.length).toBe(0); + }); + + it('returns results for a particular session entity_id', async () => { + const esClient = elasticsearchServiceMock.createElasticsearchClient(getResponse()); + const alertsClient = new AlertsClient({ ...alertsClientParams, esClient }); + + const body = await doSearch(alertsClient, 'asdf'); + + expect(body.events.length).toBe(mockEvents.length); + }); + }); +}); diff --git a/x-pack/plugins/session_view/server/routes/alerts_route.ts b/x-pack/plugins/session_view/server/routes/alerts_route.ts new file mode 100644 index 00000000000000..3d03cb5cb82143 --- /dev/null +++ b/x-pack/plugins/session_view/server/routes/alerts_route.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 { schema } from '@kbn/config-schema'; +import { IRouter } from '../../../../../src/core/server'; +import { + ALERTS_ROUTE, + ALERTS_PER_PAGE, + ENTRY_SESSION_ENTITY_ID_PROPERTY, +} from '../../common/constants'; +import { expandDottedObject } from '../../common/utils/expand_dotted_object'; +import type { AlertsClient, RuleRegistryPluginStartContract } from '../../../rule_registry/server'; + +export const registerAlertsRoute = ( + router: IRouter, + ruleRegistry: RuleRegistryPluginStartContract +) => { + router.get( + { + path: ALERTS_ROUTE, + validate: { + query: schema.object({ + sessionEntityId: schema.string(), + }), + }, + }, + async (context, request, response) => { + const client = await ruleRegistry.getRacClientWithRequest(request); + const { sessionEntityId } = request.query; + const body = await doSearch(client, sessionEntityId); + + return response.ok({ body }); + } + ); +}; + +export const doSearch = async (client: AlertsClient, sessionEntityId: string) => { + const indices = await client.getAuthorizedAlertsIndices(['siem']); + + if (!indices) { + return { events: [] }; + } + + const results = await client.find({ + query: { + match: { + [ENTRY_SESSION_ENTITY_ID_PROPERTY]: sessionEntityId, + }, + }, + track_total_hits: false, + size: ALERTS_PER_PAGE, + index: indices.join(','), + }); + + const events = results.hits.hits.map((hit: any) => { + // the alert indexes flattens many properties. this util unflattens them as session view expects structured json. + hit._source = expandDottedObject(hit._source); + + return hit; + }); + + return { events }; +}; diff --git a/x-pack/plugins/session_view/server/routes/index.ts b/x-pack/plugins/session_view/server/routes/index.ts index b8cb80dc1d1d47..17efeb5d07a7bb 100644 --- a/x-pack/plugins/session_view/server/routes/index.ts +++ b/x-pack/plugins/session_view/server/routes/index.ts @@ -6,11 +6,14 @@ */ import { IRouter } from '../../../../../src/core/server'; import { registerProcessEventsRoute } from './process_events_route'; +import { registerAlertsRoute } from './alerts_route'; import { registerAlertStatusRoute } from './alert_status_route'; import { sessionEntryLeadersRoute } from './session_entry_leaders_route'; +import { RuleRegistryPluginStartContract } from '../../../rule_registry/server'; -export const registerRoutes = (router: IRouter) => { +export const registerRoutes = (router: IRouter, ruleRegistry: RuleRegistryPluginStartContract) => { registerProcessEventsRoute(router); registerAlertStatusRoute(router); sessionEntryLeadersRoute(router); + registerAlertsRoute(router, ruleRegistry); }; diff --git a/x-pack/plugins/session_view/server/routes/process_events_route.ts b/x-pack/plugins/session_view/server/routes/process_events_route.ts index 47e2d917733d5b..7be1885c70ab12 100644 --- a/x-pack/plugins/session_view/server/routes/process_events_route.ts +++ b/x-pack/plugins/session_view/server/routes/process_events_route.ts @@ -11,10 +11,8 @@ import { PROCESS_EVENTS_ROUTE, PROCESS_EVENTS_PER_PAGE, PROCESS_EVENTS_INDEX, - ALERTS_INDEX, ENTRY_SESSION_ENTITY_ID_PROPERTY, } from '../../common/constants'; -import { expandDottedObject } from '../../common/utils/expand_dotted_object'; export const registerProcessEventsRoute = (router: IRouter) => { router.get( @@ -45,35 +43,25 @@ export const doSearch = async ( forward = true ) => { const search = await client.search({ - // TODO: move alerts into it's own route with it's own pagination. - index: [PROCESS_EVENTS_INDEX, ALERTS_INDEX], - ignore_unavailable: true, + index: [PROCESS_EVENTS_INDEX], body: { query: { match: { [ENTRY_SESSION_ENTITY_ID_PROPERTY]: sessionEntityId, }, }, - // This runtime_mappings is a temporary fix, so we are able to Query these ECS fields while they are not available - // TODO: Remove the runtime_mappings once process.entry_leader.entity_id is implemented to ECS - runtime_mappings: { - [ENTRY_SESSION_ENTITY_ID_PROPERTY]: { - type: 'keyword', - }, - }, size: PROCESS_EVENTS_PER_PAGE, - sort: [{ '@timestamp': forward ? 'asc' : 'desc' }], + // we first sort by process.start, this allows lifecycle events to load all at once for a given process, and + // avoid issues like where the session leaders 'end' event is loaded at the very end of what could be multiple pages of events + sort: [ + { 'process.start': forward ? 'asc' : 'desc' }, + { '@timestamp': forward ? 'asc' : 'desc' }, + ], search_after: cursor ? [cursor] : undefined, }, }); - const events = search.hits.hits.map((hit: any) => { - // TODO: re-eval if this is needed after moving alerts to it's own route. - // the .siem-signals-default index flattens many properties. this util unflattens them. - hit._source = expandDottedObject(hit._source); - - return hit; - }); + const events = search.hits.hits; if (!forward) { events.reverse(); diff --git a/x-pack/plugins/session_view/server/types.ts b/x-pack/plugins/session_view/server/types.ts index 0d1375081ca870..29995077ccfbe7 100644 --- a/x-pack/plugins/session_view/server/types.ts +++ b/x-pack/plugins/session_view/server/types.ts @@ -4,8 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { + RuleRegistryPluginSetupContract as RuleRegistryPluginSetup, + RuleRegistryPluginStartContract as RuleRegistryPluginStart, +} from '../../rule_registry/server'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface SessionViewSetupPlugins {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface SessionViewStartPlugins {} +export interface SessionViewSetupPlugins { + ruleRegistry: RuleRegistryPluginSetup; +} + +export interface SessionViewStartPlugins { + ruleRegistry: RuleRegistryPluginStart; +} diff --git a/x-pack/plugins/session_view/tsconfig.json b/x-pack/plugins/session_view/tsconfig.json index a99e83976a31d4..0a21d320dfb294 100644 --- a/x-pack/plugins/session_view/tsconfig.json +++ b/x-pack/plugins/session_view/tsconfig.json @@ -37,6 +37,7 @@ { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" }, { "path": "../infra/tsconfig.json" }, - { "path": "../../../src/plugins/kibana_utils/tsconfig.json" } + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../rule_registry/tsconfig.json" } ] } From d253355234e2b1b393ec7e6dc10641edfe8f900c Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Wed, 23 Mar 2022 11:46:42 -0400 Subject: [PATCH 48/64] [SearchProfiler] Handle scenario when user has no indices (#128066) --- .../application/hooks/use_request_profile.ts | 31 ++++++++- .../apps/dev_tools/searchprofiler_editor.ts | 64 +++++++++++++++---- x-pack/test/functional/config.js | 8 +++ 3 files changed, 89 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/searchprofiler/public/application/hooks/use_request_profile.ts b/x-pack/plugins/searchprofiler/public/application/hooks/use_request_profile.ts index c27ca90e6e2f28..7f5d31b781310b 100644 --- a/x-pack/plugins/searchprofiler/public/application/hooks/use_request_profile.ts +++ b/x-pack/plugins/searchprofiler/public/application/hooks/use_request_profile.ts @@ -21,6 +21,16 @@ interface ReturnValue { error?: string; } +interface ProfileResponse { + profile?: { shards: ShardSerialized[] }; + _shards: { + failed: number; + skipped: number; + total: number; + successful: number; + }; +} + const extractProfilerErrorMessage = (e: any): string | undefined => { if (e.body?.attributes?.error?.reason) { const { reason, line, col } = e.body.attributes.error; @@ -67,8 +77,7 @@ export const useRequestProfile = () => { try { const resp = await http.post< - | { ok: true; resp: { profile: { shards: ShardSerialized[] } } } - | { ok: false; err: { msg: string } } + { ok: true; resp: ProfileResponse } | { ok: false; err: { msg: string } } >('../api/searchprofiler/profile', { body: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }, @@ -78,7 +87,23 @@ export const useRequestProfile = () => { return { data: null, error: resp.err.msg }; } - return { data: resp.resp.profile.shards }; + // If a user attempts to run Search Profiler without any indices, + // _shards=0 and a "profile" output will not be returned + if (resp.resp._shards.total === 0) { + notifications.addDanger({ + 'data-test-subj': 'noShardsNotification', + title: i18n.translate('xpack.searchProfiler.errorNoShardsTitle', { + defaultMessage: 'Unable to profile', + }), + text: i18n.translate('xpack.searchProfiler.errorNoShardsDescription', { + defaultMessage: 'Verify your index input matches a valid index', + }), + }); + + return { data: null }; + } + + return { data: resp.resp.profile!.shards }; } catch (e) { const profilerErrorMessage = extractProfilerErrorMessage(e); if (profilerErrorMessage) { diff --git a/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts b/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts index 3ab27e52477a6f..9a2968a1fd8b56 100644 --- a/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts +++ b/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { asyncForEach } from '@kbn/std'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { @@ -14,6 +15,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const aceEditor = getService('aceEditor'); const retry = getService('retry'); const security = getService('security'); + const es = getService('es'); + const log = getService('log'); const editorTestSubjectSelector = 'searchProfilerEditor'; @@ -34,23 +37,23 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const okInput = [ `{ -"query": { -"match_all": {}`, + "query": { + "match_all": {}`, `{ -"query": { -"match_all": { -"test": """{ "more": "json" }"""`, + "query": { + "match_all": { + "test": """{ "more": "json" }"""`, ]; const notOkInput = [ `{ -"query": { -"match_all": { -"test": """{ "more": "json" }""`, + "query": { + "match_all": { + "test": """{ "more": "json" }""`, `{ -"query": { -"match_all": { -"test": """{ "more": "json" }""'`, + "query": { + "match_all": { + "test": """{ "more": "json" }""'`, ]; const expectHasParseErrorsToBe = (expectation: boolean) => async (inputs: string[]) => { @@ -70,5 +73,44 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await expectHasParseErrorsToBe(false)(okInput); await expectHasParseErrorsToBe(true)(notOkInput); }); + + describe('No indices', () => { + before(async () => { + // Delete any existing indices that were not properly cleaned up + try { + const indices = await es.indices.get({ + index: '*', + }); + const indexNames = Object.keys(indices); + + if (indexNames.length > 0) { + await asyncForEach(indexNames, async (indexName) => { + await es.indices.delete({ index: indexName }); + }); + } + } catch (e) { + log.debug('[Setup error] Error deleting existing indices'); + throw e; + } + }); + + it('returns error if profile is executed with no valid indices', async () => { + const input = { + query: { + match_all: {}, + }, + }; + + await aceEditor.setValue(editorTestSubjectSelector, JSON.stringify(input)); + + await testSubjects.click('profileButton'); + + await retry.waitFor('notification renders', async () => { + const notification = await testSubjects.find('noShardsNotification'); + const notificationText = await notification.getVisibleText(); + return notificationText.includes('Unable to profile'); + }); + }); + }); }); } diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 28000c3d4bac85..b7774b463d058e 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -425,6 +425,14 @@ export default async function ({ readConfigFile }) { }, global_devtools_read: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['read', 'all'], + }, + ], + }, kibana: [ { feature: { From 09f78b01b966854d63b5cd7c79e36c9f35bbd580 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 23 Mar 2022 09:33:28 -0700 Subject: [PATCH 49/64] skip suite failing es promotion (#128396) --- x-pack/test/functional/apps/lens/show_underlying_data.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/lens/show_underlying_data.ts b/x-pack/test/functional/apps/lens/show_underlying_data.ts index d6ae299baceaf6..2444e8714e0145 100644 --- a/x-pack/test/functional/apps/lens/show_underlying_data.ts +++ b/x-pack/test/functional/apps/lens/show_underlying_data.ts @@ -16,7 +16,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const find = getService('find'); const browser = getService('browser'); - describe('show underlying data', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/128396 + describe.skip('show underlying data', () => { it('should show the open button for a compatible saved visualization', async () => { await PageObjects.visualize.gotoVisualizationLandingPage(); await listingTable.searchForItemWithName('lnsXYvis'); From 2f06801f8ee18b2cdd7ce2280530fe8be479eb6c Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 23 Mar 2022 12:58:41 -0400 Subject: [PATCH 50/64] [Fleet] Fix refresh assets tab on package install (#128285) --- .../integrations/sections/epm/screens/detail/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx index d002a743e77bcc..dbd1c71da3d1b5 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx @@ -144,9 +144,13 @@ export function Detail() { // Refresh package info when status change const [oldPackageInstallStatus, setOldPackageStatus] = useState(packageInstallStatus); + useEffect(() => { + if (packageInstallStatus === 'not_installed') { + setOldPackageStatus(packageInstallStatus); + } if (oldPackageInstallStatus === 'not_installed' && packageInstallStatus === 'installed') { - setOldPackageStatus(oldPackageInstallStatus); + setOldPackageStatus(packageInstallStatus); refreshPackageInfo(); } }, [packageInstallStatus, oldPackageInstallStatus, refreshPackageInfo]); From 42e6cee204043b97eda251b5fefffdaf4008ce43 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Wed, 23 Mar 2022 19:00:00 +0100 Subject: [PATCH 51/64] [Cases] Select case modal hook hides closed and all dropdown filters by default (#128380) --- .../use_cases_add_to_existing_case_modal.test.tsx | 4 ++++ .../selector_modal/use_cases_add_to_existing_case_modal.tsx | 2 ++ 2 files changed, 6 insertions(+) diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx index df40ccd3b1e90b..b0e316e8917441 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx @@ -9,6 +9,7 @@ import { renderHook } from '@testing-library/react-hooks'; import React from 'react'; +import { CaseStatuses, StatusAll } from '../../../../common'; import { CasesContext } from '../../cases_context'; import { CasesContextStoreActionsList } from '../../cases_context/cases_context_reducer'; import { useCasesAddToExistingCaseModal } from './use_cases_add_to_existing_case_modal'; @@ -62,6 +63,9 @@ describe('use cases add to existing case modal hook', () => { expect(dispatch).toHaveBeenCalledWith( expect.objectContaining({ type: CasesContextStoreActionsList.OPEN_ADD_TO_CASE_MODAL, + payload: expect.objectContaining({ + hiddenStatuses: [CaseStatuses.closed, StatusAll], + }), }) ); }); diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx index 5341f5be4183d5..1e65fee4565b22 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx @@ -6,6 +6,7 @@ */ import { useCallback } from 'react'; +import { CaseStatuses, StatusAll } from '../../../../common'; import { AllCasesSelectorModalProps } from '.'; import { useCasesToast } from '../../../common/use_cases_toast'; import { Case } from '../../../containers/types'; @@ -44,6 +45,7 @@ export const useCasesAddToExistingCaseModal = (props: AllCasesSelectorModalProps type: CasesContextStoreActionsList.OPEN_ADD_TO_CASE_MODAL, payload: { ...props, + hiddenStatuses: [CaseStatuses.closed, StatusAll], onRowClick: (theCase?: Case) => { // when the case is undefined in the modal // the user clicked "create new case" From f49f58614f3e6fe2310f61d19a6571b49f4053a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=BCrkan=20YALAMAN?= Date: Wed, 23 Mar 2022 19:34:03 +0100 Subject: [PATCH 52/64] [App Search] Fix sorting options for elasticsearch index based engines (#128384) * Fix sorting options for elasticsearch index based engines * review changes and missing translation changes --- .../build_search_ui_config.ts | 12 +++--- .../search_experience/search_experience.tsx | 40 +++++++++++++++---- .../app_search/components/engine/types.ts | 1 + .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 6 files changed, 41 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts index 25342f24cc8726..9c06527162b813 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts @@ -9,7 +9,12 @@ import { Schema } from '../../../../shared/schema/types'; import { Fields } from './types'; -export const buildSearchUIConfig = (apiConnector: object, schema: Schema, fields: Fields) => { +export const buildSearchUIConfig = ( + apiConnector: object, + schema: Schema, + fields: Fields, + initialState = { sortDirection: 'desc', sortField: 'id' } +) => { const facets = fields.filterFields.reduce( (facetsConfig, fieldName) => ({ ...facetsConfig, @@ -22,10 +27,7 @@ export const buildSearchUIConfig = (apiConnector: object, schema: Schema, fields alwaysSearchOnInitialLoad: true, apiConnector, trackUrlState: false, - initialState: { - sortDirection: 'desc', - sortField: 'id', - }, + initialState, searchQuery: { disjunctiveFacets: fields.filterFields, facets, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx index ed2a1ed54f06d3..52e0acbc815203 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx @@ -31,25 +31,39 @@ import { SearchExperienceContent } from './search_experience_content'; import { Fields, SortOption } from './types'; import { SearchBoxView, SortingView, MultiCheckboxFacetsView } from './views'; -const RECENTLY_UPLOADED = i18n.translate( - 'xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.recentlyUploaded', +const DOCUMENT_ID = i18n.translate( + 'xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.documentId', { - defaultMessage: 'Recently Uploaded', + defaultMessage: 'Document ID', } ); + +const RELEVANCE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.relevance', + { defaultMessage: 'Relevance' } +); + const DEFAULT_SORT_OPTIONS: SortOption[] = [ { - name: DESCENDING(RECENTLY_UPLOADED), + name: DESCENDING(DOCUMENT_ID), value: 'id', direction: 'desc', }, { - name: ASCENDING(RECENTLY_UPLOADED), + name: ASCENDING(DOCUMENT_ID), value: 'id', direction: 'asc', }, ]; +const RELEVANCE_SORT_OPTIONS: SortOption[] = [ + { + name: RELEVANCE, + value: '_score', + direction: 'desc', + }, +]; + export const SearchExperience: React.FC = () => { const { engine } = useValues(EngineLogic); const { http } = useValues(HttpLogic); @@ -66,8 +80,10 @@ export const SearchExperience: React.FC = () => { sortFields: [], } ); + const sortOptions = + engine.type === 'elasticsearch' ? RELEVANCE_SORT_OPTIONS : DEFAULT_SORT_OPTIONS; - const sortingOptions = buildSortOptions(fields, DEFAULT_SORT_OPTIONS); + const sortingOptions = buildSortOptions(fields, sortOptions); const connector = new AppSearchAPIConnector({ cacheResponses: false, @@ -78,7 +94,17 @@ export const SearchExperience: React.FC = () => { }, }); - const searchProviderConfig = buildSearchUIConfig(connector, engine.schema || {}, fields); + const initialState = { + sortField: engine.type === 'elasticsearch' ? '_score' : 'id', + sortDirection: 'desc', + }; + + const searchProviderConfig = buildSearchUIConfig( + connector, + engine.schema || {}, + fields, + initialState + ); return (
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts index 6faa749f958646..acdeed4854ecd8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts @@ -12,6 +12,7 @@ export enum EngineTypes { default = 'default', indexed = 'indexed', meta = 'meta', + elasticsearch = 'elasticsearch', } export interface Engine { name: string; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 8914efcf12ded4..db10095ce05911 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -8729,7 +8729,6 @@ "xpack.enterpriseSearch.appSearch.documents.search.sortBy.ariaLabel": "Trier les résultats par", "xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.ascendingDropDownOptionLabel": "{fieldName} (croiss.)", "xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.descendingDropDownOptionLabel": "{fieldName} (décroiss.)", - "xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.recentlyUploaded": "Récemment chargé", "xpack.enterpriseSearch.appSearch.documents.title": "Documents", "xpack.enterpriseSearch.appSearch.editorRoleTypeDescription": "Les éditeurs peuvent gérer les paramètres de recherche.", "xpack.enterpriseSearch.appSearch.emptyState.createFirstEngineCta": "Créer un moteur", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 48f0d74d73765c..f1ab772dbb2439 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10279,7 +10279,6 @@ "xpack.enterpriseSearch.appSearch.documents.search.sortBy.ariaLabel": "結果の並べ替え条件", "xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.ascendingDropDownOptionLabel": "{fieldName}(昇順)", "xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.descendingDropDownOptionLabel": "{fieldName}(降順)", - "xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.recentlyUploaded": "最近アップロードされたドキュメント", "xpack.enterpriseSearch.appSearch.documents.title": "ドキュメント", "xpack.enterpriseSearch.appSearch.editorRoleTypeDescription": "エディターは検索設定を管理できます。", "xpack.enterpriseSearch.appSearch.emptyState.createFirstEngineCta": "エンジンを作成", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index bbc00d8d205f7a..51c4915baab297 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10300,7 +10300,6 @@ "xpack.enterpriseSearch.appSearch.documents.search.sortBy.ariaLabel": "结果排序方式", "xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.ascendingDropDownOptionLabel": "{fieldName}(升序)", "xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.descendingDropDownOptionLabel": "{fieldName}(降序)", - "xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.recentlyUploaded": "最近上传", "xpack.enterpriseSearch.appSearch.documents.title": "文档", "xpack.enterpriseSearch.appSearch.editorRoleTypeDescription": "编辑人员可以管理搜索设置。", "xpack.enterpriseSearch.appSearch.emptyState.createFirstEngineCta": "创建引擎", From 98300c236404d0378caf26de08b0866877f997b2 Mon Sep 17 00:00:00 2001 From: Ashokaditya <1849116+ashokaditya@users.noreply.github.com> Date: Wed, 23 Mar 2022 19:39:38 +0100 Subject: [PATCH 53/64] [Security Solution][Endpoint] Accept all kinds of filenames (without wildcard) in wildcard-ed event filter `file.path.text` (#127432) * update filename regex to include multiple hyphens and periods Uses a much simpler pattern that covers a whole gamut file name patterns. fixes elastic/security-team/issues/3294 * remove duplicated code * add tests for `process.name` entry for filenames with wildcard path refs elastic/kibana/pull/120349 elastic/kibana/pull/125202 * Add file.name optimized entry when wildcard filepath in file.path.text has a filename fixes elastic/security-team/issues/3294 * update regex to include unicode chars review changes * add tests for `file.name` and `process.name` entries if it already exists This works out of the box and we don't add endpoint related `file.name` or `process.name` entry when it already is added by the user refs elastic/kibana/pull/127958#discussion_r829086447 elastic/security-team/issues/3199 * fix `file.name` and `file.path.text` entries for linux and mac/linux refs elastic/kibana/pull/127098 * do not add endpoint optimized entry Add `file.name` and `process.name` entry for wildcard path values only when file.name and process.name entries do not already exist. The earlier commit 8a516ae9c0580eb44b57666e7a5934c543c3e4bb was mistakenly labeled as this worked out of the box. In the same commit we notice that the test data had a wildcard file path that did not add a `file.name` or `process.name` entry. For more see: elastic/kibana/pull/127958#discussion_r829086447 elastic/security-team/issues/3199 * update regex to include gamut of unicode characters review suggestions * remove regex altogether simplifies the logic to check if path is without wildcard characters. This way it includes all other strings as valid filenames that do not have * or ? * update artifact creation for `file.path.text` entries Similar to when we normalize `file.path.caseless` entries, except that the `type` is `*_cased` for linux and `*_caseless` for non-linux --- .../src/path_validations/index.test.ts | 89 ++- .../src/path_validations/index.ts | 25 +- .../endpoint/lib/artifacts/lists.test.ts | 616 ++++++++++++++++++ .../server/endpoint/lib/artifacts/lists.ts | 50 +- .../manifest_manager/manifest_manager.ts | 109 ++-- 5 files changed, 790 insertions(+), 99 deletions(-) diff --git a/packages/kbn-securitysolution-utils/src/path_validations/index.test.ts b/packages/kbn-securitysolution-utils/src/path_validations/index.test.ts index ee2d8764a30afb..5bb84816b16023 100644 --- a/packages/kbn-securitysolution-utils/src/path_validations/index.test.ts +++ b/packages/kbn-securitysolution-utils/src/path_validations/index.test.ts @@ -20,10 +20,31 @@ describe('validateFilePathInput', () => { describe('windows', () => { const os = OperatingSystem.WINDOWS; + it('does not warn on valid filenames', () => { + expect( + validateFilePathInput({ + os, + value: 'C:\\Windows\\*\\FILENAME.EXE-1231205124.gz', + }) + ).not.toBeDefined(); + expect( + validateFilePathInput({ + os, + value: "C:\\Windows\\*\\test$ as2@13---12!@#A,DS.#$^&$!#~ 'as'd.华语.txt", + }) + ).toEqual(undefined); + }); + it('warns on wildcard in file name at the end of the path', () => { expect(validateFilePathInput({ os, value: 'c:\\path*.exe' })).toEqual( FILENAME_WILDCARD_WARNING ); + expect( + validateFilePathInput({ + os, + value: 'C:\\Windows\\*\\FILENAME.EXE-*.gz', + }) + ).toEqual(FILENAME_WILDCARD_WARNING); }); it('warns on unix paths or non-windows paths', () => { @@ -34,6 +55,7 @@ describe('validateFilePathInput', () => { expect(validateFilePathInput({ os, value: 'c:\\path/opt' })).toEqual(FILEPATH_WARNING); expect(validateFilePathInput({ os, value: '1242' })).toEqual(FILEPATH_WARNING); expect(validateFilePathInput({ os, value: 'w12efdfa' })).toEqual(FILEPATH_WARNING); + expect(validateFilePathInput({ os, value: 'c:\\folder\\' })).toEqual(FILEPATH_WARNING); }); }); describe('unix paths', () => { @@ -42,8 +64,22 @@ describe('validateFilePathInput', () => { ? OperatingSystem.MAC : OperatingSystem.LINUX; + it('does not warn on valid filenames', () => { + expect(validateFilePathInput({ os, value: '/opt/*/FILENAME.EXE-1231205124.gz' })).not.toEqual( + FILENAME_WILDCARD_WARNING + ); + expect( + validateFilePathInput({ + os, + value: "/opt/*/test$ as2@13---12!@#A,DS.#$^&$!#~ 'as'd.华语.txt", + }) + ).not.toEqual(FILENAME_WILDCARD_WARNING); + }); it('warns on wildcard in file name at the end of the path', () => { expect(validateFilePathInput({ os, value: '/opt/bin*' })).toEqual(FILENAME_WILDCARD_WARNING); + expect(validateFilePathInput({ os, value: '/opt/FILENAME.EXE-*.gz' })).toEqual( + FILENAME_WILDCARD_WARNING + ); }); it('warns on windows paths', () => { @@ -54,6 +90,7 @@ describe('validateFilePathInput', () => { expect(validateFilePathInput({ os, value: 'opt/bin\\file.exe' })).toEqual(FILEPATH_WARNING); expect(validateFilePathInput({ os, value: '1242' })).toEqual(FILEPATH_WARNING); expect(validateFilePathInput({ os, value: 'w12efdfa' })).toEqual(FILEPATH_WARNING); + expect(validateFilePathInput({ os, value: '/folder/' })).toEqual(FILEPATH_WARNING); }); }); }); @@ -577,50 +614,82 @@ describe('Unacceptable Mac/Linux exact paths', () => { }); }); -describe('Executable filenames with wildcard PATHS', () => { +describe('hasSimpleExecutableName', () => { it('should return TRUE when MAC/LINUX wildcard paths have an executable name', () => { + const os = + parseInt((Math.random() * 2).toString(), 10) === 1 + ? OperatingSystem.MAC + : OperatingSystem.LINUX; + expect( hasSimpleExecutableName({ - os: OperatingSystem.LINUX, + os, type: 'wildcard', value: '/opt/*/app', }) ).toEqual(true); expect( hasSimpleExecutableName({ - os: OperatingSystem.MAC, + os, type: 'wildcard', value: '/op*/**/app.dmg', }) ).toEqual(true); - }); - - it('should return TRUE when WINDOWS wildcards paths have a executable name', () => { expect( hasSimpleExecutableName({ - os: OperatingSystem.WINDOWS, + os, type: 'wildcard', - value: 'c:\\**\\path.exe', + value: "/sy*/test$ as2@13---12!@#A,DS.#$^&$!#~ 'as'd.华语.txt", }) ).toEqual(true); }); it('should return FALSE when MAC/LINUX wildcard paths have a wildcard in executable name', () => { + const os = + parseInt((Math.random() * 2).toString(), 10) === 1 + ? OperatingSystem.MAC + : OperatingSystem.LINUX; + expect( hasSimpleExecutableName({ - os: OperatingSystem.LINUX, + os, type: 'wildcard', value: '/op/*/*pp', }) ).toEqual(false); expect( hasSimpleExecutableName({ - os: OperatingSystem.MAC, + os, type: 'wildcard', value: '/op*/b**/ap.m**', }) ).toEqual(false); }); + + it('should return TRUE when WINDOWS wildcards paths have a executable name', () => { + expect( + hasSimpleExecutableName({ + os: OperatingSystem.WINDOWS, + type: 'wildcard', + value: 'c:\\**\\path.exe', + }) + ).toEqual(true); + expect( + hasSimpleExecutableName({ + os: OperatingSystem.WINDOWS, + type: 'wildcard', + value: 'C:\\*\\file-name.path华语 1234.txt', + }) + ).toEqual(true); + expect( + hasSimpleExecutableName({ + os: OperatingSystem.WINDOWS, + type: 'wildcard', + value: "C:\\*\\test$ as2@13---12!@#A,DS.#$^&$!#~ 'as'd.华语.txt", + }) + ).toEqual(true); + }); + it('should return FALSE when WINDOWS wildcards paths have a wildcard in executable name', () => { expect( hasSimpleExecutableName({ diff --git a/packages/kbn-securitysolution-utils/src/path_validations/index.ts b/packages/kbn-securitysolution-utils/src/path_validations/index.ts index 97a726703feef2..b64cb4cf6a0524 100644 --- a/packages/kbn-securitysolution-utils/src/path_validations/index.ts +++ b/packages/kbn-securitysolution-utils/src/path_validations/index.ts @@ -31,20 +31,6 @@ export const enum OperatingSystem { export type EntryTypes = 'match' | 'wildcard' | 'match_any'; export type TrustedAppEntryTypes = Extract; -/* - * regex to match executable names - * starts matching from the eol of the path - * file names with a single or multiple spaces (for spaced names) - * and hyphens and combinations of these that produce complex names - * such as: - * c:\home\lib\dmp.dmp - * c:\home\lib\my-binary-app-+/ some/ x/ dmp.dmp - * /home/lib/dmp.dmp - * /home/lib/my-binary-app+-\ some\ x\ dmp.dmp - */ -export const WIN_EXEC_PATH = /(\\[-\w]+|\\[-\w]+[\.]+[\w]+)$/i; -export const UNIX_EXEC_PATH = /(\/[-\w]+|\/[-\w]+[\.]+[\w]+)$/i; - export const validateFilePathInput = ({ os, value = '', @@ -70,7 +56,7 @@ export const validateFilePathInput = ({ } if (isValidFilePath) { - if (!hasSimpleFileName) { + if (hasSimpleFileName !== undefined && !hasSimpleFileName) { return FILENAME_WILDCARD_WARNING; } } else { @@ -86,9 +72,14 @@ export const hasSimpleExecutableName = ({ os: OperatingSystem; type: EntryTypes; value: string; -}): boolean => { +}): boolean | undefined => { + const separator = os === OperatingSystem.WINDOWS ? '\\' : '/'; + const lastString = value.split(separator).pop(); + if (!lastString) { + return; + } if (type === 'wildcard') { - return os === OperatingSystem.WINDOWS ? WIN_EXEC_PATH.test(value) : UNIX_EXEC_PATH.test(value); + return (lastString.split('*').length || lastString.split('?').length) === 1; } return true; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts index 83dbcf1ca6f6de..179ea3827df0c4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts @@ -513,6 +513,622 @@ describe('artifacts lists', () => { }); }); + describe('Endpoint Artifacts', () => { + const getOsFilter = (os: 'macos' | 'linux' | 'windows') => + `exception-list-agnostic.attributes.os_types:"${os} "`; + + describe('linux', () => { + test('it should add process.name entry when wildcard process.executable entry has filename', async () => { + const os = 'linux'; + const testEntries: EntriesArray = [ + { + field: 'process.executable.caseless', + operator: 'included', + type: 'wildcard', + value: '/usr/bi*/doc.md', + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'process.executable', + operator: 'included', + type: 'wildcard_cased', + value: '/usr/bi*/doc.md', + }, + { + field: 'process.name', + operator: 'included', + type: 'exact_cased', + value: 'doc.md', + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should add file.name entry when wildcard file.path.text entry has filename', async () => { + const os = 'linux'; + const testEntries: EntriesArray = [ + { + field: 'file.path.text', + operator: 'included', + type: 'wildcard', + value: '/usr/bi*/doc.md', + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'file.path', + operator: 'included', + type: 'wildcard_cased', + value: '/usr/bi*/doc.md', + }, + { + field: 'file.name', + operator: 'included', + type: 'exact_cased', + value: 'doc.md', + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should not add process.name entry when wildcard process.executable entry has wildcard filename', async () => { + const os = 'linux'; + const testEntries: EntriesArray = [ + { + field: 'process.executable.caseless', + operator: 'included', + type: 'wildcard', + value: '/usr/bin/*.md', + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'process.executable', + operator: 'included', + type: 'wildcard_cased', + value: '/usr/bin/*.md', + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should not add process.name entry when process.name entry already exists', async () => { + const os = 'linux'; + const testEntries: EntriesArray = [ + { + field: 'process.executable.caseless', + operator: 'included', + type: 'wildcard', + value: '/usr/bi*/donotadd.md', + }, + { + field: 'process.name', + operator: 'included', + type: 'match', + value: 'appname.exe', + }, + { + field: 'process.name', + operator: 'included', + type: 'match_any', + value: ['one.exe', 'two.exe'], + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'process.executable', + operator: 'included', + type: 'wildcard_cased', + value: '/usr/bi*/donotadd.md', + }, + { + field: 'process.name', + operator: 'included', + type: 'exact_cased', + value: 'appname.exe', + }, + { + field: 'process.name', + operator: 'included', + type: 'exact_cased_any', + value: ['one.exe', 'two.exe'], + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should not add file.name entry when wildcard file.path.text entry has wildcard filename', async () => { + const os = 'linux'; + const testEntries: EntriesArray = [ + { + field: 'file.path.text', + operator: 'included', + type: 'wildcard', + value: '/usr/bin/*.md', + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'file.path', + operator: 'included', + type: 'wildcard_cased', + value: '/usr/bin/*.md', + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should not add file.name entry when file.name entry already exists', async () => { + const os = 'linux'; + const testEntries: EntriesArray = [ + { + field: 'file.path.text', + operator: 'included', + type: 'wildcard', + value: '/usr/b*/donotadd.md', + }, + { + field: 'file.name', + operator: 'included', + type: 'match', + value: 'filename.app', + }, + { + field: 'file.name', + operator: 'included', + type: 'match_any', + value: ['one.app', 'two.app'], + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'file.path', + operator: 'included', + type: 'wildcard_cased', + value: '/usr/b*/donotadd.md', + }, + { + field: 'file.name', + operator: 'included', + type: 'exact_cased', + value: 'filename.app', + }, + { + field: 'file.name', + operator: 'included', + type: 'exact_cased_any', + value: ['one.app', 'two.app'], + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + }); + + describe('macos/windows', () => { + test('it should add process.name entry for process.executable entry with wildcard type', async () => { + const os = Math.floor(Math.random() * 2) === 0 ? 'windows' : 'macos'; + const value = os === 'windows' ? 'C:\\My Doc*\\doc.md' : '/usr/bi*/doc.md'; + + const testEntries: EntriesArray = [ + { + field: 'process.executable.caseless', + operator: 'included', + type: 'wildcard', + value, + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'process.executable', + operator: 'included', + type: 'wildcard_caseless', + value, + }, + { + field: 'process.name', + operator: 'included', + type: 'exact_caseless', + value: 'doc.md', + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should add file.name entry when wildcard file.path.text entry has filename', async () => { + const os = Math.floor(Math.random() * 2) === 0 ? 'windows' : 'macos'; + const value = os === 'windows' ? 'C:\\My Doc*\\doc.md' : '/usr/bi*/doc.md'; + + const testEntries: EntriesArray = [ + { + field: 'file.path.text', + operator: 'included', + type: 'wildcard', + value, + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'file.path', + operator: 'included', + type: 'wildcard_caseless', + value, + }, + { + field: 'file.name', + operator: 'included', + type: 'exact_caseless', + value: 'doc.md', + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should not add process.name entry when wildcard process.executable entry has wildcard filename', async () => { + const os = Math.floor(Math.random() * 2) === 0 ? 'windows' : 'macos'; + const value = os === 'windows' ? 'C:\\My Doc*\\*.md' : '/usr/bin/*.md'; + + const testEntries: EntriesArray = [ + { + field: 'process.executable.caseless', + operator: 'included', + type: 'wildcard', + value, + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'process.executable', + operator: 'included', + type: 'wildcard_caseless', + value, + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should not add process.name entry when process.name entry already exists', async () => { + const os = Math.floor(Math.random() * 2) === 0 ? 'windows' : 'macos'; + const value = os === 'windows' ? 'C:\\My Doc*\\donotadd.md' : '/usr/bin/donotadd.md'; + + const testEntries: EntriesArray = [ + { + field: 'process.executable.caseless', + operator: 'included', + type: 'wildcard', + value, + }, + { + field: 'process.name', + operator: 'included', + type: 'match', + value: 'appname.exe', + }, + { + field: 'process.name', + operator: 'included', + type: 'match_any', + value: ['one.exe', 'two.exe'], + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'process.executable', + operator: 'included', + type: 'wildcard_caseless', + value, + }, + { + field: 'process.name', + operator: 'included', + type: 'exact_caseless', + value: 'appname.exe', + }, + { + field: 'process.name', + operator: 'included', + type: 'exact_caseless_any', + value: ['one.exe', 'two.exe'], + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should not add file.name entry when wildcard file.path.text entry has wildcard filename', async () => { + const os = Math.floor(Math.random() * 2) === 0 ? 'windows' : 'macos'; + const value = os === 'windows' ? 'C:\\My Doc*\\*.md' : '/usr/bin/*.md'; + const testEntries: EntriesArray = [ + { + field: 'file.path.text', + operator: 'included', + type: 'wildcard', + value, + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'file.path', + operator: 'included', + type: 'wildcard_caseless', + value, + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should not add file.name entry when file.name entry already exists', async () => { + const os = Math.floor(Math.random() * 2) === 0 ? 'windows' : 'macos'; + const value = os === 'windows' ? 'C:\\My Doc*\\donotadd.md' : '/usr/bin/donotadd.md'; + + const testEntries: EntriesArray = [ + { + field: 'file.path.text', + operator: 'included', + type: 'wildcard', + value, + }, + { + field: 'file.name', + operator: 'included', + type: 'match', + value: 'filename.app', + }, + { + field: 'file.name', + operator: 'included', + type: 'match_any', + value: ['one.app', 'two.app'], + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'file.path', + operator: 'included', + type: 'wildcard_caseless', + value, + }, + { + field: 'file.name', + operator: 'included', + type: 'exact_caseless', + value: 'filename.app', + }, + { + field: 'file.name', + operator: 'included', + type: 'exact_caseless_any', + value: ['one.app', 'two.app'], + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + }); + }); + const TEST_EXCEPTION_LIST_ITEM = { entries: [ { diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index 7521ccbf9df915..2ea52485e625bb 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -187,11 +187,16 @@ function getMatcherFunction({ matchAny?: boolean; os: ExceptionListItemSchema['os_types'][number]; }): TranslatedEntryMatcher { + const doesFieldEndWith: boolean = + field.endsWith('.caseless') || field.endsWith('.name') || field.endsWith('.text'); + return matchAny - ? field.endsWith('.caseless') && os !== 'linux' - ? 'exact_caseless_any' + ? doesFieldEndWith + ? os === 'linux' + ? 'exact_cased_any' + : 'exact_caseless_any' : 'exact_cased_any' - : field.endsWith('.caseless') + : doesFieldEndWith ? os === 'linux' ? 'exact_cased' : 'exact_caseless' @@ -213,7 +218,9 @@ function getMatcherWildcardFunction({ } function normalizeFieldName(field: string): string { - return field.endsWith('.caseless') ? field.substring(0, field.lastIndexOf('.')) : field; + return field.endsWith('.caseless') || field.endsWith('.text') + ? field.substring(0, field.lastIndexOf('.')) + : field; } function translateItem( @@ -223,7 +230,7 @@ function translateItem( const itemSet = new Set(); const getEntries = (): TranslatedExceptionListItem['entries'] => { return item.entries.reduce((translatedEntries, entry) => { - const translatedEntry = translateEntry(schemaVersion, entry, item.os_types[0]); + const translatedEntry = translateEntry(schemaVersion, item.entries, entry, item.os_types[0]); if (translatedEntry !== undefined) { if (translatedEntryType.is(translatedEntry)) { @@ -256,12 +263,11 @@ function translateItem( }; } -function appendProcessNameEntry({ - wildcardProcessEntry, +function appendOptimizedEntryForEndpoint({ entry, os, + wildcardProcessEntry, }: { - wildcardProcessEntry: TranslatedEntryMatchWildcard; entry: { field: string; operator: 'excluded' | 'included'; @@ -269,11 +275,15 @@ function appendProcessNameEntry({ value: string; }; os: ExceptionListItemSchema['os_types'][number]; + wildcardProcessEntry: TranslatedEntryMatchWildcard; }): TranslatedPerformantEntries { const entries: TranslatedPerformantEntries = [ wildcardProcessEntry, { - field: normalizeFieldName('process.name'), + field: + entry.field === 'file.path.text' + ? normalizeFieldName('file.name') + : normalizeFieldName('process.name'), operator: entry.operator, type: (os === 'linux' ? 'exact_cased' : 'exact_caseless') as Extract< TranslatedEntryMatcher, @@ -291,6 +301,7 @@ function appendProcessNameEntry({ function translateEntry( schemaVersion: string, + exceptionListItemEntries: ExceptionListItemSchema['entries'], entry: Entry | EntryNested, os: ExceptionListItemSchema['os_types'][number] ): TranslatedEntry | TranslatedPerformantEntries | undefined { @@ -298,7 +309,12 @@ function translateEntry( case 'nested': { const nestedEntries = entry.entries.reduce( (entries, nestedEntry) => { - const translatedEntry = translateEntry(schemaVersion, nestedEntry, os); + const translatedEntry = translateEntry( + schemaVersion, + exceptionListItemEntries, + nestedEntry, + os + ); if (nestedEntry !== undefined && translatedEntryNestedEntry.is(translatedEntry)) { entries.push(translatedEntry); } @@ -354,11 +370,21 @@ function translateEntry( type: entry.type, value: entry.value, }); - if (hasExecutableName) { + + const existingFields = exceptionListItemEntries.map((e) => e.field); + const doAddPerformantEntries = !( + existingFields.includes('process.name') || existingFields.includes('file.name') + ); + + if (hasExecutableName && doAddPerformantEntries) { // when path has a full executable name // append a process.name entry based on os // `exact_cased` for linux and `exact_caseless` for others - return appendProcessNameEntry({ entry, os, wildcardProcessEntry }); + return appendOptimizedEntryForEndpoint({ + entry, + os, + wildcardProcessEntry, + }); } else { return wildcardProcessEntry; } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index 7be2a36396a715..a8c63bbb88e132 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -31,6 +31,7 @@ import { getArtifactId, getEndpointExceptionList, Manifest, + ArtifactListId, } from '../../../lib/artifacts'; import { InternalArtifactCompleteSchema, @@ -48,6 +49,11 @@ interface ArtifactsBuildResult { policySpecificArtifacts: Record; } +interface BuildArtifactsForOsOptions { + listId: ArtifactListId; + name: string; +} + const iterateArtifactsBuildResult = async ( result: ArtifactsBuildResult, callback: (artifact: InternalArtifactCompleteSchema, policyId?: string) => Promise @@ -174,20 +180,29 @@ export class ManifestManager { /** * Builds an artifact (one per supported OS) based on the current state of the - * Trusted Apps list (which uses the `exception-list-agnostic` SO type) + * artifacts list (Trusted Apps, Host Iso. Exceptions, Event Filters, Blocklists) + * (which uses the `exception-list-agnostic` SO type) */ - protected async buildTrustedAppsArtifact(os: string, policyId?: string) { + protected async buildArtifactsForOs({ + listId, + name, + os, + policyId, + }: { + os: string; + policyId?: string; + } & BuildArtifactsForOsOptions): Promise { return buildArtifact( await getEndpointExceptionList({ elClient: this.exceptionListClient, schemaVersion: this.schemaVersion, os, policyId, - listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + listId, }), this.schemaVersion, os, - ArtifactConstants.GLOBAL_TRUSTED_APPS_NAME + name ); } @@ -198,9 +213,13 @@ export class ManifestManager { protected async buildTrustedAppsArtifacts(): Promise { const defaultArtifacts: InternalArtifactCompleteSchema[] = []; const policySpecificArtifacts: Record = {}; + const buildArtifactsForOsOptions: BuildArtifactsForOsOptions = { + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + name: ArtifactConstants.GLOBAL_TRUSTED_APPS_NAME, + }; for (const os of ArtifactConstants.SUPPORTED_TRUSTED_APPS_OPERATING_SYSTEMS) { - defaultArtifacts.push(await this.buildTrustedAppsArtifact(os)); + defaultArtifacts.push(await this.buildArtifactsForOs({ os, ...buildArtifactsForOsOptions })); } await iterateAllListItems( @@ -208,7 +227,9 @@ export class ManifestManager { async (policyId) => { for (const os of ArtifactConstants.SUPPORTED_TRUSTED_APPS_OPERATING_SYSTEMS) { policySpecificArtifacts[policyId] = policySpecificArtifacts[policyId] || []; - policySpecificArtifacts[policyId].push(await this.buildTrustedAppsArtifact(os, policyId)); + policySpecificArtifacts[policyId].push( + await this.buildArtifactsForOs({ os, policyId, ...buildArtifactsForOsOptions }) + ); } } ); @@ -224,9 +245,13 @@ export class ManifestManager { protected async buildEventFiltersArtifacts(): Promise { const defaultArtifacts: InternalArtifactCompleteSchema[] = []; const policySpecificArtifacts: Record = {}; + const buildArtifactsForOsOptions: BuildArtifactsForOsOptions = { + listId: ENDPOINT_EVENT_FILTERS_LIST_ID, + name: ArtifactConstants.GLOBAL_EVENT_FILTERS_NAME, + }; for (const os of ArtifactConstants.SUPPORTED_EVENT_FILTERS_OPERATING_SYSTEMS) { - defaultArtifacts.push(await this.buildEventFiltersForOs(os)); + defaultArtifacts.push(await this.buildArtifactsForOs({ os, ...buildArtifactsForOsOptions })); } await iterateAllListItems( @@ -234,7 +259,9 @@ export class ManifestManager { async (policyId) => { for (const os of ArtifactConstants.SUPPORTED_EVENT_FILTERS_OPERATING_SYSTEMS) { policySpecificArtifacts[policyId] = policySpecificArtifacts[policyId] || []; - policySpecificArtifacts[policyId].push(await this.buildEventFiltersForOs(os, policyId)); + policySpecificArtifacts[policyId].push( + await this.buildArtifactsForOs({ os, policyId, ...buildArtifactsForOsOptions }) + ); } } ); @@ -242,21 +269,6 @@ export class ManifestManager { return { defaultArtifacts, policySpecificArtifacts }; } - protected async buildEventFiltersForOs(os: string, policyId?: string) { - return buildArtifact( - await getEndpointExceptionList({ - elClient: this.exceptionListClient, - schemaVersion: this.schemaVersion, - os, - policyId, - listId: ENDPOINT_EVENT_FILTERS_LIST_ID, - }), - this.schemaVersion, - os, - ArtifactConstants.GLOBAL_EVENT_FILTERS_NAME - ); - } - /** * Builds an array of Blocklist entries (one per supported OS) based on the current state of the * Blocklist list @@ -265,9 +277,13 @@ export class ManifestManager { protected async buildBlocklistArtifacts(): Promise { const defaultArtifacts: InternalArtifactCompleteSchema[] = []; const policySpecificArtifacts: Record = {}; + const buildArtifactsForOsOptions: BuildArtifactsForOsOptions = { + listId: ENDPOINT_BLOCKLISTS_LIST_ID, + name: ArtifactConstants.GLOBAL_BLOCKLISTS_NAME, + }; for (const os of ArtifactConstants.SUPPORTED_EVENT_FILTERS_OPERATING_SYSTEMS) { - defaultArtifacts.push(await this.buildBlocklistForOs(os)); + defaultArtifacts.push(await this.buildArtifactsForOs({ os, ...buildArtifactsForOsOptions })); } await iterateAllListItems( @@ -275,7 +291,9 @@ export class ManifestManager { async (policyId) => { for (const os of ArtifactConstants.SUPPORTED_EVENT_FILTERS_OPERATING_SYSTEMS) { policySpecificArtifacts[policyId] = policySpecificArtifacts[policyId] || []; - policySpecificArtifacts[policyId].push(await this.buildBlocklistForOs(os, policyId)); + policySpecificArtifacts[policyId].push( + await this.buildArtifactsForOs({ os, policyId, ...buildArtifactsForOsOptions }) + ); } } ); @@ -283,21 +301,6 @@ export class ManifestManager { return { defaultArtifacts, policySpecificArtifacts }; } - protected async buildBlocklistForOs(os: string, policyId?: string) { - return buildArtifact( - await getEndpointExceptionList({ - elClient: this.exceptionListClient, - schemaVersion: this.schemaVersion, - os, - policyId, - listId: ENDPOINT_BLOCKLISTS_LIST_ID, - }), - this.schemaVersion, - os, - ArtifactConstants.GLOBAL_BLOCKLISTS_NAME - ); - } - /** * Builds an array of endpoint host isolation exception (one per supported OS) based on the current state of the * Host Isolation Exception List @@ -307,9 +310,13 @@ export class ManifestManager { protected async buildHostIsolationExceptionsArtifacts(): Promise { const defaultArtifacts: InternalArtifactCompleteSchema[] = []; const policySpecificArtifacts: Record = {}; + const buildArtifactsForOsOptions: BuildArtifactsForOsOptions = { + listId: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, + name: ArtifactConstants.GLOBAL_HOST_ISOLATION_EXCEPTIONS_NAME, + }; for (const os of ArtifactConstants.SUPPORTED_HOST_ISOLATION_EXCEPTIONS_OPERATING_SYSTEMS) { - defaultArtifacts.push(await this.buildHostIsolationExceptionForOs(os)); + defaultArtifacts.push(await this.buildArtifactsForOs({ os, ...buildArtifactsForOsOptions })); } await iterateAllListItems( @@ -318,7 +325,7 @@ export class ManifestManager { for (const os of ArtifactConstants.SUPPORTED_HOST_ISOLATION_EXCEPTIONS_OPERATING_SYSTEMS) { policySpecificArtifacts[policyId] = policySpecificArtifacts[policyId] || []; policySpecificArtifacts[policyId].push( - await this.buildHostIsolationExceptionForOs(os, policyId) + await this.buildArtifactsForOs({ os, policyId, ...buildArtifactsForOsOptions }) ); } } @@ -327,24 +334,6 @@ export class ManifestManager { return { defaultArtifacts, policySpecificArtifacts }; } - protected async buildHostIsolationExceptionForOs( - os: string, - policyId?: string - ): Promise { - return buildArtifact( - await getEndpointExceptionList({ - elClient: this.exceptionListClient, - schemaVersion: this.schemaVersion, - os, - policyId, - listId: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, - }), - this.schemaVersion, - os, - ArtifactConstants.GLOBAL_HOST_ISOLATION_EXCEPTIONS_NAME - ); - } - /** * Writes new artifact SO. * From 5e73ef53277aae2da5e94b10d1fe6138a4721db1 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 23 Mar 2022 12:41:27 -0600 Subject: [PATCH 54/64] [Security Solution] Collapse KPI and Table queries on Explore pages (#127930) --- .../__snapshots__/index.test.tsx.snap | 28 +- .../components/header_section/index.test.tsx | 90 +++++ .../components/header_section/index.tsx | 133 ++++--- .../matrix_histogram/index.test.tsx | 89 ++++- .../components/matrix_histogram/index.tsx | 45 ++- .../matrix_histogram/matrix_loader.tsx | 2 +- .../ml/anomaly/use_anomalies_table_data.ts | 4 +- .../ml/tables/anomalies_host_table.test.tsx | 88 +++++ .../ml/tables/anomalies_host_table.tsx | 40 +- .../tables/anomalies_network_table.test.tsx | 90 +++++ .../ml/tables/anomalies_network_table.tsx | 41 +- .../ml/tables/anomalies_user_table.test.tsx | 89 +++++ .../ml/tables/anomalies_user_table.tsx | 39 +- .../__snapshots__/index.test.tsx.snap | 3 + .../components/paginated_table/index.test.tsx | 365 ++++-------------- .../components/paginated_table/index.tsx | 113 +++--- .../components/stat_items/index.test.tsx | 198 ++++++---- .../common/components/stat_items/index.tsx | 262 +++++++------ .../containers/matrix_histogram/index.test.ts | 13 +- .../containers/matrix_histogram/index.ts | 8 + .../containers/query_toggle/index.test.tsx | 74 ++++ .../common/containers/query_toggle/index.tsx | 55 +++ .../containers/query_toggle/translations.tsx | 17 + .../use_search_strategy/index.test.ts | 17 +- .../containers/use_search_strategy/index.tsx | 8 + .../alerts_count_panel/index.test.tsx | 42 ++ .../alerts_kpis/alerts_count_panel/index.tsx | 37 +- .../alerts_histogram_panel/index.test.tsx | 46 +++ .../alerts_histogram_panel/index.tsx | 53 ++- .../alerts_kpis/common/components.tsx | 14 +- .../alerts/use_query.test.tsx | 18 + .../detection_engine/alerts/use_query.tsx | 6 + .../__snapshots__/index.test.tsx.snap | 1 + .../authentications_table/index.test.tsx | 1 + .../authentications_table/index.tsx | 3 + .../host_risk_score_table/index.tsx | 3 + .../__snapshots__/index.test.tsx.snap | 1 + .../components/hosts_table/index.test.tsx | 4 + .../hosts/components/hosts_table/index.tsx | 3 + .../kpi_hosts/authentications/index.test.tsx | 66 ++++ .../kpi_hosts/authentications/index.tsx | 13 +- .../components/kpi_hosts/common/index.tsx | 21 +- .../components/kpi_hosts/hosts/index.test.tsx | 66 ++++ .../components/kpi_hosts/hosts/index.tsx | 13 +- .../kpi_hosts/risky_hosts/index.tsx | 9 +- .../kpi_hosts/unique_ips/index.test.tsx | 66 ++++ .../components/kpi_hosts/unique_ips/index.tsx | 13 +- .../index.test.tsx | 96 ++++- .../top_host_score_contributors/index.tsx | 62 ++- .../__snapshots__/index.test.tsx.snap | 1 + .../uncommon_process_table/index.test.tsx | 121 ++---- .../uncommon_process_table/index.tsx | 3 + .../containers/authentications/index.test.tsx | 30 ++ .../containers/authentications/index.tsx | 10 +- .../hosts/containers/hosts/index.test.tsx | 30 ++ .../public/hosts/containers/hosts/index.tsx | 8 + .../kpi_hosts/authentications/index.test.tsx | 28 ++ .../kpi_hosts/authentications/index.tsx | 10 +- .../containers/kpi_hosts/hosts/index.test.tsx | 28 ++ .../containers/kpi_hosts/hosts/index.tsx | 10 +- .../hosts/containers/kpi_hosts/index.tsx | 10 - .../kpi_hosts/unique_ips/index.test.tsx | 28 ++ .../containers/kpi_hosts/unique_ips/index.tsx | 10 +- .../uncommon_processes/index.test.tsx | 30 ++ .../containers/uncommon_processes/index.tsx | 10 +- .../authentications_query_tab_body.test.tsx | 68 ++++ .../authentications_query_tab_body.tsx | 11 +- .../host_risk_score_tab_body.test.tsx | 81 ++++ .../navigation/host_risk_score_tab_body.tsx | 13 +- .../navigation/hosts_query_tab_body.test.tsx | 68 ++++ .../pages/navigation/hosts_query_tab_body.tsx | 21 +- .../uncommon_process_query_tab_body.test.tsx | 68 ++++ .../uncommon_process_query_tab_body.tsx | 13 +- .../__snapshots__/embeddable.test.tsx.snap | 1 + .../components/embeddables/embeddable.tsx | 2 +- .../embeddables/embedded_map.test.tsx | 10 +- .../components/embeddables/embedded_map.tsx | 39 +- .../components/kpi_network/dns/index.test.tsx | 66 ++++ .../components/kpi_network/dns/index.tsx | 13 +- .../network/components/kpi_network/mock.ts | 2 + .../kpi_network/network_events/index.test.tsx | 66 ++++ .../kpi_network/network_events/index.tsx | 14 +- .../kpi_network/tls_handshakes/index.test.tsx | 66 ++++ .../kpi_network/tls_handshakes/index.tsx | 13 +- .../kpi_network/unique_flows/index.test.tsx | 66 ++++ .../kpi_network/unique_flows/index.tsx | 13 +- .../unique_private_ips/index.test.tsx | 66 ++++ .../kpi_network/unique_private_ips/index.tsx | 16 +- .../__snapshots__/index.test.tsx.snap | 1 + .../network_dns_table/index.test.tsx | 37 +- .../components/network_dns_table/index.tsx | 3 + .../__snapshots__/index.test.tsx.snap | 1 + .../network_http_table/index.test.tsx | 36 +- .../components/network_http_table/index.tsx | 3 + .../__snapshots__/index.test.tsx.snap | 2 + .../index.test.tsx | 72 +--- .../network_top_countries_table/index.tsx | 3 + .../__snapshots__/index.test.tsx.snap | 2 + .../network_top_n_flow_table/index.test.tsx | 52 +-- .../network_top_n_flow_table/index.tsx | 3 + .../components/tls_table/index.test.tsx | 37 +- .../network/components/tls_table/index.tsx | 3 + .../components/users_table/index.test.tsx | 40 +- .../network/components/users_table/index.tsx | 3 + .../containers/kpi_network/dns/index.test.tsx | 28 ++ .../containers/kpi_network/dns/index.tsx | 10 +- .../network/containers/kpi_network/index.tsx | 12 - .../kpi_network/network_events/index.test.tsx | 28 ++ .../kpi_network/network_events/index.tsx | 10 +- .../kpi_network/tls_handshakes/index.test.tsx | 28 ++ .../kpi_network/tls_handshakes/index.tsx | 10 +- .../kpi_network/unique_flows/index.test.tsx | 28 ++ .../kpi_network/unique_flows/index.tsx | 11 +- .../unique_private_ips/index.test.tsx | 28 ++ .../kpi_network/unique_private_ips/index.tsx | 10 +- .../containers/network_dns/index.test.tsx | 31 ++ .../network/containers/network_dns/index.tsx | 10 +- .../containers/network_http/index.test.tsx | 31 ++ .../network/containers/network_http/index.tsx | 14 +- .../network_top_countries/index.test.tsx | 33 ++ .../network_top_countries/index.tsx | 10 +- .../network_top_n_flow/index.test.tsx | 33 ++ .../containers/network_top_n_flow/index.tsx | 10 +- .../network/containers/tls/index.test.tsx | 34 ++ .../public/network/containers/tls/index.tsx | 10 +- .../network/containers/users/index.test.tsx | 34 ++ .../public/network/containers/users/index.tsx | 10 +- .../details/network_http_query_table.tsx | 13 +- .../network_top_countries_query_table.tsx | 13 +- .../network_top_n_flow_query_table.tsx | 13 +- .../network/pages/details/tls_query_table.tsx | 13 +- .../pages/details/users_query_table.tsx | 13 +- .../navigation/countries_query_tab_body.tsx | 13 +- .../pages/navigation/dns_query_tab_body.tsx | 13 +- .../pages/navigation/http_query_tab_body.tsx | 13 +- .../pages/navigation/ips_query_tab_body.tsx | 13 +- .../pages/navigation/tls_query_tab_body.tsx | 13 +- .../components/overview_host/index.test.tsx | 29 +- .../components/overview_host/index.tsx | 43 ++- .../overview_network/index.test.tsx | 29 +- .../components/overview_network/index.tsx | 43 ++- .../containers/overview_host/index.test.tsx | 28 ++ .../containers/overview_host/index.tsx | 7 + .../overview_network/index.test.tsx | 28 ++ .../containers/overview_network/index.tsx | 7 + .../risk_score/containers/all/index.tsx | 8 + .../kpi_users/total_users/index.test.tsx | 68 ++++ .../kpi_users/total_users/index.tsx | 14 +- .../user_risk_score_table/index.test.tsx | 5 +- .../user_risk_score_table/index.tsx | 3 + .../all_users_query_tab_body.test.tsx | 68 ++++ .../navigation/all_users_query_tab_body.tsx | 13 +- .../user_risk_score_tab_body.test.tsx | 81 ++++ .../navigation/user_risk_score_tab_body.tsx | 13 +- 154 files changed, 3965 insertions(+), 1144 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_user_table.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/containers/query_toggle/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/containers/query_toggle/index.tsx create mode 100644 x-pack/plugins/security_solution/public/common/containers/query_toggle/translations.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/containers/authentications/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/containers/hosts/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/index.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.test.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_score_tab_body.test.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.test.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/components/kpi_network/tls_handshakes/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/components/kpi_network/unique_flows/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/components/kpi_network/unique_private_ips/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/network/containers/kpi_network/index.tsx create mode 100644 x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/containers/network_dns/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/containers/network_http/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/containers/tls/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/containers/users/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/overview/containers/overview_host/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/overview/containers/overview_network/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/users/components/kpi_users/total_users/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/users/pages/navigation/all_users_query_tab_body.test.tsx create mode 100644 x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.test.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/header_section/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/header_section/__snapshots__/index.test.tsx.snap index 6701224289e668..45a6e20cf087d7 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_section/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/header_section/__snapshots__/index.test.tsx.snap @@ -18,19 +18,25 @@ exports[`HeaderSection it renders 1`] = ` responsive={false} > - -

- + - Test title - -

-
+

+ + Test title + +

+ +
+ diff --git a/x-pack/plugins/security_solution/public/common/components/header_section/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_section/index.test.tsx index 5ec97ea59bc1de..2296dc78241f2a 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_section/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_section/index.test.tsx @@ -180,4 +180,94 @@ describe('HeaderSection', () => { expect(wrapper.find('[data-test-subj="inspect-icon-button"]').first().exists()).toBe(false); }); + + test('it does not render query-toggle-header when no arguments provided', () => { + const wrapper = mount( + + +

{'Test children'}

+
+
+ ); + + expect(wrapper.find('[data-test-subj="query-toggle-header"]').first().exists()).toBe(false); + }); + + test('it does render query-toggle-header when toggleQuery arguments provided', () => { + const wrapper = mount( + + +

{'Test children'}

+
+
+ ); + + expect(wrapper.find('[data-test-subj="query-toggle-header"]').first().exists()).toBe(true); + }); + + test('it does render everything but title when toggleStatus = true', () => { + const wrapper = mount( + + +

{'Test children'}

+
+
+ ); + + expect(wrapper.find('[data-test-subj="query-toggle-header"]').first().prop('iconType')).toBe( + 'arrowDown' + ); + expect(wrapper.find('[data-test-subj="header-section-supplements"]').first().exists()).toBe( + true + ); + expect(wrapper.find('[data-test-subj="header-section-subtitle"]').first().exists()).toBe(true); + expect(wrapper.find('[data-test-subj="header-section-filters"]').first().exists()).toBe(true); + expect(wrapper.find('[data-test-subj="inspect-icon-button"]').first().exists()).toBe(true); + }); + test('it does not render anything but title when toggleStatus = false', () => { + const wrapper = mount( + + +

{'Test children'}

+
+
+ ); + + expect(wrapper.find('[data-test-subj="query-toggle-header"]').first().prop('iconType')).toBe( + 'arrowRight' + ); + expect(wrapper.find('[data-test-subj="header-section-supplements"]').first().exists()).toBe( + false + ); + expect(wrapper.find('[data-test-subj="header-section-filters"]').first().exists()).toBe(false); + expect(wrapper.find('[data-test-subj="header-section-subtitle"]').first().exists()).toBe(false); + expect(wrapper.find('[data-test-subj="inspect-icon-button"]').first().exists()).toBe(false); + }); + + test('it toggles query when icon is clicked', () => { + const mockToggle = jest.fn(); + const wrapper = mount( + + +

{'Test children'}

+
+
+ ); + wrapper.find('[data-test-subj="query-toggle-header"]').first().simulate('click'); + expect(mockToggle).toBeCalledWith(false); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx index ae07a03ba6407f..7997dfa83e27b2 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx @@ -5,13 +5,21 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiTitle, EuiTitleSize } from '@elastic/eui'; -import React from 'react'; +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiIconTip, + EuiTitle, + EuiTitleSize, +} from '@elastic/eui'; +import React, { useCallback } from 'react'; import styled, { css } from 'styled-components'; import { InspectButton } from '../inspect'; import { Subtitle } from '../subtitle'; +import * as i18n from '../../containers/query_toggle/translations'; interface HeaderProps { border?: boolean; @@ -51,6 +59,8 @@ export interface HeaderSectionProps extends HeaderProps { split?: boolean; stackHeader?: boolean; subtitle?: string | React.ReactNode; + toggleQuery?: (status: boolean) => void; + toggleStatus?: boolean; title: string | React.ReactNode; titleSize?: EuiTitleSize; tooltip?: string; @@ -72,56 +82,87 @@ const HeaderSectionComponent: React.FC = ({ subtitle, title, titleSize = 'm', + toggleQuery, + toggleStatus = true, tooltip, -}) => ( -
- - - - - -

- {title} - {tooltip && ( - <> - {' '} - - +}) => { + const toggle = useCallback(() => { + if (toggleQuery) { + toggleQuery(!toggleStatus); + } + }, [toggleQuery, toggleStatus]); + return ( +
+ + + + + + {toggleQuery && ( + + + )} -

-
+ + +

+ {title} + {tooltip && ( + <> + {' '} + + + )} +

+
+
+
- {!hideSubtitle && ( - - )} -
- - {id && showInspectButton && ( - - + {!hideSubtitle && toggleStatus && ( + + )} - )} - {headerFilters && {headerFilters}} -
- + {id && showInspectButton && toggleStatus && ( + + + + )} - {children && ( - - {children} + {headerFilters && toggleStatus && ( + + {headerFilters} + + )} + - )} - -
-); + + {children && toggleStatus && ( + + {children} + + )} + +
+ ); +}; export const HeaderSection = React.memo(HeaderSectionComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx index aee49bd1b00ae9..1de9e08b4c65c5 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx @@ -15,6 +15,9 @@ import { TestProviders } from '../../mock'; import { mockRuntimeMappings } from '../../containers/source/mock'; import { dnsTopDomainsLensAttributes } from '../visualization_actions/lens_attributes/network/dns_top_domains'; import { useRouteSpy } from '../../utils/route/use_route_spy'; +import { useQueryToggle } from '../../containers/query_toggle'; + +jest.mock('../../containers/query_toggle'); jest.mock('../../lib/kibana'); jest.mock('./matrix_loader', () => ({ @@ -25,9 +28,7 @@ jest.mock('../charts/barchart', () => ({ BarChart: () =>
, })); -jest.mock('../../containers/matrix_histogram', () => ({ - useMatrixHistogramCombined: jest.fn(), -})); +jest.mock('../../containers/matrix_histogram'); jest.mock('../visualization_actions', () => ({ VisualizationActions: jest.fn(({ className }: { className: string }) => ( @@ -78,9 +79,13 @@ describe('Matrix Histogram Component', () => { title: 'mockTitle', runtimeMappings: mockRuntimeMappings, }; - - beforeAll(() => { - (useMatrixHistogramCombined as jest.Mock).mockReturnValue([ + const mockUseMatrix = useMatrixHistogramCombined as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const mockSetToggle = jest.fn(); + beforeEach(() => { + jest.clearAllMocks(); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); + mockUseMatrix.mockReturnValue([ false, { data: null, @@ -88,14 +93,16 @@ describe('Matrix Histogram Component', () => { totalCount: null, }, ]); - wrapper = mount(, { - wrappingComponent: TestProviders, - }); }); describe('on initial load', () => { + beforeEach(() => { + wrapper = mount(, { + wrappingComponent: TestProviders, + }); + }); test('it requests Matrix Histogram', () => { - expect(useMatrixHistogramCombined).toHaveBeenCalledWith({ + expect(mockUseMatrix).toHaveBeenCalledWith({ endDate: mockMatrixOverTimeHistogramProps.endDate, errorMessage: mockMatrixOverTimeHistogramProps.errorMessage, histogramType: mockMatrixOverTimeHistogramProps.histogramType, @@ -114,6 +121,9 @@ describe('Matrix Histogram Component', () => { describe('spacer', () => { test('it renders a spacer by default', () => { + wrapper = mount(, { + wrappingComponent: TestProviders, + }); expect(wrapper.find('[data-test-subj="spacer"]').exists()).toBe(true); }); @@ -129,8 +139,11 @@ describe('Matrix Histogram Component', () => { }); describe('not initial load', () => { - beforeAll(() => { - (useMatrixHistogramCombined as jest.Mock).mockReturnValue([ + beforeEach(() => { + wrapper = mount(, { + wrappingComponent: TestProviders, + }); + mockUseMatrix.mockReturnValue([ false, { data: [ @@ -159,6 +172,9 @@ describe('Matrix Histogram Component', () => { describe('select dropdown', () => { test('should be hidden if only one option is provided', () => { + wrapper = mount(, { + wrappingComponent: TestProviders, + }); expect(wrapper.find('EuiSelect').exists()).toBe(false); }); }); @@ -287,4 +303,53 @@ describe('Matrix Histogram Component', () => { expect(wrapper.find('[data-test-subj="mock-viz-actions"]').exists()).toBe(false); }); }); + + describe('toggle query', () => { + const testProps = { + ...mockMatrixOverTimeHistogramProps, + lensAttributes: dnsTopDomainsLensAttributes, + }; + + test('toggleQuery updates toggleStatus', () => { + wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(mockUseMatrix.mock.calls[0][0].skip).toEqual(false); + wrapper.find('[data-test-subj="query-toggle-header"]').first().simulate('click'); + expect(mockSetToggle).toBeCalledWith(false); + expect(mockUseMatrix.mock.calls[1][0].skip).toEqual(true); + }); + + test('toggleStatus=true, do not skip', () => { + wrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(mockUseMatrix.mock.calls[0][0].skip).toEqual(false); + }); + + test('toggleStatus=true, render components', () => { + wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(wrapper.find('MatrixLoader').exists()).toBe(true); + }); + + test('toggleStatus=false, do not render components', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(wrapper.find('MatrixLoader').exists()).toBe(false); + }); + + test('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + wrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(mockUseMatrix.mock.calls[0][0].skip).toEqual(true); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx index dbf525f8e14cb1..488948de074f64 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx @@ -34,6 +34,7 @@ import { GetLensAttributes, LensAttributes } from '../visualization_actions/type import { useKibana, useGetUserCasesPermissions } from '../../lib/kibana'; import { APP_ID, SecurityPageName } from '../../../../common/constants'; import { useRouteSpy } from '../../utils/route/use_route_spy'; +import { useQueryToggle } from '../../containers/query_toggle'; export type MatrixHistogramComponentProps = MatrixHistogramProps & Omit & { @@ -148,6 +149,19 @@ export const MatrixHistogramComponent: React.FC = }, [defaultStackByOption, stackByOptions] ); + const { toggleStatus, setToggleStatus } = useQueryToggle(id); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); + const toggleQuery = useCallback( + (status: boolean) => { + setToggleStatus(status); + // toggle on = skipQuery false + setQuerySkip(!status); + }, + [setQuerySkip, setToggleStatus] + ); const matrixHistogramRequest = { endDate, @@ -161,9 +175,8 @@ export const MatrixHistogramComponent: React.FC = runtimeMappings, isPtrIncluded, docValueFields, - skip, + skip: querySkip, }; - const [loading, { data, inspect, totalCount, refetch }] = useMatrixHistogramCombined(matrixHistogramRequest); const [{ pageName }] = useRouteSpy(); @@ -225,7 +238,7 @@ export const MatrixHistogramComponent: React.FC = > {loading && !isInitialLoading && ( @@ -239,8 +252,11 @@ export const MatrixHistogramComponent: React.FC = = {headerChildren} - - {isInitialLoading ? ( - - ) : ( - - )} + {toggleStatus ? ( + isInitialLoading ? ( + + ) : ( + + ) + ) : null} {showSpacer && } diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/matrix_loader.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/matrix_loader.tsx index efa4ba4c6eb0f7..8eca508a4b74bd 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/matrix_loader.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/matrix_loader.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import styled from 'styled-components'; const StyledEuiFlexGroup = styled(EuiFlexGroup)` - flex 1; + flex: 1; `; const MatrixLoaderComponent = () => ( diff --git a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts index f1cab9c2f441dd..58610298d43950 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts @@ -80,7 +80,9 @@ export const useAnomaliesTableData = ({ earliestMs: number, latestMs: number ) { - if (isMlUser && !skip && jobIds.length > 0) { + if (skip) { + setLoading(false); + } else if (isMlUser && !skip && jobIds.length > 0) { try { const data = await anomaliesTableData( { diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.test.tsx new file mode 100644 index 00000000000000..7701880bd7b2ed --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.test.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mount } from 'enzyme'; +import { AnomaliesHostTable } from './anomalies_host_table'; +import { TestProviders } from '../../../mock'; +import React from 'react'; +import { useQueryToggle } from '../../../containers/query_toggle'; +import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data'; +import { HostsType } from '../../../../hosts/store/model'; +import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; + +jest.mock('../../../containers/query_toggle'); +jest.mock('../anomaly/use_anomalies_table_data'); +jest.mock('../../../../../common/machine_learning/has_ml_user_permissions'); + +describe('Anomalies host table', () => { + describe('toggle query', () => { + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const mockUseAnomaliesTableData = useAnomaliesTableData as jest.Mock; + const mockSetToggle = jest.fn(); + const testProps = { + startDate: '2019-07-17T20:00:00.000Z', + endDate: '2019-07-18T20:00:00.000Z', + narrowDateRange: jest.fn(), + skip: false, + type: HostsType.page, + }; + beforeEach(() => { + jest.clearAllMocks(); + (hasMlUserPermissions as jest.Mock).mockReturnValue(true); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); + mockUseAnomaliesTableData.mockReturnValue([ + false, + { + anomalies: [], + interval: '10', + }, + ]); + }); + + test('toggleQuery updates toggleStatus', () => { + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(false); + wrapper.find('[data-test-subj="query-toggle-header"]').first().simulate('click'); + expect(mockSetToggle).toBeCalledWith(false); + expect(mockUseAnomaliesTableData.mock.calls[1][0].skip).toEqual(true); + }); + + test('toggleStatus=true, do not skip', () => { + mount(, { + wrappingComponent: TestProviders, + }); + + expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(false); + }); + + test('toggleStatus=true, render components', () => { + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(wrapper.find('[data-test-subj="host-anomalies-table"]').exists()).toBe(true); + }); + + test('toggleStatus=false, do not render components', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(wrapper.find('[data-test-subj="host-anomalies-table"]').exists()).toBe(false); + }); + + test('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + mount(, { + wrappingComponent: TestProviders, + }); + + expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx index 318f452e0c1dfa..eec90e6117c283 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data'; import { HeaderSection } from '../../header_section'; @@ -21,6 +21,7 @@ import { BasicTable } from './basic_table'; import { getCriteriaFromHostType } from '../criteria/get_criteria_from_host_type'; import { Panel } from '../../panel'; import { anomaliesTableDefaultEquality } from './default_equality'; +import { useQueryToggle } from '../../../containers/query_toggle'; const sorting = { sort: { @@ -37,10 +38,24 @@ const AnomaliesHostTableComponent: React.FC = ({ type, }) => { const capabilities = useMlCapabilities(); + const { toggleStatus, setToggleStatus } = useQueryToggle(`AnomaliesHostTable`); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); + const toggleQuery = useCallback( + (status: boolean) => { + setToggleStatus(status); + // toggle on = skipQuery false + setQuerySkip(!status); + }, + [setQuerySkip, setToggleStatus] + ); + const [loading, tableData] = useAnomaliesTableData({ startDate, endDate, - skip, + skip: querySkip, criteriaFields: getCriteriaFromHostType(type, hostName), filterQuery: { exists: { field: 'host.name' }, @@ -64,21 +79,26 @@ const AnomaliesHostTableComponent: React.FC = ({ return ( - - type is not as specific as EUI's... - columns={columns} - items={hosts} - pagination={pagination} - sorting={sorting} - /> + {toggleStatus && ( + type is not as specific as EUI's... + columns={columns} + items={hosts} + pagination={pagination} + sorting={sorting} + /> + )} {loading && ( diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.test.tsx new file mode 100644 index 00000000000000..b7491562a5d72a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.test.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 { mount } from 'enzyme'; +import { AnomaliesNetworkTable } from './anomalies_network_table'; +import { TestProviders } from '../../../mock'; +import React from 'react'; +import { useQueryToggle } from '../../../containers/query_toggle'; +import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data'; +import { NetworkType } from '../../../../network/store/model'; +import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; +import { FlowTarget } from '../../../../../common/search_strategy'; + +jest.mock('../../../containers/query_toggle'); +jest.mock('../anomaly/use_anomalies_table_data'); +jest.mock('../../../../../common/machine_learning/has_ml_user_permissions'); + +describe('Anomalies network table', () => { + describe('toggle query', () => { + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const mockUseAnomaliesTableData = useAnomaliesTableData as jest.Mock; + const mockSetToggle = jest.fn(); + const testProps = { + startDate: '2019-07-17T20:00:00.000Z', + endDate: '2019-07-18T20:00:00.000Z', + flowTarget: FlowTarget.destination, + narrowDateRange: jest.fn(), + skip: false, + type: NetworkType.page, + }; + beforeEach(() => { + jest.clearAllMocks(); + (hasMlUserPermissions as jest.Mock).mockReturnValue(true); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); + mockUseAnomaliesTableData.mockReturnValue([ + false, + { + anomalies: [], + interval: '10', + }, + ]); + }); + + test('toggleQuery updates toggleStatus', () => { + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(false); + wrapper.find('[data-test-subj="query-toggle-header"]').first().simulate('click'); + expect(mockSetToggle).toBeCalledWith(false); + expect(mockUseAnomaliesTableData.mock.calls[1][0].skip).toEqual(true); + }); + + test('toggleStatus=true, do not skip', () => { + mount(, { + wrappingComponent: TestProviders, + }); + + expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(false); + }); + + test('toggleStatus=true, render components', () => { + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(wrapper.find('[data-test-subj="network-anomalies-table"]').exists()).toBe(true); + }); + + test('toggleStatus=false, do not render components', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(wrapper.find('[data-test-subj="network-anomalies-table"]').exists()).toBe(false); + }); + + test('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + mount(, { + wrappingComponent: TestProviders, + }); + + expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx index 78795c6d3614a9..242114a806ca86 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data'; import { HeaderSection } from '../../header_section'; @@ -20,6 +20,7 @@ import { BasicTable } from './basic_table'; import { networkEquality } from './network_equality'; import { getCriteriaFromNetworkType } from '../criteria/get_criteria_from_network_type'; import { Panel } from '../../panel'; +import { useQueryToggle } from '../../../containers/query_toggle'; const sorting = { sort: { @@ -37,10 +38,25 @@ const AnomaliesNetworkTableComponent: React.FC = ({ flowTarget, }) => { const capabilities = useMlCapabilities(); + + const { toggleStatus, setToggleStatus } = useQueryToggle(`AnomaliesNetwork-${flowTarget}`); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); + const toggleQuery = useCallback( + (status: boolean) => { + setToggleStatus(status); + // toggle on = skipQuery false + setQuerySkip(!status); + }, + [setQuerySkip, setToggleStatus] + ); + const [loading, tableData] = useAnomaliesTableData({ startDate, endDate, - skip, + skip: querySkip, criteriaFields: getCriteriaFromNetworkType(type, ip, flowTarget), }); @@ -63,18 +79,23 @@ const AnomaliesNetworkTableComponent: React.FC = ({ subtitle={`${i18n.SHOWING}: ${pagination.totalItemCount.toLocaleString()} ${i18n.UNIT( pagination.totalItemCount )}`} + height={!toggleStatus ? 40 : undefined} title={i18n.ANOMALIES} tooltip={i18n.TOOLTIP} + toggleQuery={toggleQuery} + toggleStatus={toggleStatus} isInspectDisabled={skip} /> - - type is not as specific as EUI's... - columns={columns} - items={networks} - pagination={pagination} - sorting={sorting} - /> + {toggleStatus && ( + type is not as specific as EUI's... + columns={columns} + items={networks} + pagination={pagination} + sorting={sorting} + /> + )} {loading && ( diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_user_table.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_user_table.test.tsx new file mode 100644 index 00000000000000..40aab638b854ac --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_user_table.test.tsx @@ -0,0 +1,89 @@ +/* + * 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 { mount } from 'enzyme'; +import { AnomaliesUserTable } from './anomalies_user_table'; +import { TestProviders } from '../../../mock'; +import React from 'react'; +import { useQueryToggle } from '../../../containers/query_toggle'; +import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data'; +import { UsersType } from '../../../../users/store/model'; +import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; + +jest.mock('../../../containers/query_toggle'); +jest.mock('../anomaly/use_anomalies_table_data'); +jest.mock('../../../../../common/machine_learning/has_ml_user_permissions'); + +describe('Anomalies user table', () => { + describe('toggle query', () => { + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const mockUseAnomaliesTableData = useAnomaliesTableData as jest.Mock; + const mockSetToggle = jest.fn(); + const testProps = { + startDate: '2019-07-17T20:00:00.000Z', + endDate: '2019-07-18T20:00:00.000Z', + narrowDateRange: jest.fn(), + userName: 'coolguy', + skip: false, + type: UsersType.page, + }; + beforeEach(() => { + jest.clearAllMocks(); + (hasMlUserPermissions as jest.Mock).mockReturnValue(true); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); + mockUseAnomaliesTableData.mockReturnValue([ + false, + { + anomalies: [], + interval: '10', + }, + ]); + }); + + test('toggleQuery updates toggleStatus', () => { + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(false); + wrapper.find('[data-test-subj="query-toggle-header"]').first().simulate('click'); + expect(mockSetToggle).toBeCalledWith(false); + expect(mockUseAnomaliesTableData.mock.calls[1][0].skip).toEqual(true); + }); + + test('toggleStatus=true, do not skip', () => { + mount(, { + wrappingComponent: TestProviders, + }); + + expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(false); + }); + + test('toggleStatus=true, render components', () => { + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(wrapper.find('[data-test-subj="user-anomalies-table"]').exists()).toBe(true); + }); + + test('toggleStatus=false, do not render components', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(wrapper.find('[data-test-subj="user-anomalies-table"]').exists()).toBe(false); + }); + + test('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + mount(, { + wrappingComponent: TestProviders, + }); + + expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_user_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_user_table.tsx index 061f2c04cef6d5..c67455c0772b96 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_user_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_user_table.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data'; import { HeaderSection } from '../../header_section'; @@ -23,6 +23,7 @@ import { Panel } from '../../panel'; import { anomaliesTableDefaultEquality } from './default_equality'; import { convertAnomaliesToUsers } from './convert_anomalies_to_users'; import { getAnomaliesUserTableColumnsCurated } from './get_anomalies_user_table_columns'; +import { useQueryToggle } from '../../../containers/query_toggle'; const sorting = { sort: { @@ -40,10 +41,24 @@ const AnomaliesUserTableComponent: React.FC = ({ }) => { const capabilities = useMlCapabilities(); + const { toggleStatus, setToggleStatus } = useQueryToggle(`AnomaliesUserTable`); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); + const toggleQuery = useCallback( + (status: boolean) => { + setToggleStatus(status); + // toggle on = skipQuery false + setQuerySkip(!status); + }, + [setQuerySkip, setToggleStatus] + ); + const [loading, tableData] = useAnomaliesTableData({ startDate, endDate, - skip, + skip: querySkip, criteriaFields: getCriteriaFromUsersType(type, userName), filterQuery: { exists: { field: 'user.name' }, @@ -67,21 +82,27 @@ const AnomaliesUserTableComponent: React.FC = ({ return ( - type is not as specific as EUI's... - columns={columns} - items={users} - pagination={pagination} - sorting={sorting} - /> + {toggleStatus && ( + type is not as specific as EUI's... + columns={columns} + items={users} + pagination={pagination} + sorting={sorting} + /> + )} {loading && ( diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap index a2fffc32be46d5..bf03d637e8811e 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap @@ -11,6 +11,8 @@ exports[`Paginated Table Component rendering it renders the default load more ta

@@ -58,6 +60,7 @@ exports[`Paginated Table Component rendering it renders the default load more ta }, ] } + data-test-subj="paginated-basic-table" items={ Array [ Object { diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx index 0c09dce9c07cb3..57686126dfb10f 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx @@ -15,6 +15,8 @@ import { getHostsColumns, mockData, rowItems, sortedHosts } from './index.mock'; import { ThemeProvider } from 'styled-components'; import { getMockTheme } from '../../lib/kibana/kibana_react.mock'; import { Direction } from '../../../../common/search_strategy'; +import { useQueryToggle } from '../../containers/query_toggle'; +jest.mock('../../containers/query_toggle'); jest.mock('react', () => { const r = jest.requireActual('react'); @@ -36,37 +38,41 @@ const mockTheme = getMockTheme({ }); describe('Paginated Table Component', () => { - let loadPage: jest.Mock; - let updateLimitPagination: jest.Mock; - let updateActivePage: jest.Mock; + const loadPage = jest.fn(); + const updateLimitPagination = jest.fn(); + const updateActivePage = jest.fn(); + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const mockSetToggle = jest.fn(); + const mockSetQuerySkip = jest.fn(); + beforeEach(() => { - loadPage = jest.fn(); - updateLimitPagination = jest.fn(); - updateActivePage = jest.fn(); + jest.clearAllMocks(); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); }); + const testProps = { + activePage: 0, + columns: getHostsColumns(), + headerCount: 1, + headerSupplement:

{'My test supplement.'}

, + headerTitle: 'Hosts', + headerTooltip: 'My test tooltip', + headerUnit: 'Test Unit', + itemsPerRow: rowItems, + limit: 1, + loading: false, + loadPage, + pageOfItems: mockData.Hosts.edges, + setQuerySkip: jest.fn(), + showMorePagesIndicator: true, + totalCount: 10, + updateActivePage, + updateLimitPagination: (limit: number) => updateLimitPagination({ limit }), + }; + describe('rendering', () => { test('it renders the default load more table', () => { - const wrapper = shallow( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> - ); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); @@ -74,24 +80,7 @@ describe('Paginated Table Component', () => { test('it renders the loading panel at the beginning ', () => { const wrapper = mount( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={true} - loadPage={loadPage} - pageOfItems={[]} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> +
); @@ -103,24 +92,7 @@ describe('Paginated Table Component', () => { test('it renders the over loading panel after data has been in the table ', () => { const wrapper = mount( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={true} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> +
); @@ -130,24 +102,7 @@ describe('Paginated Table Component', () => { test('it renders the correct amount of pages and starts at activePage: 0', () => { const wrapper = mount( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> +
); @@ -167,24 +122,7 @@ describe('Paginated Table Component', () => { test('it render popover to select new limit in table', () => { const wrapper = mount( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> +
); @@ -195,24 +133,7 @@ describe('Paginated Table Component', () => { test('it will NOT render popover to select new limit in table if props itemsPerRow is empty', () => { const wrapper = mount( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={[]} - limit={2} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> +
); @@ -224,24 +145,11 @@ describe('Paginated Table Component', () => { const wrapper = mount( {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} limit={2} - loading={false} - loadPage={jest.fn()} onChange={mockOnChange} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} sorting={{ direction: Direction.asc, field: 'node.host.name' }} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} />
); @@ -253,22 +161,9 @@ describe('Paginated Table Component', () => { const wrapper = mount( {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} + {...testProps} limit={DEFAULT_MAX_TABLE_QUERY_SIZE} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} totalCount={DEFAULT_MAX_TABLE_QUERY_SIZE * 3} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} />
); @@ -279,24 +174,7 @@ describe('Paginated Table Component', () => { test('Should show items per row if totalCount is greater than items', () => { const wrapper = mount( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={DEFAULT_MAX_TABLE_QUERY_SIZE} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={30} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> +
); expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeTruthy(); @@ -305,24 +183,7 @@ describe('Paginated Table Component', () => { test('Should hide items per row if totalCount is less than items', () => { const wrapper = mount( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={DEFAULT_MAX_TABLE_QUERY_SIZE} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={1} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> +
); expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeFalsy(); @@ -331,24 +192,7 @@ describe('Paginated Table Component', () => { test('Should hide pagination if totalCount is zero', () => { const wrapper = mount( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={DEFAULT_MAX_TABLE_QUERY_SIZE} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={0} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> +
); @@ -360,24 +204,7 @@ describe('Paginated Table Component', () => { test('should call updateActivePage with 1 when clicking to the first page', () => { const wrapper = mount( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> +
); wrapper.find('[data-test-subj="pagination-button-next"]').first().simulate('click'); @@ -387,24 +214,7 @@ describe('Paginated Table Component', () => { test('Should call updateActivePage with 0 when you pick a new limit', () => { const wrapper = mount( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> +
); wrapper.find('[data-test-subj="pagination-button-next"]').first().simulate('click'); @@ -417,22 +227,8 @@ describe('Paginated Table Component', () => { test('should update the page when the activePage is changed from redux', () => { const ourProps: BasicTableProps = { + ...testProps, activePage: 3, - columns: getHostsColumns(), - headerCount: 1, - headerSupplement:

{'My test supplement.'}

, - headerTitle: 'Hosts', - headerTooltip: 'My test tooltip', - headerUnit: 'Test Unit', - itemsPerRow: rowItems, - limit: 1, - loading: false, - loadPage, - pageOfItems: mockData.Hosts.edges, - showMorePagesIndicator: true, - totalCount: 10, - updateActivePage, - updateLimitPagination: (limit) => updateLimitPagination({ limit }), }; // enzyme does not allow us to pass props to child of HOC @@ -462,24 +258,7 @@ describe('Paginated Table Component', () => { test('Should call updateLimitPagination when you pick a new limit', () => { const wrapper = mount( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> +
); @@ -494,24 +273,11 @@ describe('Paginated Table Component', () => { const wrapper = mount( {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} limit={2} - loading={false} - loadPage={jest.fn()} onChange={mockOnChange} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} sorting={{ direction: Direction.asc, field: 'node.host.name' }} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} />
); @@ -524,4 +290,41 @@ describe('Paginated Table Component', () => { ]); }); }); + + describe('Toggle query', () => { + test('toggleQuery updates toggleStatus', () => { + const wrapper = mount( + + + + ); + wrapper.find('[data-test-subj="query-toggle-header"]').first().simulate('click'); + expect(mockSetToggle).toBeCalledWith(false); + expect(mockSetQuerySkip).toBeCalledWith(true); + }); + + test('toggleStatus=true, render table', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="paginated-basic-table"]').first().exists()).toEqual( + true + ); + }); + + test('toggleStatus=false, hide table', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="paginated-basic-table"]').first().exists()).toEqual( + false + ); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx index 310ab039057c2f..b9de144c5735e3 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx @@ -20,7 +20,7 @@ import { EuiTableRowCellProps, } from '@elastic/eui'; import { noop } from 'lodash/fp'; -import React, { FC, memo, useState, useMemo, useEffect, ComponentType } from 'react'; +import React, { FC, memo, useState, useMemo, useEffect, ComponentType, useCallback } from 'react'; import styled from 'styled-components'; import { Direction } from '../../../../common/search_strategy'; @@ -49,6 +49,7 @@ import { useStateToaster } from '../toasters'; import * as i18n from './translations'; import { Panel } from '../panel'; import { InspectButtonContainer } from '../inspect'; +import { useQueryToggle } from '../../containers/query_toggle'; const DEFAULT_DATA_TEST_SUBJ = 'paginated-table'; @@ -113,6 +114,7 @@ export interface BasicTableProps { onChange?: (criteria: Criteria) => void; // eslint-disable-next-line @typescript-eslint/no-explicit-any pageOfItems: any[]; + setQuerySkip: (skip: boolean) => void; showMorePagesIndicator: boolean; sorting?: SortingBasicTable; split?: boolean; @@ -153,6 +155,7 @@ const PaginatedTableComponent: FC = ({ loadPage, onChange = noop, pageOfItems, + setQuerySkip, showMorePagesIndicator, sorting = null, split, @@ -253,10 +256,24 @@ const PaginatedTableComponent: FC = ({ [sorting] ); + const { toggleStatus, setToggleStatus } = useQueryToggle(id); + + const toggleQuery = useCallback( + (status: boolean) => { + setToggleStatus(status); + // toggle on = skipQuery false + setQuerySkip(!status); + }, + [setQuerySkip, setToggleStatus] + ); + return ( = ({ > {!loadingInitial && headerSupplement} - - {loadingInitial ? ( - - ) : ( - <> - - - - {itemsPerRow && itemsPerRow.length > 0 && totalCount >= itemsPerRow[0].numberOfRow && ( - - - - )} - - - - {totalCount > 0 && ( - - )} - - - {(isInspect || myLoading) && ( - - )} - - )} + {toggleStatus && + (loadingInitial ? ( + + ) : ( + <> + + + + {itemsPerRow && + itemsPerRow.length > 0 && + totalCount >= itemsPerRow[0].numberOfRow && ( + + + + )} + + + + {totalCount > 0 && ( + + )} + + + {(isInspect || myLoading) && ( + + )} + + ))} ); diff --git a/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx index 5f2c76632aba9c..944eeb8b42a57b 100644 --- a/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx @@ -41,6 +41,7 @@ import { NetworkKpiStrategyResponse, } from '../../../../common/search_strategy'; import { getMockTheme } from '../../lib/kibana/kibana_react.mock'; +import * as module from '../../containers/query_toggle'; const from = '2019-06-15T06:00:00.000Z'; const to = '2019-06-18T06:00:00.000Z'; @@ -53,26 +54,37 @@ jest.mock('../charts/barchart', () => { return { BarChart: () =>
}; }); +const mockSetToggle = jest.fn(); + +jest + .spyOn(module, 'useQueryToggle') + .mockImplementation(() => ({ toggleStatus: true, setToggleStatus: mockSetToggle })); +const mockSetQuerySkip = jest.fn(); describe('Stat Items Component', () => { const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } }); const state: State = mockGlobalState; const { storage } = createSecuritySolutionStorageMock(); const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - + const testProps = { + description: 'HOSTS', + fields: [{ key: 'hosts', value: null, color: '#6092C0', icon: 'cross' }], + from, + id: 'statItems', + key: 'mock-keys', + loading: false, + setQuerySkip: mockSetQuerySkip, + to, + narrowDateRange: mockNarrowDateRange, + }; + beforeEach(() => { + jest.clearAllMocks(); + }); describe.each([ [ mount( - + ), @@ -81,17 +93,7 @@ describe('Stat Items Component', () => { mount( - + ), @@ -118,62 +120,59 @@ describe('Stat Items Component', () => { }); }); + const mockStatItemsData: StatItemsProps = { + ...testProps, + areaChart: [ + { + key: 'uniqueSourceIpsHistogram', + value: [ + { x: new Date('2019-05-03T13:00:00.000Z').toISOString(), y: 565975 }, + { x: new Date('2019-05-04T01:00:00.000Z').toISOString(), y: 1084366 }, + { x: new Date('2019-05-04T13:00:00.000Z').toISOString(), y: 12280 }, + ], + color: '#D36086', + }, + { + key: 'uniqueDestinationIpsHistogram', + value: [ + { x: new Date('2019-05-03T13:00:00.000Z').toISOString(), y: 565975 }, + { x: new Date('2019-05-04T01:00:00.000Z').toISOString(), y: 1084366 }, + { x: new Date('2019-05-04T13:00:00.000Z').toISOString(), y: 12280 }, + ], + color: '#9170B8', + }, + ], + barChart: [ + { key: 'uniqueSourceIps', value: [{ x: 'uniqueSourceIps', y: '1714' }], color: '#D36086' }, + { + key: 'uniqueDestinationIps', + value: [{ x: 'uniqueDestinationIps', y: 2354 }], + color: '#9170B8', + }, + ], + description: 'UNIQUE_PRIVATE_IPS', + enableAreaChart: true, + enableBarChart: true, + fields: [ + { + key: 'uniqueSourceIps', + description: 'Source', + value: 1714, + color: '#D36086', + icon: 'cross', + }, + { + key: 'uniqueDestinationIps', + description: 'Dest.', + value: 2359, + color: '#9170B8', + icon: 'cross', + }, + ], + }; + + let wrapper: ReactWrapper; describe('rendering kpis with charts', () => { - const mockStatItemsData: StatItemsProps = { - areaChart: [ - { - key: 'uniqueSourceIpsHistogram', - value: [ - { x: new Date('2019-05-03T13:00:00.000Z').toISOString(), y: 565975 }, - { x: new Date('2019-05-04T01:00:00.000Z').toISOString(), y: 1084366 }, - { x: new Date('2019-05-04T13:00:00.000Z').toISOString(), y: 12280 }, - ], - color: '#D36086', - }, - { - key: 'uniqueDestinationIpsHistogram', - value: [ - { x: new Date('2019-05-03T13:00:00.000Z').toISOString(), y: 565975 }, - { x: new Date('2019-05-04T01:00:00.000Z').toISOString(), y: 1084366 }, - { x: new Date('2019-05-04T13:00:00.000Z').toISOString(), y: 12280 }, - ], - color: '#9170B8', - }, - ], - barChart: [ - { key: 'uniqueSourceIps', value: [{ x: 'uniqueSourceIps', y: '1714' }], color: '#D36086' }, - { - key: 'uniqueDestinationIps', - value: [{ x: 'uniqueDestinationIps', y: 2354 }], - color: '#9170B8', - }, - ], - description: 'UNIQUE_PRIVATE_IPS', - enableAreaChart: true, - enableBarChart: true, - fields: [ - { - key: 'uniqueSourceIps', - description: 'Source', - value: 1714, - color: '#D36086', - icon: 'cross', - }, - { - key: 'uniqueDestinationIps', - description: 'Dest.', - value: 2359, - color: '#9170B8', - icon: 'cross', - }, - ], - from, - id: 'statItems', - key: 'mock-keys', - to, - narrowDateRange: mockNarrowDateRange, - }; - let wrapper: ReactWrapper; beforeAll(() => { wrapper = mount( @@ -202,6 +201,43 @@ describe('Stat Items Component', () => { expect(wrapper.find(EuiHorizontalRule)).toHaveLength(1); }); }); + describe('Toggle query', () => { + test('toggleQuery updates toggleStatus', () => { + wrapper = mount( + + + + ); + wrapper.find('[data-test-subj="query-toggle-stat"]').first().simulate('click'); + expect(mockSetToggle).toBeCalledWith(false); + expect(mockSetQuerySkip).toBeCalledWith(true); + }); + test('toggleStatus=true, render all', () => { + wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="inspect-icon-button"]').first().exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="stat-title"]').first().exists()).toEqual(true); + }); + test('toggleStatus=false, render none', () => { + jest + .spyOn(module, 'useQueryToggle') + .mockImplementation(() => ({ toggleStatus: false, setToggleStatus: mockSetToggle })); + wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="inspect-icon-button"]').first().exists()).toEqual( + false + ); + expect(wrapper.find('[data-test-subj="stat-title"]').first().exists()).toEqual(false); + }); + }); }); describe('addValueToFields', () => { @@ -244,7 +280,9 @@ describe('useKpiMatrixStatus', () => { 'statItem', from, to, - mockNarrowDateRange + mockNarrowDateRange, + mockSetQuerySkip, + false ); return ( @@ -262,8 +300,10 @@ describe('useKpiMatrixStatus', () => { ); - - expect(wrapper.find('MockChildComponent').get(0).props).toEqual(mockEnableChartsData); + const result = { ...wrapper.find('MockChildComponent').get(0).props }; + const { setQuerySkip, ...restResult } = result; + const { setQuerySkip: a, ...restExpect } = mockEnableChartsData; + expect(restResult).toEqual(restExpect); }); test('it should not append areaChart if enableAreaChart is off', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx b/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx index 424920d34e2e87..6de3cc07472bca 100644 --- a/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx @@ -12,13 +12,16 @@ import { EuiPanel, EuiHorizontalRule, EuiIcon, + EuiButtonIcon, + EuiLoadingSpinner, EuiTitle, IconType, } from '@elastic/eui'; import { get, getOr } from 'lodash/fp'; -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; +import { useQueryToggle } from '../../containers/query_toggle'; import { HostsKpiStrategyResponse, @@ -34,6 +37,7 @@ import { InspectButton } from '../inspect'; import { VisualizationActions, HISTOGRAM_ACTIONS_BUTTON_CLASS } from '../visualization_actions'; import { HoverVisibilityContainer } from '../hover_visibility_container'; import { LensAttributes } from '../visualization_actions/types'; +import * as i18n from '../../containers/query_toggle/translations'; import { UserskKpiStrategyResponse } from '../../../../common/search_strategy/security_solution/users'; const FlexItem = styled(EuiFlexItem)` @@ -84,6 +88,8 @@ export interface StatItemsProps extends StatItems { narrowDateRange: UpdateDateRange; to: string; showInspectButton?: boolean; + loading: boolean; + setQuerySkip: (skip: boolean) => void; } export const numberFormatter = (value: string | number): string => value.toLocaleString(); @@ -176,33 +182,27 @@ export const useKpiMatrixStatus = ( id: string, from: string, to: string, - narrowDateRange: UpdateDateRange -): StatItemsProps[] => { - const [statItemsProps, setStatItemsProps] = useState(mappings as StatItemsProps[]); - - useEffect(() => { - setStatItemsProps( - mappings.map((stat) => { - return { - ...stat, - areaChart: stat.enableAreaChart ? addValueToAreaChart(stat.fields, data) : undefined, - barChart: stat.enableBarChart ? addValueToBarChart(stat.fields, data) : undefined, - fields: addValueToFields(stat.fields, data), - id, - key: `kpi-summary-${stat.key}`, - statKey: `${stat.key}`, - from, - to, - narrowDateRange, - }; - }) - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [data]); - - return statItemsProps; -}; - + narrowDateRange: UpdateDateRange, + setQuerySkip: (skip: boolean) => void, + loading: boolean +): StatItemsProps[] => + mappings.map((stat) => ({ + ...stat, + areaChart: stat.enableAreaChart ? addValueToAreaChart(stat.fields, data) : undefined, + barChart: stat.enableBarChart ? addValueToBarChart(stat.fields, data) : undefined, + fields: addValueToFields(stat.fields, data), + id, + key: `kpi-summary-${stat.key}`, + statKey: `${stat.key}`, + from, + to, + narrowDateRange, + setQuerySkip, + loading, + })); +const StyledTitle = styled.h6` + line-height: 200%; +`; export const StatItemsComponent = React.memo( ({ areaChart, @@ -214,13 +214,15 @@ export const StatItemsComponent = React.memo( from, grow, id, - showInspectButton, + loading = false, + showInspectButton = true, index, narrowDateRange, statKey = 'item', to, barChartLensAttributes, areaChartLensAttributes, + setQuerySkip, }) => { const isBarChartDataAvailable = barChart && @@ -239,101 +241,143 @@ export const StatItemsComponent = React.memo( [from, to] ); + const { toggleStatus, setToggleStatus } = useQueryToggle(id); + + const toggleQuery = useCallback( + (status: boolean) => { + setToggleStatus(status); + // toggle on = skipQuery false + setQuerySkip(!status); + }, + [setQuerySkip, setToggleStatus] + ); + const toggle = useCallback(() => toggleQuery(!toggleStatus), [toggleQuery, toggleStatus]); + return ( - -
{description}
-
+ + + + + + + {description} + + +
- {showInspectButton && ( + {showInspectButton && toggleStatus && !loading && ( )}
+ {loading && ( + + + + + + )} + {toggleStatus && !loading && ( + <> + + {fields.map((field) => ( + + + {(isAreaChartDataAvailable || isBarChartDataAvailable) && field.icon && ( + + + + )} - - {fields.map((field) => ( - - - {(isAreaChartDataAvailable || isBarChartDataAvailable) && field.icon && ( - - - - )} + + + +

+ {field.value != null + ? field.value.toLocaleString() + : getEmptyTagValue()}{' '} + {field.description} +

+
+ {field.lensAttributes && timerange && ( + + )} +
+
+
+
+ ))} +
+ {(enableAreaChart || enableBarChart) && } + + {enableBarChart && ( - - -

- {field.value != null ? field.value.toLocaleString() : getEmptyTagValue()}{' '} - {field.description} -

-
- {field.lensAttributes && timerange && ( - - )} -
+
-
-
- ))} -
+ )} - {(enableAreaChart || enableBarChart) && } - - {enableBarChart && ( - - - - )} - - {enableAreaChart && from != null && to != null && ( - <> - - - - - )} - + {enableAreaChart && from != null && to != null && ( + <> + + + + + )} + + + )}
); @@ -344,6 +388,8 @@ export const StatItemsComponent = React.memo( prevProps.enableBarChart === nextProps.enableBarChart && prevProps.from === nextProps.from && prevProps.grow === nextProps.grow && + prevProps.loading === nextProps.loading && + prevProps.setQuerySkip === nextProps.setQuerySkip && prevProps.id === nextProps.id && prevProps.index === nextProps.index && prevProps.narrowDateRange === nextProps.narrowDateRange && diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.test.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.test.ts index e09dbe23d512ae..138fa99ef4074b 100644 --- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.test.ts @@ -6,7 +6,6 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; - import { useKibana } from '../../../common/lib/kibana'; import { useMatrixHistogram, useMatrixHistogramCombined } from '.'; import { MatrixHistogramType } from '../../../../common/search_strategy'; @@ -39,6 +38,7 @@ describe('useMatrixHistogram', () => { indexNames: [], stackByField: 'event.module', startDate: new Date(Date.now()).toISOString(), + skip: false, }; afterEach(() => { @@ -145,6 +145,17 @@ describe('useMatrixHistogram', () => { mockDnsSearchStrategyResponse.rawResponse.aggregations?.dns_name_query_count.buckets ); }); + + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { ...props }; + const { rerender } = renderHook(() => useMatrixHistogram(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(3); + }); }); describe('useMatrixHistogramCombined', () => { diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts index c49a9d0438b2db..f6670c98fc0eef 100644 --- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts @@ -229,6 +229,14 @@ export const useMatrixHistogram = ({ }; }, [matrixHistogramRequest, hostsSearch, skip]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + const runMatrixHistogramSearch = useCallback( (to: string, from: string) => { hostsSearch({ diff --git a/x-pack/plugins/security_solution/public/common/containers/query_toggle/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/query_toggle/index.test.tsx new file mode 100644 index 00000000000000..76f1c02dcb43c7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/query_toggle/index.test.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + renderHook, + act, + RenderResult, + WaitForNextUpdate, + cleanup, +} from '@testing-library/react-hooks'; +import { QueryToggle, useQueryToggle } from '.'; +import { RouteSpyState } from '../../utils/route/types'; +import { SecurityPageName } from '../../../../common/constants'; +import { useKibana } from '../../lib/kibana'; + +const mockRouteSpy: RouteSpyState = { + pageName: SecurityPageName.overview, + detailName: undefined, + tabName: undefined, + search: '', + pathName: '/', +}; +jest.mock('../../lib/kibana'); +jest.mock('../../utils/route/use_route_spy', () => ({ + useRouteSpy: () => [mockRouteSpy], +})); + +describe('useQueryToggle', () => { + let result: RenderResult; + let waitForNextUpdate: WaitForNextUpdate; + const mockSet = jest.fn(); + beforeAll(() => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + storage: { + get: () => true, + set: mockSet, + }, + }, + }); + }); + beforeEach(() => { + jest.clearAllMocks(); + }); + it('Toggles local storage', async () => { + await act(async () => { + ({ result, waitForNextUpdate } = renderHook(() => useQueryToggle('queryId'))); + await waitForNextUpdate(); + expect(result.current.toggleStatus).toEqual(true); + }); + act(() => { + result.current.setToggleStatus(false); + }); + expect(result.current.toggleStatus).toEqual(false); + expect(mockSet).toBeCalledWith('kibana.siem:queryId.query.toggle:overview', false); + cleanup(); + }); + it('null storage key, do not set', async () => { + await act(async () => { + ({ result, waitForNextUpdate } = renderHook(() => useQueryToggle())); + await waitForNextUpdate(); + expect(result.current.toggleStatus).toEqual(true); + }); + act(() => { + result.current.setToggleStatus(false); + }); + expect(mockSet).not.toBeCalled(); + cleanup(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/containers/query_toggle/index.tsx b/x-pack/plugins/security_solution/public/common/containers/query_toggle/index.tsx new file mode 100644 index 00000000000000..53bcd6b60fc1ba --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/query_toggle/index.tsx @@ -0,0 +1,55 @@ +/* + * 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 { useEffect, useCallback, useState } from 'react'; +import { useKibana } from '../../lib/kibana'; +import { useRouteSpy } from '../../utils/route/use_route_spy'; + +export const getUniqueStorageKey = (pageName: string, id?: string): string | null => + id && pageName.length > 0 ? `kibana.siem:${id}.query.toggle:${pageName}` : null; +export interface QueryToggle { + toggleStatus: boolean; + setToggleStatus: (b: boolean) => void; +} + +export const useQueryToggle = (id?: string): QueryToggle => { + const [{ pageName }] = useRouteSpy(); + const { + services: { storage }, + } = useKibana(); + const storageKey = getUniqueStorageKey(pageName, id); + + const [storageValue, setStorageValue] = useState( + storageKey != null ? storage.get(storageKey) ?? true : true + ); + + useEffect(() => { + if (storageKey != null) { + setStorageValue(storage.get(storageKey) ?? true); + } + }, [storage, storageKey]); + + const setToggleStatus = useCallback( + (isOpen: boolean) => { + if (storageKey != null) { + storage.set(storageKey, isOpen); + setStorageValue(isOpen); + } + }, + [storage, storageKey] + ); + + return id + ? { + toggleStatus: storageValue, + setToggleStatus, + } + : { + toggleStatus: true, + setToggleStatus: () => {}, + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/containers/query_toggle/translations.tsx b/x-pack/plugins/security_solution/public/common/containers/query_toggle/translations.tsx new file mode 100644 index 00000000000000..acb64e7e6b5109 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/query_toggle/translations.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const QUERY_BUTTON_TITLE = (buttonOn: boolean) => + buttonOn + ? i18n.translate('xpack.securitySolution.toggleQuery.on', { + defaultMessage: 'Open', + }) + : i18n.translate('xpack.securitySolution.toggleQuery.off', { + defaultMessage: 'Closed', + }); diff --git a/x-pack/plugins/security_solution/public/common/containers/use_search_strategy/index.test.ts b/x-pack/plugins/security_solution/public/common/containers/use_search_strategy/index.test.ts index 5bfa9028a0fe83..c1513b7a0485b7 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_search_strategy/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/containers/use_search_strategy/index.test.ts @@ -6,7 +6,7 @@ */ import { useSearchStrategy } from './index'; -import { renderHook } from '@testing-library/react-hooks'; +import { act, renderHook } from '@testing-library/react-hooks'; import { useObservable } from '@kbn/securitysolution-hook-utils'; import { FactoryQueryTypes } from '../../../../common/search_strategy'; @@ -200,4 +200,19 @@ describe('useSearchStrategy', () => { expect(start).toBeCalledWith(expect.objectContaining({ signal })); }); + it('skip = true will cancel any running request', () => { + const abortSpy = jest.fn(); + const signal = new AbortController().signal; + jest.spyOn(window, 'AbortController').mockReturnValue({ abort: abortSpy, signal }); + const factoryQueryType = 'fakeQueryType' as FactoryQueryTypes; + const localProps = { + ...userSearchStrategyProps, + skip: false, + factoryQueryType, + }; + const { rerender } = renderHook(() => useSearchStrategy(localProps)); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/containers/use_search_strategy/index.tsx b/x-pack/plugins/security_solution/public/common/containers/use_search_strategy/index.tsx index 77676a83d39b67..234cf039024baf 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_search_strategy/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/use_search_strategy/index.tsx @@ -96,6 +96,7 @@ export const useSearchStrategy = ({ factoryQueryType, initialResult, errorMessage, + skip = false, }: { factoryQueryType: QueryType; /** @@ -106,6 +107,7 @@ export const useSearchStrategy = ({ * Message displayed to the user on a Toast when an erro happens. */ errorMessage?: string; + skip?: boolean; }) => { const abortCtrl = useRef(new AbortController()); const { getTransformChangesIfTheyExist } = useTransforms(); @@ -154,6 +156,12 @@ export const useSearchStrategy = ({ }; }, []); + useEffect(() => { + if (skip) { + abortCtrl.current.abort(); + } + }, [skip]); + const [formatedResult, inspect] = useMemo( () => [ result diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx index d0b05587a4711f..4fc47421a720ec 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx @@ -12,7 +12,9 @@ import { mount } from 'enzyme'; import { TestProviders } from '../../../../common/mock'; import { AlertsCountPanel } from './index'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; +jest.mock('../../../../common/containers/query_toggle'); jest.mock('react-router-dom', () => { const actual = jest.requireActual('react-router-dom'); return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) }; @@ -22,6 +24,12 @@ describe('AlertsCountPanel', () => { const defaultProps = { signalIndexName: 'signalIndexName', }; + const mockSetToggle = jest.fn(); + const mockUseQueryToggle = useQueryToggle as jest.Mock; + beforeEach(() => { + jest.clearAllMocks(); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); + }); it('renders correctly', async () => { await act(async () => { @@ -54,4 +62,38 @@ describe('AlertsCountPanel', () => { }); }); }); + describe('toggleQuery', () => { + it('toggles', async () => { + await act(async () => { + const wrapper = mount( + + + + ); + wrapper.find('[data-test-subj="query-toggle-header"]').first().simulate('click'); + expect(mockSetToggle).toBeCalledWith(false); + }); + }); + it('toggleStatus=true, render', async () => { + await act(async () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="alertsCountTable"]').exists()).toEqual(true); + }); + }); + it('toggleStatus=false, hide', async () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + await act(async () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="alertsCountTable"]').exists()).toEqual(false); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx index 04b8f482fd1213..1c0e2144ad9d41 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx @@ -6,7 +6,7 @@ */ import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; -import React, { memo, useMemo, useState, useEffect } from 'react'; +import React, { memo, useMemo, useState, useEffect, useCallback } from 'react'; import uuid from 'uuid'; import type { Filter, Query } from '@kbn/es-query'; @@ -24,6 +24,7 @@ import type { AlertsCountAggregation } from './types'; import { DEFAULT_STACK_BY_FIELD } from '../common/config'; import { KpiPanel, StackByComboBox } from '../common/components'; import { useInspectButton } from '../common/hooks'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; export const DETECTIONS_ALERTS_COUNT_ID = 'detections-alerts-count'; @@ -64,6 +65,20 @@ export const AlertsCountPanel = memo( } }, [query, filters]); + const { toggleStatus, setToggleStatus } = useQueryToggle(DETECTIONS_ALERTS_COUNT_ID); + const [querySkip, setQuerySkip] = useState(!toggleStatus); + useEffect(() => { + setQuerySkip(!toggleStatus); + }, [toggleStatus]); + const toggleQuery = useCallback( + (status: boolean) => { + setToggleStatus(status); + // toggle on = skipQuery false + setQuerySkip(!status); + }, + [setQuerySkip, setToggleStatus] + ); + const { loading: isLoadingAlerts, data: alertsData, @@ -80,6 +95,7 @@ export const AlertsCountPanel = memo( runtimeMappings ), indexName: signalIndexName, + skip: querySkip, }); useEffect(() => { @@ -99,21 +115,26 @@ export const AlertsCountPanel = memo( }); return ( - - + + - + {toggleStatus && ( + + )} ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx index 29e18a1c49c12e..3135e2e1737935 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx @@ -12,9 +12,13 @@ import { mount } from 'enzyme'; import type { Filter } from '@kbn/es-query'; import { TestProviders } from '../../../../common/mock'; import { SecurityPageName } from '../../../../app/types'; +import { MatrixLoader } from '../../../../common/components/matrix_histogram/matrix_loader'; import { AlertsHistogramPanel } from './index'; import * as helpers from './helpers'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; + +jest.mock('../../../../common/containers/query_toggle'); jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); @@ -91,6 +95,12 @@ describe('AlertsHistogramPanel', () => { updateDateRange: jest.fn(), }; + const mockSetToggle = jest.fn(); + const mockUseQueryToggle = useQueryToggle as jest.Mock; + beforeEach(() => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); + }); + afterEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); @@ -339,4 +349,40 @@ describe('AlertsHistogramPanel', () => { `); }); }); + + describe('toggleQuery', () => { + it('toggles', async () => { + await act(async () => { + const wrapper = mount( + + + + ); + wrapper.find('[data-test-subj="query-toggle-header"]').first().simulate('click'); + expect(mockSetToggle).toBeCalledWith(false); + }); + }); + it('toggleStatus=true, render', async () => { + await act(async () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(MatrixLoader).exists()).toEqual(true); + }); + }); + it('toggleStatus=false, hide', async () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + await act(async () => { + const wrapper = mount( + + + + ); + expect(wrapper.find(MatrixLoader).exists()).toEqual(false); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx index 571f656389f6a7..84476c3ee68859 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx @@ -45,6 +45,7 @@ import type { AlertsStackByField } from '../common/types'; import { KpiPanel, StackByComboBox } from '../common/components'; import { useInspectButton } from '../common/hooks'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; const defaultTotalAlertsObj: AlertsTotal = { value: 0, @@ -116,6 +117,19 @@ export const AlertsHistogramPanel = memo( onlyField == null ? defaultStackByOption : onlyField ); + const { toggleStatus, setToggleStatus } = useQueryToggle(DETECTIONS_HISTOGRAM_ID); + const [querySkip, setQuerySkip] = useState(!toggleStatus); + useEffect(() => { + setQuerySkip(!toggleStatus); + }, [toggleStatus]); + const toggleQuery = useCallback( + (status: boolean) => { + setToggleStatus(status); + // toggle on = skipQuery false + setQuerySkip(!status); + }, + [setQuerySkip, setToggleStatus] + ); const { loading: isLoadingAlerts, data: alertsData, @@ -132,6 +146,7 @@ export const AlertsHistogramPanel = memo( runtimeMappings ), indexName: signalIndexName, + skip: querySkip, }); const kibana = useKibana(); @@ -270,17 +285,21 @@ export const AlertsHistogramPanel = memo( ); return ( - + ( - {isInitialLoading ? ( - - ) : ( - - )} + {toggleStatus ? ( + isInitialLoading ? ( + + ) : ( + + ) + ) : null} ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/components.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/components.tsx index 6a56f7bc220ac2..27f33409ae1a5b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/components.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/components.tsx @@ -12,17 +12,23 @@ import { PANEL_HEIGHT, MOBILE_PANEL_HEIGHT } from './config'; import { useStackByFields } from './hooks'; import * as i18n from './translations'; -export const KpiPanel = styled(EuiPanel)<{ height?: number }>` +export const KpiPanel = styled(EuiPanel)<{ height?: number; $toggleStatus: boolean }>` display: flex; flex-direction: column; position: relative; overflow: hidden; - - height: ${MOBILE_PANEL_HEIGHT}px; - @media only screen and (min-width: ${(props) => props.theme.eui.euiBreakpoints.m}) { + ${({ $toggleStatus }) => + $toggleStatus && + ` height: ${PANEL_HEIGHT}px; + `} } + ${({ $toggleStatus }) => + $toggleStatus && + ` + height: ${MOBILE_PANEL_HEIGHT}px; + `} `; interface StackedBySelectProps { selected: string; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.test.tsx index 277e2008601dc3..5ed7a219e50688 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.test.tsx @@ -129,4 +129,22 @@ describe('useQueryAlerts', () => { }); }); }); + + test('skip', async () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + await act(async () => { + const localProps = { query: mockAlertsQuery, indexName, skip: false }; + const { rerender, waitForNextUpdate } = renderHook< + [object, string], + ReturnQueryAlerts + >(() => useQueryAlerts(localProps)); + await waitForNextUpdate(); + await waitForNextUpdate(); + + localProps.skip = true; + act(() => rerender()); + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(2); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx index b2bbcdf277992e..2b98987e526756 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx @@ -94,6 +94,12 @@ export const useQueryAlerts = ({ if (!isEmpty(query) && !skip) { fetchData(); } + if (skip) { + setLoading(false); + isSubscribed = false; + abortCtrl.abort(); + } + return () => { isSubscribed = false; abortCtrl.abort(); diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/__snapshots__/index.test.tsx.snap index ed119568cdcb3a..bffd5e2261ad9a 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/__snapshots__/index.test.tsx.snap @@ -105,6 +105,7 @@ exports[`Authentication Table Component rendering it renders the authentication isInspect={false} loadPage={[MockFunction]} loading={false} + setQuerySkip={[MockFunction]} showMorePagesIndicator={true} totalCount={54} type="page" diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.test.tsx index 14dc1769dbd057..2ec333e3356397 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.test.tsx @@ -45,6 +45,7 @@ describe('Authentication Table Component', () => { isInspect={false} loading={false} loadPage={loadPage} + setQuerySkip={jest.fn()} showMorePagesIndicator={getOr( false, 'showMorePagesIndicator', diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx index 4402f6a210947c..2bbda82e153154 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx @@ -43,6 +43,7 @@ interface AuthenticationTableProps { loadPage: (newActivePage: number) => void; id: string; isInspect: boolean; + setQuerySkip: (skip: boolean) => void; showMorePagesIndicator: boolean; totalCount: number; type: hostsModel.HostsType; @@ -78,6 +79,7 @@ const AuthenticationTableComponent: React.FC = ({ isInspect, loading, loadPage, + setQuerySkip, showMorePagesIndicator, totalCount, type, @@ -133,6 +135,7 @@ const AuthenticationTableComponent: React.FC = ({ loading={loading} loadPage={loadPage} pageOfItems={data} + setQuerySkip={setQuerySkip} showMorePagesIndicator={showMorePagesIndicator} totalCount={fakeTotalCount} updateLimitPagination={updateLimitPagination} diff --git a/x-pack/plugins/security_solution/public/hosts/components/host_risk_score_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/host_risk_score_table/index.tsx index e4130eee219099..f4da6983fc590f 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/host_risk_score_table/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/host_risk_score_table/index.tsx @@ -54,6 +54,7 @@ interface HostRiskScoreTableProps { isInspect: boolean; loading: boolean; loadPage: (newActivePage: number) => void; + setQuerySkip: (skip: boolean) => void; severityCount: SeverityCount; totalCount: number; type: hostsModel.HostsType; @@ -71,6 +72,7 @@ const HostRiskScoreTableComponent: React.FC = ({ isInspect, loading, loadPage, + setQuerySkip, severityCount, totalCount, type, @@ -207,6 +209,7 @@ const HostRiskScoreTableComponent: React.FC = ({ loadPage={loadPage} onChange={onSort} pageOfItems={data} + setQuerySkip={setQuerySkip} showMorePagesIndicator={false} sorting={sort} split={true} diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/__snapshots__/index.test.tsx.snap index 59a00cbf190f68..f646fc12c4697c 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/__snapshots__/index.test.tsx.snap @@ -36,6 +36,7 @@ exports[`Hosts Table rendering it renders the default Hosts table 1`] = ` isInspect={false} loadPage={[MockFunction]} loading={false} + setQuerySkip={[MockFunction]} showMorePagesIndicator={false} totalCount={-1} type="page" diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx index 71efbb0a44d150..43dc31c68d1bc4 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx @@ -69,6 +69,7 @@ describe('Hosts Table', () => { fakeTotalCount={0} loading={false} loadPage={loadPage} + setQuerySkip={jest.fn()} showMorePagesIndicator={false} totalCount={-1} type={hostsModel.HostsType.page} @@ -91,6 +92,7 @@ describe('Hosts Table', () => { data={mockData} totalCount={0} fakeTotalCount={-1} + setQuerySkip={jest.fn()} showMorePagesIndicator={false} loadPage={loadPage} type={hostsModel.HostsType.page} @@ -113,6 +115,7 @@ describe('Hosts Table', () => { data={mockData} totalCount={0} fakeTotalCount={-1} + setQuerySkip={jest.fn()} showMorePagesIndicator={false} loadPage={loadPage} type={hostsModel.HostsType.page} @@ -136,6 +139,7 @@ describe('Hosts Table', () => { data={mockData} totalCount={0} fakeTotalCount={-1} + setQuerySkip={jest.fn()} showMorePagesIndicator={false} loadPage={loadPage} type={hostsModel.HostsType.page} diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx index 01306004844d8e..42c8254ffd1838 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx @@ -42,6 +42,7 @@ interface HostsTableProps { isInspect: boolean; loading: boolean; loadPage: (newActivePage: number) => void; + setQuerySkip: (skip: boolean) => void; showMorePagesIndicator: boolean; totalCount: number; type: hostsModel.HostsType; @@ -77,6 +78,7 @@ const HostsTableComponent: React.FC = ({ isInspect, loading, loadPage, + setQuerySkip, showMorePagesIndicator, totalCount, type, @@ -172,6 +174,7 @@ const HostsTableComponent: React.FC = ({ loadPage={loadPage} onChange={onChange} pageOfItems={data} + setQuerySkip={setQuerySkip} showMorePagesIndicator={showMorePagesIndicator} sorting={sorting} totalCount={fakeTotalCount} diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.test.tsx new file mode 100644 index 00000000000000..164b88399bbe91 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.test.tsx @@ -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 { useHostsKpiAuthentications } from '../../../containers/kpi_hosts/authentications'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import React from 'react'; +import { HostsKpiAuthentications } from './index'; + +jest.mock('../../../../common/containers/query_toggle'); +jest.mock('../../../containers/kpi_hosts/authentications'); +jest.mock('../common', () => ({ + KpiBaseComponentManage: () => , +})); + +describe('Authentications KPI', () => { + const mockUseHostsKpiAuthentications = useHostsKpiAuthentications as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', + indexNames: [], + narrowDateRange: jest.fn(), + setQuery: jest.fn(), + skip: false, + }; + beforeEach(() => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseHostsKpiAuthentications.mockReturnValue([ + false, + { + id: '123', + inspect: { + dsl: [], + response: [], + }, + refetch: jest.fn(), + }, + ]); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseHostsKpiAuthentications.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseHostsKpiAuthentications.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx index 1158c842e04cbc..f12eca88ffc950 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx @@ -5,17 +5,18 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { StatItems } from '../../../../common/components/stat_items'; import { kpiUserAuthenticationsAreaLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_user_authentications_area'; import { kpiUserAuthenticationsBarLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_user_authentications_bar'; import { kpiUserAuthenticationsMetricSuccessLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_user_authentications_metric_success'; import { kpiUserAuthenticationsMetricFailureLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_user_authentication_metric_failure'; -import { useHostsKpiAuthentications } from '../../../containers/kpi_hosts/authentications'; +import { useHostsKpiAuthentications, ID } from '../../../containers/kpi_hosts/authentications'; import { KpiBaseComponentManage } from '../common'; import { HostsKpiProps, HostsKpiChartColors } from '../types'; import * as i18n from './translations'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; export const fieldsMapping: Readonly = [ { @@ -57,12 +58,17 @@ const HostsKpiAuthenticationsComponent: React.FC = ({ setQuery, skip, }) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [loading, { refetch, id, inspect, ...data }] = useHostsKpiAuthentications({ filterQuery, endDate: to, indexNames, startDate: from, - skip, + skip: querySkip, }); return ( @@ -77,6 +83,7 @@ const HostsKpiAuthenticationsComponent: React.FC = ({ narrowDateRange={narrowDateRange} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} /> ); }; diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/common/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/common/index.tsx index e3460ec22e73ef..4296ae4984b958 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/common/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/common/index.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiFlexItem, EuiLoadingSpinner, EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; @@ -42,10 +42,11 @@ interface KpiBaseComponentProps { from: string; to: string; narrowDateRange: UpdateDateRange; + setQuerySkip: (skip: boolean) => void; } export const KpiBaseComponent = React.memo( - ({ fieldsMapping, data, id, loading = false, from, to, narrowDateRange }) => { + ({ fieldsMapping, data, id, loading = false, from, to, narrowDateRange, setQuerySkip }) => { const { cases } = useKibana().services; const CasesContext = cases.ui.getCasesContext(); const userPermissions = useGetUserCasesPermissions(); @@ -57,13 +58,11 @@ export const KpiBaseComponent = React.memo( id, from, to, - narrowDateRange + narrowDateRange, + setQuerySkip, + loading ); - if (loading) { - return ; - } - return ( @@ -87,11 +86,3 @@ export const KpiBaseComponent = React.memo( KpiBaseComponent.displayName = 'KpiBaseComponent'; export const KpiBaseComponentManage = manageQuery(KpiBaseComponent); - -export const KpiBaseComponentLoader: React.FC = () => ( - - - - - -); diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.test.tsx new file mode 100644 index 00000000000000..49b69865155641 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.test.tsx @@ -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 { useHostsKpiHosts } from '../../../containers/kpi_hosts/hosts'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import React from 'react'; +import { HostsKpiHosts } from './index'; + +jest.mock('../../../../common/containers/query_toggle'); +jest.mock('../../../containers/kpi_hosts/hosts'); +jest.mock('../common', () => ({ + KpiBaseComponentManage: () => , +})); + +describe('Hosts KPI', () => { + const mockUseHostsKpiHosts = useHostsKpiHosts as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', + indexNames: [], + narrowDateRange: jest.fn(), + setQuery: jest.fn(), + skip: false, + }; + beforeEach(() => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseHostsKpiHosts.mockReturnValue([ + false, + { + id: '123', + inspect: { + dsl: [], + response: [], + }, + refetch: jest.fn(), + }, + ]); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseHostsKpiHosts.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseHostsKpiHosts.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.tsx index 79118b66a3f719..b29bdddd44e351 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.tsx @@ -5,15 +5,16 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { StatItems } from '../../../../common/components/stat_items'; import { kpiHostAreaLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_host_area'; import { kpiHostMetricLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_host_metric'; -import { useHostsKpiHosts } from '../../../containers/kpi_hosts/hosts'; +import { useHostsKpiHosts, ID } from '../../../containers/kpi_hosts/hosts'; import { KpiBaseComponentManage } from '../common'; import { HostsKpiProps, HostsKpiChartColors } from '../types'; import * as i18n from './translations'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; export const fieldsMapping: Readonly = [ { @@ -42,12 +43,17 @@ const HostsKpiHostsComponent: React.FC = ({ setQuery, skip, }) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [loading, { refetch, id, inspect, ...data }] = useHostsKpiHosts({ filterQuery, endDate: to, indexNames, startDate: from, - skip, + skip: querySkip, }); return ( @@ -62,6 +68,7 @@ const HostsKpiHostsComponent: React.FC = ({ narrowDateRange={narrowDateRange} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} /> ); }; diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx index f515490252d400..0a86a9006b6370 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx @@ -11,6 +11,7 @@ import { EuiHorizontalRule, EuiIcon, EuiPanel, + EuiLoadingSpinner, EuiTitle, EuiText, } from '@elastic/eui'; @@ -22,7 +23,6 @@ import { BUTTON_CLASS as INPECT_BUTTON_CLASS, } from '../../../../common/components/inspect'; -import { KpiBaseComponentLoader } from '../common'; import * as i18n from './translations'; import { useInspectQuery } from '../../../../common/hooks/use_inspect_query'; @@ -36,6 +36,13 @@ import { HoverVisibilityContainer } from '../../../../common/components/hover_vi import { KpiRiskScoreStrategyResponse, RiskSeverity } from '../../../../../common/search_strategy'; import { RiskScore } from '../../../../common/components/severity/common'; +const KpiBaseComponentLoader: React.FC = () => ( + + + + + +); const QUERY_ID = 'hostsKpiRiskyHostsQuery'; const HostCount = styled(EuiText)` diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/index.test.tsx new file mode 100644 index 00000000000000..20de5db340b5e1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/index.test.tsx @@ -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 { useHostsKpiUniqueIps } from '../../../containers/kpi_hosts/unique_ips'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import React from 'react'; +import { HostsKpiUniqueIps } from './index'; + +jest.mock('../../../../common/containers/query_toggle'); +jest.mock('../../../containers/kpi_hosts/unique_ips'); +jest.mock('../common', () => ({ + KpiBaseComponentManage: () => , +})); + +describe('Unique IPs KPI', () => { + const mockUseHostsKpiUniqueIps = useHostsKpiUniqueIps as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', + indexNames: [], + narrowDateRange: jest.fn(), + setQuery: jest.fn(), + skip: false, + }; + beforeEach(() => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseHostsKpiUniqueIps.mockReturnValue([ + false, + { + id: '123', + inspect: { + dsl: [], + response: [], + }, + refetch: jest.fn(), + }, + ]); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseHostsKpiUniqueIps.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseHostsKpiUniqueIps.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/index.tsx index ef7bdfa1dc0313..ef032d041db7df 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/index.tsx @@ -5,17 +5,18 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { StatItems } from '../../../../common/components/stat_items'; import { kpiUniqueIpsAreaLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_area'; import { kpiUniqueIpsBarLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_bar'; import { kpiUniqueIpsDestinationMetricLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_destination_metric'; import { kpiUniqueIpsSourceMetricLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_source_metric'; -import { useHostsKpiUniqueIps } from '../../../containers/kpi_hosts/unique_ips'; +import { useHostsKpiUniqueIps, ID } from '../../../containers/kpi_hosts/unique_ips'; import { KpiBaseComponentManage } from '../common'; import { HostsKpiProps, HostsKpiChartColors } from '../types'; import * as i18n from './translations'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; export const fieldsMapping: Readonly = [ { @@ -57,12 +58,17 @@ const HostsKpiUniqueIpsComponent: React.FC = ({ setQuery, skip, }) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [loading, { refetch, id, inspect, ...data }] = useHostsKpiUniqueIps({ filterQuery, endDate: to, indexNames, startDate: from, - skip, + skip: querySkip, }); return ( @@ -77,6 +83,7 @@ const HostsKpiUniqueIpsComponent: React.FC = ({ narrowDateRange={narrowDateRange} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} /> ); }; diff --git a/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.test.tsx index 2f3a414344cfc8..5ff8696ae5be35 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.test.tsx @@ -5,16 +5,30 @@ * 2.0. */ -import { render } from '@testing-library/react'; +import { render, fireEvent } from '@testing-library/react'; import React from 'react'; import { TopHostScoreContributors } from '.'; import { TestProviders } from '../../../common/mock'; import { useHostRiskScore } from '../../../risk_score/containers'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; +jest.mock('../../../common/containers/query_toggle'); jest.mock('../../../risk_score/containers'); const useHostRiskScoreMock = useHostRiskScore as jest.Mock; - +const testProps = { + setQuery: jest.fn(), + deleteQuery: jest.fn(), + hostName: 'test-host-name', + from: '2020-07-07T08:20:18.966Z', + to: '2020-07-08T08:20:18.966Z', +}; describe('Host Risk Flyout', () => { + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const mockSetToggle = jest.fn(); + beforeEach(() => { + jest.clearAllMocks(); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); + }); it('renders', () => { useHostRiskScoreMock.mockReturnValueOnce([ true, @@ -26,13 +40,7 @@ describe('Host Risk Flyout', () => { const { queryByTestId } = render( - + ); @@ -69,13 +77,7 @@ describe('Host Risk Flyout', () => { const { queryAllByRole } = render( - + ); @@ -83,4 +85,66 @@ describe('Host Risk Flyout', () => { expect(queryAllByRole('row')[2]).toHaveTextContent('second'); expect(queryAllByRole('row')[3]).toHaveTextContent('third'); }); + + describe('toggleQuery', () => { + beforeEach(() => { + useHostRiskScoreMock.mockReturnValue([ + true, + { + data: [], + isModuleEnabled: true, + }, + ]); + }); + + test('toggleQuery updates toggleStatus', () => { + const { getByTestId } = render( + + + + ); + expect(useHostRiskScoreMock.mock.calls[0][0].skip).toEqual(false); + fireEvent.click(getByTestId('query-toggle-header')); + expect(mockSetToggle).toBeCalledWith(false); + expect(useHostRiskScoreMock.mock.calls[1][0].skip).toEqual(true); + }); + + test('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(useHostRiskScoreMock.mock.calls[0][0].skip).toEqual(false); + }); + + test('toggleStatus=true, render components', () => { + const { queryByTestId } = render( + + + + ); + expect(queryByTestId('topHostScoreContributors-table')).toBeTruthy(); + }); + + test('toggleStatus=false, do not render components', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + const { queryByTestId } = render( + + + + ); + expect(queryByTestId('topHostScoreContributors-table')).toBeFalsy(); + }); + + test('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + render( + + + + ); + expect(useHostRiskScoreMock.mock.calls[0][0].skip).toEqual(true); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.tsx index 8811a6b64e7fcc..a3b7022ee83ef5 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiFlexGroup, @@ -27,6 +27,7 @@ import { HostsComponentsQueryProps } from '../../pages/navigation/types'; import { RuleLink } from '../../../detections/pages/detection_engine/rules/all/use_columns'; import { HostRiskScoreQueryId, useHostRiskScore } from '../../../risk_score/containers'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; export interface TopHostScoreContributorsProps extends Pick { @@ -77,11 +78,27 @@ const TopHostScoreContributorsComponent: React.FC const sort = useMemo(() => ({ field: RiskScoreFields.timestamp, direction: Direction.desc }), []); + const { toggleStatus, setToggleStatus } = useQueryToggle(QUERY_ID); + const [querySkip, setQuerySkip] = useState(!toggleStatus); + useEffect(() => { + setQuerySkip(!toggleStatus); + }, [toggleStatus]); + + const toggleQuery = useCallback( + (status: boolean) => { + setToggleStatus(status); + // toggle on = skipQuery false + setQuerySkip(!status); + }, + [setQuerySkip, setToggleStatus] + ); + const [loading, { data, refetch, inspect }] = useHostRiskScore({ filterQuery: hostName ? buildHostNamesFilter([hostName]) : undefined, timerange, onlyLatest: false, sort, + skip: querySkip, pagination: { querySize: 1, cursorStart: 0, @@ -119,24 +136,37 @@ const TopHostScoreContributorsComponent: React.FC - - - - - - - - - - - + {toggleStatus && ( + + + + )} + + {toggleStatus && ( + + + + + + )} ); diff --git a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/__snapshots__/index.test.tsx.snap index a93c4062e88084..19a6018f6b6806 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/__snapshots__/index.test.tsx.snap @@ -205,6 +205,7 @@ exports[`Uncommon Process Table Component rendering it renders the default Uncom isInspect={false} loadPage={[MockFunction]} loading={false} + setQuerySkip={[MockFunction]} showMorePagesIndicator={true} totalCount={5} type="page" diff --git a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx index 29d3f110e8181f..300abc60818cb6 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx @@ -36,21 +36,24 @@ describe('Uncommon Process Table Component', () => { const loadPage = jest.fn(); const mount = useMountAppended(); + const defaultProps = { + data: mockData.edges, + fakeTotalCount: getOr(50, 'fakeTotalCount', mockData.pageInfo), + id: 'uncommonProcess', + isInspect: false, + loading: false, + loadPage, + setQuerySkip: jest.fn(), + showMorePagesIndicator: getOr(false, 'showMorePagesIndicator', mockData.pageInfo), + totalCount: mockData.totalCount, + type: hostsModel.HostsType.page, + }; + describe('rendering', () => { test('it renders the default Uncommon process table', () => { const wrapper = shallow( - + ); @@ -60,17 +63,7 @@ describe('Uncommon Process Table Component', () => { test('it has a double dash (empty value) without any hosts at all', () => { const wrapper = mount( - + ); expect(wrapper.find('.euiTableRow').at(0).find('.euiTableRowCell').at(3).text()).toBe( @@ -81,17 +74,7 @@ describe('Uncommon Process Table Component', () => { test('it has a single host without any extra comma when the number of hosts is exactly 1', () => { const wrapper = mount( - + ); @@ -103,17 +86,7 @@ describe('Uncommon Process Table Component', () => { test('it has a single link when the number of hosts is exactly 1', () => { const wrapper = mount( - + ); @@ -125,17 +98,7 @@ describe('Uncommon Process Table Component', () => { test('it has a comma separated list of hosts when the number of hosts is greater than 1', () => { const wrapper = mount( - + ); @@ -147,17 +110,7 @@ describe('Uncommon Process Table Component', () => { test('it has 2 links when the number of hosts is equal to 2', () => { const wrapper = mount( - + ); @@ -169,17 +122,7 @@ describe('Uncommon Process Table Component', () => { test('it is empty when all hosts are invalid because they do not contain an id and a name', () => { const wrapper = mount( - + ); expect(wrapper.find('.euiTableRow').at(3).find('.euiTableRowCell').at(3).text()).toBe( @@ -190,17 +133,7 @@ describe('Uncommon Process Table Component', () => { test('it has no link when all hosts are invalid because they do not contain an id and a name', () => { const wrapper = mount( - + ); expect( @@ -211,17 +144,7 @@ describe('Uncommon Process Table Component', () => { test('it is returns two hosts when others are invalid because they do not contain an id and a name', () => { const wrapper = mount( - + ); expect(wrapper.find('.euiTableRow').at(4).find('.euiTableRowCell').at(3).text()).toBe( diff --git a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx index 0af27bdb0ba188..cbdae1747e5f68 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx @@ -30,6 +30,7 @@ interface UncommonProcessTableProps { isInspect: boolean; loading: boolean; loadPage: (newActivePage: number) => void; + setQuerySkip: (skip: boolean) => void; showMorePagesIndicator: boolean; totalCount: number; type: hostsModel.HostsType; @@ -72,6 +73,7 @@ const UncommonProcessTableComponent = React.memo( loading, loadPage, totalCount, + setQuerySkip, showMorePagesIndicator, type, }) => { @@ -125,6 +127,7 @@ const UncommonProcessTableComponent = React.memo( loading={loading} loadPage={loadPage} pageOfItems={data} + setQuerySkip={setQuerySkip} showMorePagesIndicator={showMorePagesIndicator} totalCount={fakeTotalCount} updateLimitPagination={updateLimitPagination} diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.test.tsx new file mode 100644 index 00000000000000..1f6ee4cb276ec8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.test.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../common/mock'; +import { useAuthentications } from './index'; +import { HostsType } from '../../store/model'; + +describe('authentications', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + type: HostsType.page, + skip: false, + }; + const { rerender } = renderHook(() => useAuthentications(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx index f446380e549376..1ff27e4b29917f 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx @@ -36,7 +36,7 @@ import * as i18n from './translations'; import { useTransforms } from '../../../transforms/containers/use_transforms'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -const ID = 'hostsAuthenticationsQuery'; +export const ID = 'hostsAuthenticationsQuery'; export interface AuthenticationArgs { authentications: AuthenticationsEdges[]; @@ -215,5 +215,13 @@ export const useAuthentications = ({ }; }, [authenticationsRequest, authenticationsSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, authenticationsResponse]; }; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.test.tsx new file mode 100644 index 00000000000000..df64f4cd6f81a8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.test.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../common/mock'; +import { useAllHost } from './index'; +import { HostsType } from '../../store/model'; + +describe('useAllHost', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + type: HostsType.page, + skip: false, + }; + const { rerender } = renderHook(() => useAllHost(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx index 1a9e86755cf7dc..c4259e8a5a737b 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx @@ -217,5 +217,13 @@ export const useAllHost = ({ }; }, [hostsRequest, hostsSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, hostsResponse]; }; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.test.tsx new file mode 100644 index 00000000000000..f62fc3a77786e4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../../common/mock'; +import { useHostsKpiAuthentications } from './index'; + +describe('kpi hosts - authentications', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + skip: false, + }; + const { rerender } = renderHook(() => useHostsKpiAuthentications(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx index c15c68d246f140..9fa38c14e2ea4e 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx @@ -26,7 +26,7 @@ import * as i18n from './translations'; import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; -const ID = 'hostsKpiAuthenticationsQuery'; +export const ID = 'hostsKpiAuthenticationsQuery'; export interface HostsKpiAuthenticationsArgs extends Omit { @@ -165,5 +165,13 @@ export const useHostsKpiAuthentications = ({ }; }, [hostsKpiAuthenticationsRequest, hostsKpiAuthenticationsSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, hostsKpiAuthenticationsResponse]; }; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.test.tsx new file mode 100644 index 00000000000000..f12b92f0661bc3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../../common/mock'; +import { useHostsKpiHosts } from './index'; + +describe('kpi hosts - hosts', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + skip: false, + }; + const { rerender } = renderHook(() => useHostsKpiHosts(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx index fdce4dfe79591a..63f0476c2b6319 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx @@ -26,7 +26,7 @@ import * as i18n from './translations'; import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; -const ID = 'hostsKpiHostsQuery'; +export const ID = 'hostsKpiHostsQuery'; export interface HostsKpiHostsArgs extends Omit { id: string; @@ -155,5 +155,13 @@ export const useHostsKpiHosts = ({ }; }, [hostsKpiHostsRequest, hostsKpiHostsSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, hostsKpiHostsResponse]; }; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/index.tsx deleted file mode 100644 index 8473d3971c66fc..00000000000000 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/index.tsx +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './authentications'; -export * from './hosts'; -export * from './unique_ips'; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.test.tsx new file mode 100644 index 00000000000000..ec8c73ad1d6a61 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../../common/mock'; +import { useHostsKpiUniqueIps } from './index'; + +describe('kpi hosts - Unique Ips', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + skip: false, + }; + const { rerender } = renderHook(() => useHostsKpiUniqueIps(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx index 5b9eeb2710ff32..25a9f76daf40f6 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx @@ -26,7 +26,7 @@ import * as i18n from './translations'; import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; -const ID = 'hostsKpiUniqueIpsQuery'; +export const ID = 'hostsKpiUniqueIpsQuery'; export interface HostsKpiUniqueIpsArgs extends Omit { @@ -163,5 +163,13 @@ export const useHostsKpiUniqueIps = ({ }; }, [hostsKpiUniqueIpsRequest, hostsKpiUniqueIpsSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, hostsKpiUniqueIpsResponse]; }; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.test.tsx new file mode 100644 index 00000000000000..e334465fdbc1c7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.test.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../common/mock'; +import { useUncommonProcesses } from './index'; +import { HostsType } from '../../store/model'; + +describe('useUncommonProcesses', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + type: HostsType.page, + skip: false, + }; + const { rerender } = renderHook(() => useUncommonProcesses(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx index 9548027520bd1a..d196c4ea01af1a 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx @@ -34,7 +34,7 @@ import { InspectResponse } from '../../../types'; import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -const ID = 'hostsUncommonProcessesQuery'; +export const ID = 'hostsUncommonProcessesQuery'; export interface UncommonProcessesArgs { id: string; @@ -202,5 +202,13 @@ export const useUncommonProcesses = ({ }; }, [uncommonProcessesRequest, uncommonProcessesSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, uncommonProcessesResponse]; }; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.test.tsx new file mode 100644 index 00000000000000..9d31b477a851a9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../common/mock'; +import { useAuthentications } from '../../containers/authentications'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; +import { AuthenticationsQueryTabBody } from './authentications_query_tab_body'; +import { HostsType } from '../../store/model'; + +jest.mock('../../containers/authentications'); +jest.mock('../../../common/containers/query_toggle'); +jest.mock('../../../common/lib/kibana'); + +describe('Authentications query tab body', () => { + const mockUseAuthentications = useAuthentications as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + indexNames: [], + setQuery: jest.fn(), + skip: false, + startDate: '2019-06-25T04:31:59.345Z', + endDate: '2019-06-25T06:31:59.345Z', + type: HostsType.page, + }; + beforeEach(() => { + jest.clearAllMocks(); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseAuthentications.mockReturnValue([ + false, + { + authentications: [], + id: '123', + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + totalCount: 0, + pageInfo: { activePage: 1, fakeTotalCount: 100, showMorePagesIndicator: false }, + loadPage: jest.fn(), + refetch: jest.fn(), + }, + ]); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseAuthentications.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseAuthentications.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx index 879f0fce02fd5d..1096085b93016c 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx @@ -6,7 +6,7 @@ */ import { getOr } from 'lodash/fp'; -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { AuthenticationTable } from '../../components/authentications_table'; import { manageQuery } from '../../../common/components/page/manage_query'; import { useAuthentications } from '../../containers/authentications'; @@ -22,6 +22,7 @@ import * as i18n from '../translations'; import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; import { authenticationLensAttributes } from '../../../common/components/visualization_actions/lens_attributes/hosts/authentication'; import { LensAttributes } from '../../../common/components/visualization_actions/types'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const AuthenticationTableManage = manageQuery(AuthenticationTable); @@ -76,6 +77,11 @@ const AuthenticationsQueryTabBodyComponent: React.FC startDate, type, }) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [ loading, { authentications, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, @@ -84,7 +90,7 @@ const AuthenticationsQueryTabBodyComponent: React.FC endDate, filterQuery, indexNames, - skip, + skip: querySkip, startDate, type, }); @@ -119,6 +125,7 @@ const AuthenticationsQueryTabBodyComponent: React.FC loading={loading} loadPage={loadPage} refetch={refetch} + setQuerySkip={setQuerySkip} showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} setQuery={setQuery} totalCount={totalCount} diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_score_tab_body.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_score_tab_body.test.tsx new file mode 100644 index 00000000000000..8b3a05cc3d88c4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_score_tab_body.test.tsx @@ -0,0 +1,81 @@ +/* + * 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 '@testing-library/react'; +import { TestProviders } from '../../../common/mock'; +import { useHostRiskScore, useHostRiskScoreKpi } from '../../../risk_score/containers'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; +import { HostRiskScoreQueryTabBody } from './host_risk_score_tab_body'; +import { HostsType } from '../../store/model'; + +jest.mock('../../../risk_score/containers'); +jest.mock('../../../common/containers/query_toggle'); +jest.mock('../../../common/lib/kibana'); + +describe('Host risk score query tab body', () => { + const mockUseHostRiskScore = useHostRiskScore as jest.Mock; + const mockUseHostRiskScoreKpi = useHostRiskScoreKpi as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + indexNames: [], + setQuery: jest.fn(), + skip: false, + startDate: '2019-06-25T04:31:59.345Z', + endDate: '2019-06-25T06:31:59.345Z', + type: HostsType.page, + }; + beforeEach(() => { + jest.clearAllMocks(); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseHostRiskScoreKpi.mockReturnValue({ + loading: false, + severityCount: { + unknown: 12, + low: 12, + moderate: 12, + high: 12, + critical: 12, + }, + }); + mockUseHostRiskScore.mockReturnValue([ + false, + { + hosts: [], + id: '123', + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + totalCount: 0, + pageInfo: { activePage: 1, fakeTotalCount: 100, showMorePagesIndicator: false }, + loadPage: jest.fn(), + refetch: jest.fn(), + }, + ]); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseHostRiskScore.mock.calls[0][0].skip).toEqual(false); + expect(mockUseHostRiskScoreKpi.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseHostRiskScore.mock.calls[0][0].skip).toEqual(true); + expect(mockUseHostRiskScoreKpi.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_score_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_score_tab_body.tsx index 11a422fa0cd3d6..11ba8d154cd819 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_score_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_score_tab_body.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { noop } from 'lodash/fp'; import { HostsComponentsQueryProps } from './types'; import { manageQuery } from '../../../common/components/page/manage_query'; @@ -18,6 +18,7 @@ import { useHostRiskScore, useHostRiskScoreKpi, } from '../../../risk_score/containers'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const HostRiskScoreTableManage = manageQuery(HostRiskScoreTable); @@ -43,15 +44,22 @@ export const HostRiskScoreQueryTabBody = ({ [activePage, limit] ); + const { toggleStatus } = useQueryToggle(HostRiskScoreQueryId.HOSTS_BY_RISK); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(!toggleStatus); + }, [toggleStatus]); + const [loading, { data, totalCount, inspect, isInspected, refetch }] = useHostRiskScore({ filterQuery, - skip, + skip: querySkip, pagination, sort, }); const { severityCount, loading: isKpiLoading } = useHostRiskScoreKpi({ filterQuery, + skip: querySkip, }); return ( @@ -65,6 +73,7 @@ export const HostRiskScoreQueryTabBody = ({ loadPage={noop} // It isn't necessary because PaginatedTable updates redux store and we load the page when activePage updates on the store refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} severityCount={severityCount} totalCount={totalCount} type={type} diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.test.tsx new file mode 100644 index 00000000000000..487934f30e8d6d --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../common/mock'; +import { useAllHost } from '../../containers/hosts'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; +import { HostsQueryTabBody } from './hosts_query_tab_body'; +import { HostsType } from '../../store/model'; + +jest.mock('../../containers/hosts'); +jest.mock('../../../common/containers/query_toggle'); +jest.mock('../../../common/lib/kibana'); + +describe('Hosts query tab body', () => { + const mockUseAllHost = useAllHost as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + indexNames: [], + setQuery: jest.fn(), + skip: false, + startDate: '2019-06-25T04:31:59.345Z', + endDate: '2019-06-25T06:31:59.345Z', + type: HostsType.page, + }; + beforeEach(() => { + jest.clearAllMocks(); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseAllHost.mockReturnValue([ + false, + { + hosts: [], + id: '123', + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + totalCount: 0, + pageInfo: { activePage: 1, fakeTotalCount: 100, showMorePagesIndicator: false }, + loadPage: jest.fn(), + refetch: jest.fn(), + }, + ]); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseAllHost.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseAllHost.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx index cc43cfed4619dc..b72e6572849d1d 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx @@ -6,11 +6,12 @@ */ import { getOr } from 'lodash/fp'; -import React from 'react'; -import { useAllHost } from '../../containers/hosts'; +import React, { useEffect, useState } from 'react'; +import { useAllHost, ID } from '../../containers/hosts'; import { HostsComponentsQueryProps } from './types'; import { HostsTable } from '../../components/hosts_table'; import { manageQuery } from '../../../common/components/page/manage_query'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const HostsTableManage = manageQuery(HostsTable); @@ -25,8 +26,21 @@ export const HostsQueryTabBody = ({ startDate, type, }: HostsComponentsQueryProps) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [loading, { hosts, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }] = - useAllHost({ docValueFields, endDate, filterQuery, indexNames, skip, startDate, type }); + useAllHost({ + docValueFields, + endDate, + filterQuery, + indexNames, + skip: querySkip, + startDate, + type, + }); return ( { + const mockUseUncommonProcesses = useUncommonProcesses as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + indexNames: [], + setQuery: jest.fn(), + skip: false, + startDate: '2019-06-25T04:31:59.345Z', + endDate: '2019-06-25T06:31:59.345Z', + type: HostsType.page, + }; + beforeEach(() => { + jest.clearAllMocks(); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseUncommonProcesses.mockReturnValue([ + false, + { + uncommonProcesses: [], + id: '123', + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + totalCount: 0, + pageInfo: { activePage: 1, fakeTotalCount: 100, showMorePagesIndicator: false }, + loadPage: jest.fn(), + refetch: jest.fn(), + }, + ]); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseUncommonProcesses.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseUncommonProcesses.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx index 236b732a5af05b..f6957fedd83c5f 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx @@ -6,11 +6,12 @@ */ import { getOr } from 'lodash/fp'; -import React from 'react'; -import { useUncommonProcesses } from '../../containers/uncommon_processes'; +import React, { useEffect, useState } from 'react'; +import { useUncommonProcesses, ID } from '../../containers/uncommon_processes'; import { HostsComponentsQueryProps } from './types'; import { UncommonProcessTable } from '../../components/uncommon_process_table'; import { manageQuery } from '../../../common/components/page/manage_query'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const UncommonProcessTableManage = manageQuery(UncommonProcessTable); @@ -25,6 +26,11 @@ export const UncommonProcessQueryTabBody = ({ startDate, type, }: HostsComponentsQueryProps) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [ loading, { uncommonProcesses, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, @@ -33,7 +39,7 @@ export const UncommonProcessQueryTabBody = ({ endDate, filterQuery, indexNames, - skip, + skip: querySkip, startDate, type, }); @@ -49,6 +55,7 @@ export const UncommonProcessQueryTabBody = ({ loadPage={loadPage} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} totalCount={totalCount} type={type} diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/embeddable.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/embeddable.test.tsx.snap index 8835a3ac390f35..966512170c1565 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/embeddable.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/embeddable.test.tsx.snap @@ -3,6 +3,7 @@ exports[`Embeddable it renders 1`] = `
(({ children }) => ( -
+
{children} diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx index 4b8a5b6dd99407..2166d6b495e750 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx @@ -109,7 +109,7 @@ describe('EmbeddedMapComponent', () => { beforeEach(() => { setQuery.mockClear(); - mockGetStorage.mockReturnValue(false); + mockGetStorage.mockReturnValue(true); }); afterEach(() => { @@ -190,36 +190,40 @@ describe('EmbeddedMapComponent', () => { }); test('map hidden on close', async () => { + mockGetStorage.mockReturnValue(false); const wrapper = mount( ); + expect(wrapper.find('[data-test-subj="siemEmbeddable"]').first().exists()).toEqual(false); + const container = wrapper.find('[data-test-subj="false-toggle-network-map"]').at(0); container.simulate('click'); await waitFor(() => { wrapper.update(); expect(mockSetStorage).toHaveBeenNthCalledWith(1, 'network_map_visbile', true); + expect(wrapper.find('[data-test-subj="siemEmbeddable"]').first().exists()).toEqual(true); }); }); test('map visible on open', async () => { - mockGetStorage.mockReturnValue(true); - const wrapper = mount( ); + expect(wrapper.find('[data-test-subj="siemEmbeddable"]').first().exists()).toEqual(true); const container = wrapper.find('[data-test-subj="true-toggle-network-map"]').at(0); container.simulate('click'); await waitFor(() => { wrapper.update(); expect(mockSetStorage).toHaveBeenNthCalledWith(1, 'network_map_visbile', false); + expect(wrapper.find('[data-test-subj="siemEmbeddable"]').first().exists()).toEqual(false); }); }); }); diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx index 803688bf213434..083f858dc77424 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx @@ -245,6 +245,29 @@ export const EmbeddedMapComponent = ({ [storage] ); + const content = useMemo(() => { + if (!storageValue) { + return null; + } + return ( + + + + + + + {isIndexError ? ( + + ) : embeddable != null ? ( + + ) : ( + + )} + + + ); + }, [embeddable, isIndexError, portalNode, services, storageValue]); + return isError ? null : ( - - - - - - - {isIndexError ? ( - - ) : embeddable != null ? ( - - ) : ( - - )} - - + {content} ); }; diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.test.tsx new file mode 100644 index 00000000000000..d5dee1b84f8d7b --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.test.tsx @@ -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 { useNetworkKpiDns } from '../../../containers/kpi_network/dns'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import React from 'react'; +import { NetworkKpiDns } from './index'; + +jest.mock('../../../../common/containers/query_toggle'); +jest.mock('../../../containers/kpi_network/dns'); +jest.mock('../../../../hosts/components/kpi_hosts/common', () => ({ + KpiBaseComponentManage: () => , +})); + +describe('DNS KPI', () => { + const mockUseNetworkKpiDns = useNetworkKpiDns as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', + indexNames: [], + narrowDateRange: jest.fn(), + setQuery: jest.fn(), + skip: false, + }; + beforeEach(() => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseNetworkKpiDns.mockReturnValue([ + false, + { + id: '123', + inspect: { + dsl: [], + response: [], + }, + refetch: jest.fn(), + }, + ]); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseNetworkKpiDns.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseNetworkKpiDns.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.tsx index 6291e7fd4dc126..94e81c2d80d4a5 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.tsx @@ -5,15 +5,16 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { StatItems } from '../../../../common/components/stat_items'; import { kpiDnsQueriesLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/network/kpi_dns_queries'; +import { useNetworkKpiDns, ID } from '../../../containers/kpi_network/dns'; import { KpiBaseComponentManage } from '../../../../hosts/components/kpi_hosts/common'; -import { useNetworkKpiDns } from '../../../containers/kpi_network/dns'; import { NetworkKpiProps } from '../types'; import * as i18n from './translations'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; export const fieldsMapping: Readonly = [ { @@ -38,12 +39,17 @@ const NetworkKpiDnsComponent: React.FC = ({ setQuery, skip, }) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [loading, { refetch, id, inspect, ...data }] = useNetworkKpiDns({ filterQuery, endDate: to, indexNames, startDate: from, - skip, + skip: querySkip, }); return ( @@ -58,6 +64,7 @@ const NetworkKpiDnsComponent: React.FC = ({ narrowDateRange={narrowDateRange} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} /> ); }; diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/mock.ts b/x-pack/plugins/security_solution/public/network/components/kpi_network/mock.ts index 6f35c4dead2507..f5ed1ebde6992e 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/mock.ts +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/mock.ts @@ -227,7 +227,9 @@ export const mockEnableChartsData = { ], from: '2019-06-15T06:00:00.000Z', id: 'statItem', + loading: false, statKey: 'UniqueIps', + setQuerySkip: jest.fn(), to: '2019-06-18T06:00:00.000Z', narrowDateRange: mockNarrowDateRange, areaChartLensAttributes: kpiUniquePrivateIpsAreaLensAttributes, diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.test.tsx new file mode 100644 index 00000000000000..87f1a173740f3d --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.test.tsx @@ -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 { useNetworkKpiNetworkEvents } from '../../../containers/kpi_network/network_events'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import React from 'react'; +import { NetworkKpiNetworkEvents } from './index'; + +jest.mock('../../../../common/containers/query_toggle'); +jest.mock('../../../containers/kpi_network/network_events'); +jest.mock('../../../../hosts/components/kpi_hosts/common', () => ({ + KpiBaseComponentManage: () => , +})); + +describe('Network Events KPI', () => { + const mockUseNetworkKpiNetworkEvents = useNetworkKpiNetworkEvents as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', + indexNames: [], + narrowDateRange: jest.fn(), + setQuery: jest.fn(), + skip: false, + }; + beforeEach(() => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseNetworkKpiNetworkEvents.mockReturnValue([ + false, + { + id: '123', + inspect: { + dsl: [], + response: [], + }, + refetch: jest.fn(), + }, + ]); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseNetworkKpiNetworkEvents.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseNetworkKpiNetworkEvents.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.tsx index ad2487b65f1de7..52aa98a117afac 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.tsx @@ -5,16 +5,16 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { euiPaletteColorBlind } from '@elastic/eui'; import { StatItems } from '../../../../common/components/stat_items'; -import { useNetworkKpiNetworkEvents } from '../../../containers/kpi_network/network_events'; - +import { ID, useNetworkKpiNetworkEvents } from '../../../containers/kpi_network/network_events'; import { NetworkKpiProps } from '../types'; import * as i18n from './translations'; import { kpiNetworkEventsLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/network/kpi_network_events'; import { KpiBaseComponentManage } from '../../../../hosts/components/kpi_hosts/common'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; const euiVisColorPalette = euiPaletteColorBlind(); const euiColorVis1 = euiVisColorPalette[1]; @@ -43,12 +43,17 @@ const NetworkKpiNetworkEventsComponent: React.FC = ({ setQuery, skip, }) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [loading, { refetch, id, inspect, ...data }] = useNetworkKpiNetworkEvents({ filterQuery, endDate: to, indexNames, startDate: from, - skip, + skip: querySkip, }); return ( @@ -63,6 +68,7 @@ const NetworkKpiNetworkEventsComponent: React.FC = ({ narrowDateRange={narrowDateRange} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} /> ); }; diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/tls_handshakes/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/tls_handshakes/index.test.tsx new file mode 100644 index 00000000000000..28bf73eb6b2d6c --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/tls_handshakes/index.test.tsx @@ -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 { useNetworkKpiTlsHandshakes } from '../../../containers/kpi_network/tls_handshakes'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import React from 'react'; +import { NetworkKpiTlsHandshakes } from './index'; + +jest.mock('../../../../common/containers/query_toggle'); +jest.mock('../../../containers/kpi_network/tls_handshakes'); +jest.mock('../../../../hosts/components/kpi_hosts/common', () => ({ + KpiBaseComponentManage: () => , +})); + +describe('TLS Handshakes KPI', () => { + const mockUseNetworkKpiTlsHandshakes = useNetworkKpiTlsHandshakes as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', + indexNames: [], + narrowDateRange: jest.fn(), + setQuery: jest.fn(), + skip: false, + }; + beforeEach(() => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseNetworkKpiTlsHandshakes.mockReturnValue([ + false, + { + id: '123', + inspect: { + dsl: [], + response: [], + }, + refetch: jest.fn(), + }, + ]); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseNetworkKpiTlsHandshakes.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseNetworkKpiTlsHandshakes.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/tls_handshakes/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/tls_handshakes/index.tsx index 0bdbd0a23d9f1a..c25a4cd140108c 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/tls_handshakes/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/tls_handshakes/index.tsx @@ -5,14 +5,15 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { StatItems } from '../../../../common/components/stat_items'; import { kpiTlsHandshakesLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/network/kpi_tls_handshakes'; +import { useNetworkKpiTlsHandshakes, ID } from '../../../containers/kpi_network/tls_handshakes'; import { KpiBaseComponentManage } from '../../../../hosts/components/kpi_hosts/common'; -import { useNetworkKpiTlsHandshakes } from '../../../containers/kpi_network/tls_handshakes'; import { NetworkKpiProps } from '../types'; import * as i18n from './translations'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; export const fieldsMapping: Readonly = [ { @@ -37,12 +38,17 @@ const NetworkKpiTlsHandshakesComponent: React.FC = ({ setQuery, skip, }) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [loading, { refetch, id, inspect, ...data }] = useNetworkKpiTlsHandshakes({ filterQuery, endDate: to, indexNames, startDate: from, - skip, + skip: querySkip, }); return ( @@ -57,6 +63,7 @@ const NetworkKpiTlsHandshakesComponent: React.FC = ({ narrowDateRange={narrowDateRange} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} /> ); }; diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_flows/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_flows/index.test.tsx new file mode 100644 index 00000000000000..c1a28bdc28692f --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_flows/index.test.tsx @@ -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 { useNetworkKpiUniqueFlows } from '../../../containers/kpi_network/unique_flows'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import React from 'react'; +import { NetworkKpiUniqueFlows } from './index'; + +jest.mock('../../../../common/containers/query_toggle'); +jest.mock('../../../containers/kpi_network/unique_flows'); +jest.mock('../../../../hosts/components/kpi_hosts/common', () => ({ + KpiBaseComponentManage: () => , +})); + +describe('Unique Flows KPI', () => { + const mockUseNetworkKpiUniqueFlows = useNetworkKpiUniqueFlows as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', + indexNames: [], + narrowDateRange: jest.fn(), + setQuery: jest.fn(), + skip: false, + }; + beforeEach(() => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseNetworkKpiUniqueFlows.mockReturnValue([ + false, + { + id: '123', + inspect: { + dsl: [], + response: [], + }, + refetch: jest.fn(), + }, + ]); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseNetworkKpiUniqueFlows.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseNetworkKpiUniqueFlows.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_flows/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_flows/index.tsx index 5c3624130b36f8..d6874818ab901d 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_flows/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_flows/index.tsx @@ -5,14 +5,15 @@ * 2.0. */ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { StatItems } from '../../../../common/components/stat_items'; import { kpiUniqueFlowIdsLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/network/kpi_unique_flow_ids'; +import { useNetworkKpiUniqueFlows, ID } from '../../../containers/kpi_network/unique_flows'; import { KpiBaseComponentManage } from '../../../../hosts/components/kpi_hosts/common'; -import { useNetworkKpiUniqueFlows } from '../../../containers/kpi_network/unique_flows'; import { NetworkKpiProps } from '../types'; import * as i18n from './translations'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; export const fieldsMapping: Readonly = [ { @@ -37,12 +38,17 @@ const NetworkKpiUniqueFlowsComponent: React.FC = ({ setQuery, skip, }) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [loading, { refetch, id, inspect, ...data }] = useNetworkKpiUniqueFlows({ filterQuery, endDate: to, indexNames, startDate: from, - skip, + skip: querySkip, }); return ( @@ -57,6 +63,7 @@ const NetworkKpiUniqueFlowsComponent: React.FC = ({ narrowDateRange={narrowDateRange} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} /> ); }; diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_private_ips/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_private_ips/index.test.tsx new file mode 100644 index 00000000000000..25807f3dc2cad8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_private_ips/index.test.tsx @@ -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 { useNetworkKpiUniquePrivateIps } from '../../../containers/kpi_network/unique_private_ips'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import React from 'react'; +import { NetworkKpiUniquePrivateIps } from './index'; + +jest.mock('../../../../common/containers/query_toggle'); +jest.mock('../../../containers/kpi_network/unique_private_ips'); +jest.mock('../../../../hosts/components/kpi_hosts/common', () => ({ + KpiBaseComponentManage: () => , +})); + +describe('Unique Private IPs KPI', () => { + const mockUseNetworkKpiUniquePrivateIps = useNetworkKpiUniquePrivateIps as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', + indexNames: [], + narrowDateRange: jest.fn(), + setQuery: jest.fn(), + skip: false, + }; + beforeEach(() => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseNetworkKpiUniquePrivateIps.mockReturnValue([ + false, + { + id: '123', + inspect: { + dsl: [], + response: [], + }, + refetch: jest.fn(), + }, + ]); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseNetworkKpiUniquePrivateIps.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseNetworkKpiUniquePrivateIps.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_private_ips/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_private_ips/index.tsx index e546deb7019e84..91791d09f81137 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_private_ips/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_private_ips/index.tsx @@ -5,11 +5,14 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { euiPaletteColorBlind } from '@elastic/eui'; import { StatItems } from '../../../../common/components/stat_items'; -import { useNetworkKpiUniquePrivateIps } from '../../../containers/kpi_network/unique_private_ips'; +import { + useNetworkKpiUniquePrivateIps, + ID, +} from '../../../containers/kpi_network/unique_private_ips'; import { NetworkKpiProps } from '../types'; import * as i18n from './translations'; import { kpiUniquePrivateIpsSourceMetricLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_source_metric'; @@ -17,6 +20,7 @@ import { kpiUniquePrivateIpsDestinationMetricLensAttributes } from '../../../../ import { kpiUniquePrivateIpsAreaLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_area'; import { kpiUniquePrivateIpsBarLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_bar'; import { KpiBaseComponentManage } from '../../../../hosts/components/kpi_hosts/common'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; const euiVisColorPalette = euiPaletteColorBlind(); const euiColorVis2 = euiVisColorPalette[2]; @@ -62,12 +66,17 @@ const NetworkKpiUniquePrivateIpsComponent: React.FC = ({ setQuery, skip, }) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [loading, { refetch, id, inspect, ...data }] = useNetworkKpiUniquePrivateIps({ filterQuery, endDate: to, indexNames, startDate: from, - skip, + skip: querySkip, }); return ( @@ -82,6 +91,7 @@ const NetworkKpiUniquePrivateIpsComponent: React.FC = ({ narrowDateRange={narrowDateRange} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} /> ); }; diff --git a/x-pack/plugins/security_solution/public/network/components/network_dns_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/network_dns_table/__snapshots__/index.test.tsx.snap index 0119859d37672f..c43df33721bf1f 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_dns_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/network_dns_table/__snapshots__/index.test.tsx.snap @@ -141,6 +141,7 @@ exports[`NetworkTopNFlow Table Component rendering it renders the default Networ isInspect={false} loadPage={[MockFunction]} loading={false} + setQuerySkip={[MockFunction]} showMorePagesIndicator={true} totalCount={80} type="page" diff --git a/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx index fc28067866146c..2757baef2c1f4c 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx @@ -34,6 +34,19 @@ describe('NetworkTopNFlow Table Component', () => { let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); const mount = useMountAppended(); + const defaultProps = { + data: mockData.edges, + fakeTotalCount: getOr(50, 'fakeTotalCount', mockData.pageInfo), + id: 'dns', + isInspect: false, + loading: false, + loadPage, + setQuerySkip: jest.fn(), + showMorePagesIndicator: getOr(false, 'showMorePagesIndicator', mockData.pageInfo), + totalCount: mockData.totalCount, + type: networkModel.NetworkType.page, + }; + beforeEach(() => { store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); }); @@ -42,17 +55,7 @@ describe('NetworkTopNFlow Table Component', () => { test('it renders the default NetworkTopNFlow table', () => { const wrapper = shallow( - + ); @@ -64,17 +67,7 @@ describe('NetworkTopNFlow Table Component', () => { test('when you click on the column header, you should show the sorting icon', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.tsx index 016a40f7e2a179..a87908d27e63db 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.tsx @@ -32,6 +32,7 @@ interface NetworkDnsTableProps { isInspect: boolean; loading: boolean; loadPage: (newActivePage: number) => void; + setQuerySkip: (skip: boolean) => void; showMorePagesIndicator: boolean; totalCount: number; type: networkModel.NetworkType; @@ -56,6 +57,7 @@ const NetworkDnsTableComponent: React.FC = ({ loading, loadPage, showMorePagesIndicator, + setQuerySkip, totalCount, type, }) => { @@ -153,6 +155,7 @@ const NetworkDnsTableComponent: React.FC = ({ loadPage={loadPage} onChange={onChange} pageOfItems={data} + setQuerySkip={setQuerySkip} showMorePagesIndicator={showMorePagesIndicator} sorting={sorting} totalCount={fakeTotalCount} diff --git a/x-pack/plugins/security_solution/public/network/components/network_http_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/network_http_table/__snapshots__/index.test.tsx.snap index c5df0f6603fbf9..c26c85d311959a 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_http_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/network_http_table/__snapshots__/index.test.tsx.snap @@ -95,6 +95,7 @@ exports[`NetworkHttp Table Component rendering it renders the default NetworkHtt isInspect={false} loadPage={[MockFunction]} loading={false} + setQuerySkip={[MockFunction]} showMorePagesIndicator={false} totalCount={4} type="page" diff --git a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx index 2a85b31791f5a0..e8bac5e54765c9 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx @@ -31,6 +31,18 @@ jest.mock('../../../common/components/link_to'); describe('NetworkHttp Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; + const defaultProps = { + data: mockData.edges, + fakeTotalCount: getOr(50, 'fakeTotalCount', mockData.pageInfo), + id: 'http', + isInspect: false, + loading: false, + loadPage, + setQuerySkip: jest.fn(), + showMorePagesIndicator: getOr(false, 'showMorePagesIndicator', mockData.pageInfo), + totalCount: mockData.totalCount, + type: networkModel.NetworkType.page, + }; const { storage } = createSecuritySolutionStorageMock(); let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); @@ -44,17 +56,7 @@ describe('NetworkHttp Table Component', () => { test('it renders the default NetworkHttp table', () => { const wrapper = shallow( - + ); @@ -66,17 +68,7 @@ describe('NetworkHttp Table Component', () => { test('when you click on the column header, you should show the sorting icon', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.tsx index 2f0c4a105606c8..5bdfd45951292e 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.tsx @@ -23,6 +23,7 @@ interface NetworkHttpTableProps { isInspect: boolean; loading: boolean; loadPage: (newActivePage: number) => void; + setQuerySkip: (skip: boolean) => void; showMorePagesIndicator: boolean; totalCount: number; type: networkModel.NetworkType; @@ -46,6 +47,7 @@ const NetworkHttpTableComponent: React.FC = ({ isInspect, loading, loadPage, + setQuerySkip, showMorePagesIndicator, totalCount, type, @@ -123,6 +125,7 @@ const NetworkHttpTableComponent: React.FC = ({ loadPage={loadPage} onChange={onChange} pageOfItems={data} + setQuerySkip={setQuerySkip} showMorePagesIndicator={showMorePagesIndicator} sorting={sorting} totalCount={fakeTotalCount} diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/__snapshots__/index.test.tsx.snap index ecf7d2d0cd16f4..cd13be9cef38b2 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/__snapshots__/index.test.tsx.snap @@ -151,6 +151,7 @@ exports[`NetworkTopCountries Table Component rendering it renders the IP Details isInspect={false} loadPage={[MockFunction]} loading={false} + setQuerySkip={[MockFunction]} showMorePagesIndicator={true} totalCount={524} type="details" @@ -308,6 +309,7 @@ exports[`NetworkTopCountries Table Component rendering it renders the default Ne isInspect={false} loadPage={[MockFunction]} loading={false} + setQuerySkip={[MockFunction]} showMorePagesIndicator={true} totalCount={524} type="page" diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx index a0727fad65f188..12dc41961bdf5d 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx @@ -33,6 +33,24 @@ describe('NetworkTopCountries Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; const mount = useMountAppended(); + const defaultProps = { + data: mockData.NetworkTopCountries.edges, + fakeTotalCount: getOr(50, 'fakeTotalCount', mockData.NetworkTopCountries.pageInfo), + flowTargeted: FlowTargetSourceDest.source, + id: 'topCountriesSource', + indexPattern: mockIndexPattern, + isInspect: false, + loading: false, + loadPage, + setQuerySkip: jest.fn(), + showMorePagesIndicator: getOr( + false, + 'showMorePagesIndicator', + mockData.NetworkTopCountries.pageInfo + ), + totalCount: mockData.NetworkTopCountries.totalCount, + type: networkModel.NetworkType.page, + }; const { storage } = createSecuritySolutionStorageMock(); let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); @@ -45,23 +63,7 @@ describe('NetworkTopCountries Table Component', () => { test('it renders the default NetworkTopCountries table', () => { const wrapper = shallow( - + ); @@ -70,23 +72,7 @@ describe('NetworkTopCountries Table Component', () => { test('it renders the IP Details NetworkTopCountries table', () => { const wrapper = shallow( - + ); @@ -98,23 +84,7 @@ describe('NetworkTopCountries Table Component', () => { test('when you click on the column header, you should show the sorting icon', () => { const wrapper = mount( - + ); expect(store.getState().network.page.queries.topCountriesSource.sort).toEqual({ diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx index 80de694f89484b..00c9c7d0aaf30c 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx @@ -35,6 +35,7 @@ interface NetworkTopCountriesTableProps { isInspect: boolean; loading: boolean; loadPage: (newActivePage: number) => void; + setQuerySkip: (skip: boolean) => void; showMorePagesIndicator: boolean; totalCount: number; type: networkModel.NetworkType; @@ -62,6 +63,7 @@ const NetworkTopCountriesTableComponent: React.FC isInspect, loading, loadPage, + setQuerySkip, showMorePagesIndicator, totalCount, type, @@ -170,6 +172,7 @@ const NetworkTopCountriesTableComponent: React.FC loadPage={loadPage} onChange={onChange} pageOfItems={data} + setQuerySkip={setQuerySkip} showMorePagesIndicator={showMorePagesIndicator} sorting={{ field, direction: sort.direction }} totalCount={fakeTotalCount} diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/__snapshots__/index.test.tsx.snap index 07874f9f39f0b4..7909eba5b0d886 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/__snapshots__/index.test.tsx.snap @@ -99,6 +99,7 @@ exports[`NetworkTopNFlow Table Component rendering it renders the default Networ isInspect={false} loadPage={[MockFunction]} loading={false} + setQuerySkip={[MockFunction]} showMorePagesIndicator={true} totalCount={524} type="details" @@ -204,6 +205,7 @@ exports[`NetworkTopNFlow Table Component rendering it renders the default Networ isInspect={false} loadPage={[MockFunction]} loading={false} + setQuerySkip={[MockFunction]} showMorePagesIndicator={true} totalCount={524} type="page" diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx index e2b9447b588060..b5df028f4d7a4a 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx @@ -35,6 +35,19 @@ describe('NetworkTopNFlow Table Component', () => { const { storage } = createSecuritySolutionStorageMock(); let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); const mount = useMountAppended(); + const defaultProps = { + data: mockData.edges, + fakeTotalCount: getOr(50, 'fakeTotalCount', mockData.pageInfo), + flowTargeted: FlowTargetSourceDest.source, + id: 'topNFlowSource', + isInspect: false, + loading: false, + loadPage, + setQuerySkip: jest.fn(), + showMorePagesIndicator: getOr(false, 'showMorePagesIndicator', mockData.pageInfo), + totalCount: mockData.totalCount, + type: networkModel.NetworkType.page, + }; beforeEach(() => { store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); @@ -44,18 +57,7 @@ describe('NetworkTopNFlow Table Component', () => { test('it renders the default NetworkTopNFlow table on the Network page', () => { const wrapper = shallow( - + ); @@ -65,18 +67,7 @@ describe('NetworkTopNFlow Table Component', () => { test('it renders the default NetworkTopNFlow table on the IP Details page', () => { const wrapper = shallow( - + ); @@ -88,18 +79,7 @@ describe('NetworkTopNFlow Table Component', () => { test('when you click on the column header, you should show the sorting icon', () => { const wrapper = mount( - + ); expect(store.getState().network.page.queries.topNFlowSource.sort).toEqual({ diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.tsx index a612d3e4e1093c..12895226a82eba 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.tsx @@ -31,6 +31,7 @@ interface NetworkTopNFlowTableProps { isInspect: boolean; loading: boolean; loadPage: (newActivePage: number) => void; + setQuerySkip: (skip: boolean) => void; showMorePagesIndicator: boolean; totalCount: number; type: networkModel.NetworkType; @@ -57,6 +58,7 @@ const NetworkTopNFlowTableComponent: React.FC = ({ isInspect, loading, loadPage, + setQuerySkip, showMorePagesIndicator, totalCount, type, @@ -166,6 +168,7 @@ const NetworkTopNFlowTableComponent: React.FC = ({ loadPage={loadPage} onChange={onChange} pageOfItems={data} + setQuerySkip={setQuerySkip} showMorePagesIndicator={showMorePagesIndicator} sorting={sorting} totalCount={fakeTotalCount} diff --git a/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx index 3a1a5efef6b890..a54b219985817b 100644 --- a/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx @@ -29,7 +29,18 @@ jest.mock('../../../common/lib/kibana'); describe('Tls Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; - + const defaultProps = { + data: mockTlsData.edges, + fakeTotalCount: getOr(50, 'fakeTotalCount', mockTlsData.pageInfo), + id: 'tls', + isInspect: false, + loading: false, + loadPage, + setQuerySkip: jest.fn(), + showMorePagesIndicator: getOr(false, 'showMorePagesIndicator', mockTlsData.pageInfo), + totalCount: 1, + type: networkModel.NetworkType.details, + }; const { storage } = createSecuritySolutionStorageMock(); let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); const mount = useMountAppended(); @@ -42,17 +53,7 @@ describe('Tls Table Component', () => { test('it renders the default Domains table', () => { const wrapper = shallow( - + ); @@ -64,17 +65,7 @@ describe('Tls Table Component', () => { test('when you click on the column header, you should show the sorting icon', () => { const wrapper = mount( - + ); expect(store.getState().network.details.queries?.tls.sort).toEqual({ diff --git a/x-pack/plugins/security_solution/public/network/components/tls_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/tls_table/index.tsx index 34a218db39face..60079e50f27ceb 100644 --- a/x-pack/plugins/security_solution/public/network/components/tls_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/tls_table/index.tsx @@ -33,6 +33,7 @@ interface TlsTableProps { isInspect: boolean; loading: boolean; loadPage: (newActivePage: number) => void; + setQuerySkip: (skip: boolean) => void; showMorePagesIndicator: boolean; totalCount: number; type: networkModel.NetworkType; @@ -58,6 +59,7 @@ const TlsTableComponent: React.FC = ({ isInspect, loading, loadPage, + setQuerySkip, showMorePagesIndicator, totalCount, type, @@ -135,6 +137,7 @@ const TlsTableComponent: React.FC = ({ loadPage={loadPage} onChange={onChange} pageOfItems={data} + setQuerySkip={setQuerySkip} sorting={getSortField(sort)} totalCount={fakeTotalCount} updateActivePage={updateActivePage} diff --git a/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx index 3861433b4dcb01..95e014332d42af 100644 --- a/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx @@ -40,22 +40,25 @@ describe('Users Table Component', () => { store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); }); + const defaultProps = { + data: mockUsersData.edges, + flowTarget: FlowTarget.source, + fakeTotalCount: getOr(50, 'fakeTotalCount', mockUsersData.pageInfo), + id: 'user', + isInspect: false, + loading: false, + loadPage, + setQuerySkip: jest.fn(), + showMorePagesIndicator: getOr(false, 'showMorePagesIndicator', mockUsersData.pageInfo), + totalCount: 1, + type: networkModel.NetworkType.details, + }; + describe('Rendering', () => { test('it renders the default Users table', () => { const wrapper = shallow( - + ); @@ -67,18 +70,7 @@ describe('Users Table Component', () => { test('when you click on the column header, you should show the sorting icon', () => { const wrapper = mount( - + ); expect(store.getState().network.details.queries?.users.sort).toEqual({ diff --git a/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx index 66c36208fd98a2..efbe5b7d1d0106 100644 --- a/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx @@ -38,6 +38,7 @@ interface UsersTableProps { isInspect: boolean; loading: boolean; loadPage: (newActivePage: number) => void; + setQuerySkip: (skip: boolean) => void; showMorePagesIndicator: boolean; totalCount: number; type: networkModel.NetworkType; @@ -64,6 +65,7 @@ const UsersTableComponent: React.FC = ({ isInspect, loading, loadPage, + setQuerySkip, showMorePagesIndicator, totalCount, type, @@ -141,6 +143,7 @@ const UsersTableComponent: React.FC = ({ loadPage={loadPage} onChange={onChange} pageOfItems={data} + setQuerySkip={setQuerySkip} sorting={getSortField(sort)} totalCount={fakeTotalCount} updateActivePage={updateActivePage} diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.test.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.test.tsx new file mode 100644 index 00000000000000..44b8472a0606c1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../../common/mock'; +import { useNetworkKpiDns } from './index'; + +describe('kpi network - dns', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + skip: false, + }; + const { rerender } = renderHook(() => useNetworkKpiDns(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx index 63fb751572b0b7..89f58f547bd75a 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx @@ -30,7 +30,7 @@ import { import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; -const ID = 'networkKpiDnsQuery'; +export const ID = 'networkKpiDnsQuery'; export interface NetworkKpiDnsArgs { dnsQueries: number; @@ -160,5 +160,13 @@ export const useNetworkKpiDns = ({ }; }, [networkKpiDnsRequest, networkKpiDnsSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, networkKpiDnsResponse]; }; diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/index.tsx deleted file mode 100644 index 550cefcf13e920..00000000000000 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './dns'; -export * from './network_events'; -export * from './tls_handshakes'; -export * from './unique_flows'; -export * from './unique_private_ips'; diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.test.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.test.tsx new file mode 100644 index 00000000000000..4171a86fae9cc3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../../common/mock'; +import { useNetworkKpiNetworkEvents } from './index'; + +describe('kpi network - network events', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + skip: false, + }; + const { rerender } = renderHook(() => useNetworkKpiNetworkEvents(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx index 4ecf455a31724c..51a5367446b6e3 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx @@ -30,7 +30,7 @@ import { import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; -const ID = 'networkKpiNetworkEventsQuery'; +export const ID = 'networkKpiNetworkEventsQuery'; export interface NetworkKpiNetworkEventsArgs { networkEvents: number; @@ -163,5 +163,13 @@ export const useNetworkKpiNetworkEvents = ({ }; }, [networkKpiNetworkEventsRequest, networkKpiNetworkEventsSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, networkKpiNetworkEventsResponse]; }; diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.test.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.test.tsx new file mode 100644 index 00000000000000..bad0e6ad715128 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../../common/mock'; +import { useNetworkKpiTlsHandshakes } from './index'; + +describe('kpi network - tls handshakes', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + skip: false, + }; + const { rerender } = renderHook(() => useNetworkKpiTlsHandshakes(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx index 2dbf909334b157..ba42d79ad0eedc 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx @@ -30,7 +30,7 @@ import { import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; -const ID = 'networkKpiTlsHandshakesQuery'; +export const ID = 'networkKpiTlsHandshakesQuery'; export interface NetworkKpiTlsHandshakesArgs { tlsHandshakes: number; @@ -163,5 +163,13 @@ export const useNetworkKpiTlsHandshakes = ({ }; }, [networkKpiTlsHandshakesRequest, networkKpiTlsHandshakesSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, networkKpiTlsHandshakesResponse]; }; diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.test.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.test.tsx new file mode 100644 index 00000000000000..83cb2a40aabce8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../../common/mock'; +import { useNetworkKpiUniqueFlows } from './index'; + +describe('kpi network - unique flows', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + skip: false, + }; + const { rerender } = renderHook(() => useNetworkKpiUniqueFlows(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx index 612aac175fd9a2..130efc8d755a6e 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx @@ -29,7 +29,7 @@ import { import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; -const ID = 'networkKpiUniqueFlowsQuery'; +export const ID = 'networkKpiUniqueFlowsQuery'; export interface NetworkKpiUniqueFlowsArgs { uniqueFlowId: number; @@ -84,7 +84,6 @@ export const useNetworkKpiUniqueFlows = ({ const asyncSearch = async () => { abortCtrl.current = new AbortController(); setLoading(true); - searchSubscription$.current = data.search .search( request, @@ -155,5 +154,13 @@ export const useNetworkKpiUniqueFlows = ({ }; }, [networkKpiUniqueFlowsRequest, networkKpiUniqueFlowsSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, networkKpiUniqueFlowsResponse]; }; diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.test.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.test.tsx new file mode 100644 index 00000000000000..370c4e671e886f --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../../common/mock'; +import { useNetworkKpiUniquePrivateIps } from './index'; + +describe('kpi network - unique private ips', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + skip: false, + }; + const { rerender } = renderHook(() => useNetworkKpiUniquePrivateIps(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx index 42a8e30a8f9061..b68c4fcb698c0b 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx @@ -31,7 +31,7 @@ import { import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; -const ID = 'networkKpiUniquePrivateIpsQuery'; +export const ID = 'networkKpiUniquePrivateIpsQuery'; export interface NetworkKpiUniquePrivateIpsArgs { uniqueDestinationPrivateIps: number; @@ -175,5 +175,13 @@ export const useNetworkKpiUniquePrivateIps = ({ }; }, [networkKpiUniquePrivateIpsRequest, networkKpiUniquePrivateIpsSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, networkKpiUniquePrivateIpsResponse]; }; diff --git a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.test.tsx b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.test.tsx new file mode 100644 index 00000000000000..f303cdf85a5f8e --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.test.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../common/mock'; +import { useNetworkDns } from './index'; +import { NetworkType } from '../../store/model'; + +describe('useNetworkDns', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + docValueFields: [], + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + type: NetworkType.page, + skip: false, + }; + const { rerender } = renderHook(() => useNetworkDns(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx index 47e60f27a7dbdf..86949777dd535c 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx @@ -32,7 +32,7 @@ import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -const ID = 'networkDnsQuery'; +export const ID = 'networkDnsQuery'; export interface NetworkDnsArgs { id: string; @@ -207,5 +207,13 @@ export const useNetworkDns = ({ }; }, [networkDnsRequest, networkDnsSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, networkDnsResponse]; }; diff --git a/x-pack/plugins/security_solution/public/network/containers/network_http/index.test.tsx b/x-pack/plugins/security_solution/public/network/containers/network_http/index.test.tsx new file mode 100644 index 00000000000000..b687896efcea45 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/network_http/index.test.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../common/mock'; +import { useNetworkHttp } from './index'; +import { NetworkType } from '../../store/model'; + +describe('useNetworkHttp', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + docValueFields: [], + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + type: NetworkType.page, + skip: false, + }; + const { rerender } = renderHook(() => useNetworkHttp(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx index 98105f5cac25a5..eba2b22f30e296 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx @@ -31,7 +31,7 @@ import { InspectResponse } from '../../../types'; import { getInspectResponse } from '../../../helpers'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -const ID = 'networkHttpQuery'; +export const ID = 'networkHttpQuery'; export interface NetworkHttpArgs { id: string; @@ -94,7 +94,7 @@ export const useNetworkHttp = ({ const [networkHttpResponse, setNetworkHttpResponse] = useState({ networkHttp: [], - id: ID, + id, inspect: { dsl: [], response: [], @@ -116,11 +116,9 @@ export const useNetworkHttp = ({ if (request == null || skip) { return; } - const asyncSearch = async () => { abortCtrl.current = new AbortController(); setLoading(true); - searchSubscription$.current = data.search .search(request, { strategy: 'securitySolutionSearchStrategy', @@ -193,5 +191,13 @@ export const useNetworkHttp = ({ }; }, [networkHttpRequest, networkHttpSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, networkHttpResponse]; }; diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.test.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.test.tsx new file mode 100644 index 00000000000000..fe7507c85567a4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.test.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../common/mock'; +import { useNetworkTopCountries } from './index'; +import { NetworkType } from '../../store/model'; +import { FlowTargetSourceDest } from '../../../../common/search_strategy'; + +describe('useNetworkTopCountries', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + docValueFields: [], + flowTarget: FlowTargetSourceDest.source, + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + type: NetworkType.page, + skip: false, + }; + const { rerender } = renderHook(() => useNetworkTopCountries(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx index f64ee85ab7cf03..6110e84804fe37 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx @@ -32,7 +32,7 @@ import * as i18n from './translations'; import { useTransforms } from '../../../transforms/containers/use_transforms'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -const ID = 'networkTopCountriesQuery'; +export const ID = 'networkTopCountriesQuery'; export interface NetworkTopCountriesArgs { id: string; @@ -218,5 +218,13 @@ export const useNetworkTopCountries = ({ }; }, [networkTopCountriesRequest, networkTopCountriesSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, networkTopCountriesResponse]; }; diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.test.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.test.tsx new file mode 100644 index 00000000000000..c31dec3ce0aed4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.test.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../common/mock'; +import { useNetworkTopNFlow } from './index'; +import { NetworkType } from '../../store/model'; +import { FlowTargetSourceDest } from '../../../../common/search_strategy'; + +describe('useNetworkTopNFlow', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + docValueFields: [], + flowTarget: FlowTargetSourceDest.source, + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + type: NetworkType.page, + skip: false, + }; + const { rerender } = renderHook(() => useNetworkTopNFlow(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx index 0b4c164782f3d5..022b76c315c173 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx @@ -32,7 +32,7 @@ import * as i18n from './translations'; import { useTransforms } from '../../../transforms/containers/use_transforms'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -const ID = 'networkTopNFlowQuery'; +export const ID = 'networkTopNFlowQuery'; export interface NetworkTopNFlowArgs { id: string; @@ -215,5 +215,13 @@ export const useNetworkTopNFlow = ({ }; }, [networkTopNFlowRequest, networkTopNFlowSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, networkTopNFlowResponse]; }; diff --git a/x-pack/plugins/security_solution/public/network/containers/tls/index.test.tsx b/x-pack/plugins/security_solution/public/network/containers/tls/index.test.tsx new file mode 100644 index 00000000000000..6b236d4ddfb20d --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/tls/index.test.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../common/mock'; +import { useNetworkTls } from './index'; +import { NetworkType } from '../../store/model'; +import { FlowTargetSourceDest } from '../../../../common/search_strategy'; + +describe('useNetworkTls', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + docValueFields: [], + flowTarget: FlowTargetSourceDest.source, + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + ip: '1.1.1.1', + type: NetworkType.page, + skip: false, + }; + const { rerender } = renderHook(() => useNetworkTls(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx index 754f0cac8868c1..ed771455446c06 100644 --- a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx @@ -29,7 +29,7 @@ import { getInspectResponse } from '../../../helpers'; import { FlowTargetSourceDest, PageInfoPaginated } from '../../../../common/search_strategy'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -const ID = 'networkTlsQuery'; +export const ID = 'networkTlsQuery'; export interface NetworkTlsArgs { id: string; @@ -196,5 +196,13 @@ export const useNetworkTls = ({ }; }, [networkTlsRequest, networkTlsSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, networkTlsResponse]; }; diff --git a/x-pack/plugins/security_solution/public/network/containers/users/index.test.tsx b/x-pack/plugins/security_solution/public/network/containers/users/index.test.tsx new file mode 100644 index 00000000000000..4a6c1fac4191cc --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/users/index.test.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../common/mock'; +import { useNetworkUsers } from './index'; +import { NetworkType } from '../../store/model'; +import { FlowTarget } from '../../../../common/search_strategy'; + +describe('useNetworkUsers', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + docValueFields: [], + ip: '1.1.1.1', + flowTarget: FlowTarget.source, + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + type: NetworkType.page, + skip: false, + }; + const { rerender } = renderHook(() => useNetworkUsers(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/containers/users/index.tsx b/x-pack/plugins/security_solution/public/network/containers/users/index.tsx index d4be09f97591d6..9ad2c59f6bb798 100644 --- a/x-pack/plugins/security_solution/public/network/containers/users/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/users/index.tsx @@ -31,7 +31,7 @@ import { InspectResponse } from '../../../types'; import { PageInfoPaginated } from '../../../../common/search_strategy'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -const ID = 'networkUsersQuery'; +export const ID = 'networkUsersQuery'; export interface NetworkUsersArgs { id: string; @@ -195,5 +195,13 @@ export const useNetworkUsers = ({ }; }, [networkUsersRequest, networkUsersSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, networkUsersResponse]; }; diff --git a/x-pack/plugins/security_solution/public/network/pages/details/network_http_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/details/network_http_query_table.tsx index 4a4004b9a5f0ca..d615bd8264b4bd 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/network_http_query_table.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/network_http_query_table.tsx @@ -5,12 +5,13 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { getOr } from 'lodash/fp'; import { manageQuery } from '../../../common/components/page/manage_query'; import { OwnProps } from './types'; -import { useNetworkHttp } from '../../containers/network_http'; +import { useNetworkHttp, ID } from '../../containers/network_http'; import { NetworkHttpTable } from '../../components/network_http_table'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const NetworkHttpTableManage = manageQuery(NetworkHttpTable); @@ -24,6 +25,11 @@ export const NetworkHttpQueryTable = ({ startDate, type, }: OwnProps) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [ loading, { id, inspect, isInspected, loadPage, networkHttp, pageInfo, refetch, totalCount }, @@ -32,7 +38,7 @@ export const NetworkHttpQueryTable = ({ filterQuery, indexNames, ip, - skip, + skip: querySkip, startDate, type, }); @@ -48,6 +54,7 @@ export const NetworkHttpQueryTable = ({ loadPage={loadPage} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} totalCount={totalCount} type={type} diff --git a/x-pack/plugins/security_solution/public/network/pages/details/network_top_countries_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/details/network_top_countries_query_table.tsx index 742f0f6ff9a9dc..4243635ebb218d 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/network_top_countries_query_table.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/network_top_countries_query_table.tsx @@ -5,12 +5,13 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { getOr } from 'lodash/fp'; import { manageQuery } from '../../../common/components/page/manage_query'; import { NetworkWithIndexComponentsQueryTableProps } from './types'; -import { useNetworkTopCountries } from '../../containers/network_top_countries'; +import { useNetworkTopCountries, ID } from '../../containers/network_top_countries'; import { NetworkTopCountriesTable } from '../../components/network_top_countries_table'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const NetworkTopCountriesTableManage = manageQuery(NetworkTopCountriesTable); @@ -26,6 +27,11 @@ export const NetworkTopCountriesQueryTable = ({ type, indexPattern, }: NetworkWithIndexComponentsQueryTableProps) => { + const { toggleStatus } = useQueryToggle(`${ID}-${flowTarget}`); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [ loading, { id, inspect, isInspected, loadPage, networkTopCountries, pageInfo, refetch, totalCount }, @@ -35,7 +41,7 @@ export const NetworkTopCountriesQueryTable = ({ filterQuery, indexNames, ip, - skip, + skip: querySkip, startDate, type, }); @@ -53,6 +59,7 @@ export const NetworkTopCountriesQueryTable = ({ loadPage={loadPage} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} totalCount={totalCount} type={type} diff --git a/x-pack/plugins/security_solution/public/network/pages/details/network_top_n_flow_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/details/network_top_n_flow_query_table.tsx index 374dd6e6564e35..3df5397600c121 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/network_top_n_flow_query_table.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/network_top_n_flow_query_table.tsx @@ -6,11 +6,12 @@ */ import { getOr } from 'lodash/fp'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { manageQuery } from '../../../common/components/page/manage_query'; import { NetworkTopNFlowTable } from '../../components/network_top_n_flow_table'; -import { useNetworkTopNFlow } from '../../containers/network_top_n_flow'; +import { useNetworkTopNFlow, ID } from '../../containers/network_top_n_flow'; import { NetworkWithIndexComponentsQueryTableProps } from './types'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const NetworkTopNFlowTableManage = manageQuery(NetworkTopNFlowTable); @@ -25,6 +26,11 @@ export const NetworkTopNFlowQueryTable = ({ startDate, type, }: NetworkWithIndexComponentsQueryTableProps) => { + const { toggleStatus } = useQueryToggle(`${ID}-${flowTarget}`); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [ loading, { id, inspect, isInspected, loadPage, networkTopNFlow, pageInfo, refetch, totalCount }, @@ -34,7 +40,7 @@ export const NetworkTopNFlowQueryTable = ({ flowTarget, indexNames, ip, - skip, + skip: querySkip, startDate, type, }); @@ -51,6 +57,7 @@ export const NetworkTopNFlowQueryTable = ({ loadPage={loadPage} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} totalCount={totalCount} type={type} diff --git a/x-pack/plugins/security_solution/public/network/pages/details/tls_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/details/tls_query_table.tsx index d3da639c8cf988..f4539e1ffc63dd 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/tls_query_table.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/tls_query_table.tsx @@ -6,11 +6,12 @@ */ import { getOr } from 'lodash/fp'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { manageQuery } from '../../../common/components/page/manage_query'; import { TlsTable } from '../../components/tls_table'; -import { useNetworkTls } from '../../containers/tls'; +import { ID, useNetworkTls } from '../../containers/tls'; import { TlsQueryTableComponentProps } from './types'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const TlsTableManage = manageQuery(TlsTable); @@ -25,6 +26,11 @@ export const TlsQueryTable = ({ startDate, type, }: TlsQueryTableComponentProps) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [loading, { id, inspect, isInspected, tls, totalCount, pageInfo, loadPage, refetch }] = useNetworkTls({ endDate, @@ -32,7 +38,7 @@ export const TlsQueryTable = ({ flowTarget, indexNames, ip, - skip, + skip: querySkip, startDate, type, }); @@ -49,6 +55,7 @@ export const TlsQueryTable = ({ showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} totalCount={totalCount} type={type} /> diff --git a/x-pack/plugins/security_solution/public/network/pages/details/users_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/details/users_query_table.tsx index a73835985d7c53..9eb27c399ffbf8 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/users_query_table.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/users_query_table.tsx @@ -5,12 +5,13 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { getOr } from 'lodash/fp'; import { manageQuery } from '../../../common/components/page/manage_query'; -import { useNetworkUsers } from '../../containers/users'; +import { useNetworkUsers, ID } from '../../containers/users'; import { NetworkComponentsQueryProps } from './types'; import { UsersTable } from '../../components/users_table'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const UsersTableManage = manageQuery(UsersTable); @@ -24,6 +25,11 @@ export const UsersQueryTable = ({ startDate, type, }: NetworkComponentsQueryProps) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [ loading, { id, inspect, isInspected, networkUsers, totalCount, pageInfo, loadPage, refetch }, @@ -32,7 +38,7 @@ export const UsersQueryTable = ({ filterQuery, flowTarget, ip, - skip, + skip: querySkip, startDate, }); @@ -49,6 +55,7 @@ export const UsersQueryTable = ({ showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} totalCount={totalCount} type={type} /> diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/countries_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/countries_query_tab_body.tsx index e4bb00d1cb6322..b390ccdcfff826 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/countries_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/countries_query_tab_body.tsx @@ -5,15 +5,16 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { getOr } from 'lodash/fp'; import { NetworkTopCountriesTable } from '../../components/network_top_countries_table'; -import { useNetworkTopCountries } from '../../containers/network_top_countries'; +import { useNetworkTopCountries, ID } from '../../containers/network_top_countries'; import { networkModel } from '../../store'; import { manageQuery } from '../../../common/components/page/manage_query'; import { IPsQueryTabBodyProps as CountriesQueryTabBodyProps } from './types'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const NetworkTopCountriesTableManage = manageQuery(NetworkTopCountriesTable); @@ -27,6 +28,11 @@ export const CountriesQueryTabBody = ({ indexPattern, flowTarget, }: CountriesQueryTabBodyProps) => { + const { toggleStatus } = useQueryToggle(`${ID}-${flowTarget}`); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [ loading, { id, inspect, isInspected, loadPage, networkTopCountries, pageInfo, refetch, totalCount }, @@ -35,7 +41,7 @@ export const CountriesQueryTabBody = ({ flowTarget, filterQuery, indexNames, - skip, + skip: querySkip, startDate, type: networkModel.NetworkType.page, }); @@ -53,6 +59,7 @@ export const CountriesQueryTabBody = ({ loadPage={loadPage} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} totalCount={totalCount} type={networkModel.NetworkType.page} diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx index 21404690438a02..0ad309522a3e57 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import React, { useEffect, useCallback, useMemo } from 'react'; +import React, { useEffect, useCallback, useMemo, useState } from 'react'; import { getOr } from 'lodash/fp'; import { NetworkDnsTable } from '../../components/network_dns_table'; -import { useNetworkDns } from '../../containers/network_dns'; +import { useNetworkDns, ID } from '../../containers/network_dns'; import { manageQuery } from '../../../common/components/page/manage_query'; import { NetworkComponentQueryProps } from './types'; @@ -24,6 +24,7 @@ import { MatrixHistogramType } from '../../../../common/search_strategy/security import { networkSelectors } from '../../store'; import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; import { dnsTopDomainsLensAttributes } from '../../../common/components/visualization_actions/lens_attributes/network/dns_top_domains'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const HISTOGRAM_ID = 'networkDnsHistogramQuery'; @@ -72,6 +73,11 @@ const DnsQueryTabBodyComponent: React.FC = ({ }; }, [deleteQuery]); + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [ loading, { totalCount, networkDns, pageInfo, loadPage, id, inspect, isInspected, refetch }, @@ -80,7 +86,7 @@ const DnsQueryTabBodyComponent: React.FC = ({ endDate, filterQuery, indexNames, - skip, + skip: querySkip, startDate, type, }); @@ -122,6 +128,7 @@ const DnsQueryTabBodyComponent: React.FC = ({ loadPage={loadPage} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} totalCount={totalCount} type={type} diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/http_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/http_query_tab_body.tsx index bf9b0079650b27..98570a2f2f7409 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/http_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/http_query_tab_body.tsx @@ -5,15 +5,16 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { getOr } from 'lodash/fp'; import { NetworkHttpTable } from '../../components/network_http_table'; -import { useNetworkHttp } from '../../containers/network_http'; +import { ID, useNetworkHttp } from '../../containers/network_http'; import { networkModel } from '../../store'; import { manageQuery } from '../../../common/components/page/manage_query'; import { HttpQueryTabBodyProps } from './types'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const NetworkHttpTableManage = manageQuery(NetworkHttpTable); @@ -25,6 +26,11 @@ export const HttpQueryTabBody = ({ startDate, setQuery, }: HttpQueryTabBodyProps) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [ loading, { id, inspect, isInspected, loadPage, networkHttp, pageInfo, refetch, totalCount }, @@ -32,7 +38,7 @@ export const HttpQueryTabBody = ({ endDate, filterQuery, indexNames, - skip, + skip: querySkip, startDate, type: networkModel.NetworkType.page, }); @@ -48,6 +54,7 @@ export const HttpQueryTabBody = ({ loadPage={loadPage} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} totalCount={totalCount} type={networkModel.NetworkType.page} diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/ips_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/ips_query_tab_body.tsx index aa21fe60664153..a497a35fe35517 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/ips_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/ips_query_tab_body.tsx @@ -5,15 +5,16 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { getOr } from 'lodash/fp'; import { NetworkTopNFlowTable } from '../../components/network_top_n_flow_table'; -import { useNetworkTopNFlow } from '../../containers/network_top_n_flow'; +import { ID, useNetworkTopNFlow } from '../../containers/network_top_n_flow'; import { networkModel } from '../../store'; import { manageQuery } from '../../../common/components/page/manage_query'; import { IPsQueryTabBodyProps } from './types'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const NetworkTopNFlowTableManage = manageQuery(NetworkTopNFlowTable); @@ -26,6 +27,11 @@ export const IPsQueryTabBody = ({ setQuery, flowTarget, }: IPsQueryTabBodyProps) => { + const { toggleStatus } = useQueryToggle(`${ID}-${flowTarget}`); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [ loading, { id, inspect, isInspected, loadPage, networkTopNFlow, pageInfo, refetch, totalCount }, @@ -34,7 +40,7 @@ export const IPsQueryTabBody = ({ flowTarget, filterQuery, indexNames, - skip, + skip: querySkip, startDate, type: networkModel.NetworkType.page, }); @@ -51,6 +57,7 @@ export const IPsQueryTabBody = ({ loadPage={loadPage} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} totalCount={totalCount} type={networkModel.NetworkType.page} diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/tls_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/tls_query_tab_body.tsx index 58c6f755b91751..c06a26f5d91926 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/tls_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/tls_query_tab_body.tsx @@ -5,12 +5,13 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { getOr } from 'lodash/fp'; import { manageQuery } from '../../../common/components/page/manage_query'; -import { useNetworkTls } from '../../../network/containers/tls'; +import { useNetworkTls, ID } from '../../containers/tls'; import { TlsTable } from '../../components/tls_table'; import { TlsQueryTabBodyProps } from './types'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const TlsTableManage = manageQuery(TlsTable); @@ -25,6 +26,11 @@ const TlsQueryTabBodyComponent: React.FC = ({ startDate, type, }) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [loading, { id, inspect, isInspected, tls, totalCount, pageInfo, loadPage, refetch }] = useNetworkTls({ endDate, @@ -32,7 +38,7 @@ const TlsQueryTabBodyComponent: React.FC = ({ flowTarget, indexNames, ip, - skip, + skip: querySkip, startDate, type, }); @@ -49,6 +55,7 @@ const TlsQueryTabBodyComponent: React.FC = ({ showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} totalCount={totalCount} type={type} /> diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx index 1295693db506f9..173710a7700e8d 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx @@ -21,9 +21,12 @@ import { import { OverviewHost } from '.'; import { createStore, State } from '../../../common/store'; import { useHostOverview } from '../../containers/overview_host'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; +import { render } from '@testing-library/react'; jest.mock('../../../common/lib/kibana'); jest.mock('../../../common/components/link_to'); +jest.mock('../../../common/containers/query_toggle'); const startDate = '2020-01-20T20:49:57.080Z'; const endDate = '2020-01-21T20:49:57.080Z'; @@ -32,6 +35,7 @@ const testProps = { indexNames: [], setQuery: jest.fn(), startDate, + filterQuery: '', }; const MOCKED_RESPONSE = { overviewHost: { @@ -56,7 +60,7 @@ const MOCKED_RESPONSE = { jest.mock('../../containers/overview_host'); const useHostOverviewMock = useHostOverview as jest.Mock; -useHostOverviewMock.mockReturnValue([false, MOCKED_RESPONSE]); +const mockUseQueryToggle = useQueryToggle as jest.Mock; describe('OverviewHost', () => { const state: State = mockGlobalState; @@ -65,7 +69,10 @@ describe('OverviewHost', () => { let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); beforeEach(() => { + jest.clearAllMocks(); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); const myState = cloneDeep(state); + useHostOverviewMock.mockReturnValue([false, MOCKED_RESPONSE]); store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); }); @@ -103,4 +110,24 @@ describe('OverviewHost', () => { 'Showing: 16 events' ); }); + + test('toggleStatus=true, do not skip', () => { + const { queryByTestId } = render( + + + + ); + expect(useHostOverviewMock.mock.calls[0][0].skip).toEqual(false); + expect(queryByTestId('overview-hosts-stats')).toBeInTheDocument(); + }); + test('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + const { queryByTestId } = render( + + + + ); + expect(useHostOverviewMock.mock.calls[0][0].skip).toEqual(true); + expect(queryByTestId('overview-hosts-stats')).not.toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx index 32585c8836cc3a..1bf990b755f65d 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx @@ -9,7 +9,7 @@ import { isEmpty } from 'lodash/fp'; import { EuiFlexItem, EuiPanel } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useMemo, useCallback } from 'react'; +import React, { useMemo, useCallback, useState, useEffect } from 'react'; import { DEFAULT_NUMBER_FORMAT, APP_UI_ID } from '../../../../common/constants'; import { ESQuery } from '../../../../common/typed_json'; @@ -23,6 +23,7 @@ import { InspectButtonContainer } from '../../../common/components/inspect'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; import { SecurityPageName } from '../../../app/types'; import { LinkButton } from '../../../common/components/links'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; export interface OwnProps { startDate: GlobalTimeArgs['from']; @@ -46,12 +47,26 @@ const OverviewHostComponent: React.FC = ({ const { navigateToApp } = useKibana().services.application; const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const { toggleStatus, setToggleStatus } = useQueryToggle(OverviewHostQueryId); + const [querySkip, setQuerySkip] = useState(filterQuery === undefined || !toggleStatus); + useEffect(() => { + setQuerySkip(filterQuery === undefined || !toggleStatus); + }, [filterQuery, toggleStatus]); + const toggleQuery = useCallback( + (status: boolean) => { + setToggleStatus(status); + // toggle on = skipQuery false + setQuerySkip(!status); + }, + [setQuerySkip, setToggleStatus] + ); + const [loading, { overviewHost, id, inspect, refetch }] = useHostOverview({ endDate, filterQuery, indexNames, startDate, - skip: filterQuery === undefined, + skip: querySkip, }); const goToHost = useCallback( @@ -116,25 +131,29 @@ const OverviewHostComponent: React.FC = ({ return ( - + <>{hostPageButton} - - + {toggleStatus && ( + + )} diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx index dfc144be8e5bb8..2293a0380f3a8f 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx @@ -21,6 +21,8 @@ import { OverviewNetwork } from '.'; import { createStore, State } from '../../../common/store'; import { useNetworkOverview } from '../../containers/overview_network'; import { SecurityPageName } from '../../../app/types'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; +import { render } from '@testing-library/react'; jest.mock('../../../common/components/link_to'); const mockNavigateToApp = jest.fn(); @@ -46,6 +48,7 @@ const startDate = '2020-01-20T20:49:57.080Z'; const endDate = '2020-01-21T20:49:57.080Z'; const defaultProps = { endDate, + filterQuery: '', startDate, setQuery: jest.fn(), indexNames: [], @@ -65,9 +68,10 @@ const MOCKED_RESPONSE = { }, }; +jest.mock('../../../common/containers/query_toggle'); jest.mock('../../containers/overview_network'); const useNetworkOverviewMock = useNetworkOverview as jest.Mock; -useNetworkOverviewMock.mockReturnValue([false, MOCKED_RESPONSE]); +const mockUseQueryToggle = useQueryToggle as jest.Mock; describe('OverviewNetwork', () => { const state: State = mockGlobalState; @@ -76,6 +80,9 @@ describe('OverviewNetwork', () => { let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); beforeEach(() => { + jest.clearAllMocks(); + useNetworkOverviewMock.mockReturnValue([false, MOCKED_RESPONSE]); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); const myState = cloneDeep(state); store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); }); @@ -143,4 +150,24 @@ describe('OverviewNetwork', () => { deepLinkId: SecurityPageName.network, }); }); + + it('toggleStatus=true, do not skip', () => { + const { queryByTestId } = render( + + + + ); + expect(useNetworkOverviewMock.mock.calls[0][0].skip).toEqual(false); + expect(queryByTestId('overview-network-stats')).toBeInTheDocument(); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + const { queryByTestId } = render( + + + + ); + expect(useNetworkOverviewMock.mock.calls[0][0].skip).toEqual(true); + expect(queryByTestId('overview-network-stats')).not.toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx index 7607a9eac4926f..ce6c065d424d45 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx @@ -9,7 +9,7 @@ import { isEmpty } from 'lodash/fp'; import { EuiFlexItem, EuiPanel } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useMemo, useCallback } from 'react'; +import React, { useMemo, useCallback, useState, useEffect } from 'react'; import { DEFAULT_NUMBER_FORMAT, APP_UI_ID } from '../../../../common/constants'; import { ESQuery } from '../../../../common/typed_json'; @@ -26,6 +26,7 @@ import { InspectButtonContainer } from '../../../common/components/inspect'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; import { SecurityPageName } from '../../../app/types'; import { LinkButton } from '../../../common/components/links'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; export interface OverviewNetworkProps { startDate: GlobalTimeArgs['from']; @@ -48,12 +49,26 @@ const OverviewNetworkComponent: React.FC = ({ const { navigateToApp } = useKibana().services.application; const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const { toggleStatus, setToggleStatus } = useQueryToggle(OverviewNetworkQueryId); + const [querySkip, setQuerySkip] = useState(filterQuery === undefined || !toggleStatus); + useEffect(() => { + setQuerySkip(filterQuery === undefined || !toggleStatus); + }, [filterQuery, toggleStatus]); + const toggleQuery = useCallback( + (status: boolean) => { + setToggleStatus(status); + // toggle on = skipQuery false + setQuerySkip(!status); + }, + [setQuerySkip, setToggleStatus] + ); + const [loading, { overviewNetwork, id, inspect, refetch }] = useNetworkOverview({ endDate, filterQuery, indexNames, startDate, - skip: filterQuery === undefined, + skip: querySkip, }); const goToNetwork = useCallback( @@ -121,26 +136,30 @@ const OverviewNetworkComponent: React.FC = ({ return ( - + <> {networkPageButton} - - + {toggleStatus && ( + + )} diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.test.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.test.tsx new file mode 100644 index 00000000000000..53f07d5195c26a --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../common/mock'; +import { useHostOverview } from './index'; + +describe('useHostOverview', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + skip: false, + }; + const { rerender } = renderHook(() => useHostOverview(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx index 52b58439af0ab8..b79169b1ac762c 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx @@ -146,5 +146,12 @@ export const useHostOverview = ({ }; }, [overviewHostRequest, overviewHostSearch]); + useEffect(() => { + if (skip) { + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, overviewHostResponse]; }; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.test.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.test.tsx new file mode 100644 index 00000000000000..64cc2e6bbd1795 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../common/mock'; +import { useNetworkOverview } from './index'; + +describe('useNetworkOverview', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + skip: false, + }; + const { rerender } = renderHook(() => useNetworkOverview(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx index dd98a0ff036328..c2683b74a5b1ad 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx @@ -147,5 +147,12 @@ export const useNetworkOverview = ({ }; }, [overviewNetworkRequest, overviewNetworkSearch]); + useEffect(() => { + if (skip) { + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, overviewNetworkResponse]; }; diff --git a/x-pack/plugins/security_solution/public/risk_score/containers/all/index.tsx b/x-pack/plugins/security_solution/public/risk_score/containers/all/index.tsx index b04d9dd05f2838..8c95a081b3e86c 100644 --- a/x-pack/plugins/security_solution/public/risk_score/containers/all/index.tsx +++ b/x-pack/plugins/security_solution/public/risk_score/containers/all/index.tsx @@ -266,5 +266,13 @@ export const useRiskScore = { + if (skip) { + setLoading(false); + searchSubscription.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, riskScoreResponse]; }; diff --git a/x-pack/plugins/security_solution/public/users/components/kpi_users/total_users/index.test.tsx b/x-pack/plugins/security_solution/public/users/components/kpi_users/total_users/index.test.tsx new file mode 100644 index 00000000000000..6425f40016fb9a --- /dev/null +++ b/x-pack/plugins/security_solution/public/users/components/kpi_users/total_users/index.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQueryToggle } from '../../../../common/containers/query_toggle'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import React from 'react'; +import { TotalUsersKpi } from './index'; +import { useSearchStrategy } from '../../../../common/containers/use_search_strategy'; + +jest.mock('../../../../common/containers/query_toggle'); +jest.mock('../../../../common/containers/use_search_strategy'); +jest.mock('../../../../hosts/components/kpi_hosts/common', () => ({ + KpiBaseComponentManage: () => , +})); + +describe('Total Users KPI', () => { + const mockUseSearchStrategy = useSearchStrategy as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', + indexNames: [], + narrowDateRange: jest.fn(), + setQuery: jest.fn(), + skip: false, + }; + const mockSearch = jest.fn(); + beforeEach(() => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseSearchStrategy.mockReturnValue({ + result: [], + loading: false, + inspect: { + dsl: [], + response: [], + }, + search: mockSearch, + refetch: jest.fn(), + }); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseSearchStrategy.mock.calls[0][0].skip).toEqual(false); + expect(mockSearch).toHaveBeenCalled(); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseSearchStrategy.mock.calls[0][0].skip).toEqual(true); + expect(mockSearch).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/users/components/kpi_users/total_users/index.tsx b/x-pack/plugins/security_solution/public/users/components/kpi_users/total_users/index.tsx index 043c6b472497ee..ffa5d851875ce5 100644 --- a/x-pack/plugins/security_solution/public/users/components/kpi_users/total_users/index.tsx +++ b/x-pack/plugins/security_solution/public/users/components/kpi_users/total_users/index.tsx @@ -6,7 +6,7 @@ */ import { euiPaletteColorBlind } from '@elastic/eui'; -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { UsersQueries } from '../../../../../common/search_strategy/security_solution/users'; import { UpdateDateRange } from '../../../../common/components/charts/common'; @@ -17,6 +17,7 @@ import { KpiBaseComponentManage } from '../../../../hosts/components/kpi_hosts/c import { kpiTotalUsersMetricLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/users/kpi_total_users_metric'; import { kpiTotalUsersAreaLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/users/kpi_total_users_area'; import * as i18n from './translations'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; const euiVisColorPalette = euiPaletteColorBlind(); const euiColorVis1 = euiVisColorPalette[1]; @@ -60,15 +61,21 @@ const TotalUsersKpiComponent: React.FC = ({ setQuery, skip, }) => { + const { toggleStatus } = useQueryToggle(UsersQueries.kpiTotalUsers); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const { loading, result, search, refetch, inspect } = useSearchStrategy({ factoryQueryType: UsersQueries.kpiTotalUsers, initialResult: { users: 0, usersHistogram: [] }, errorMessage: i18n.ERROR_USERS_KPI, + skip: querySkip, }); useEffect(() => { - if (!skip) { + if (!querySkip) { search({ filterQuery, defaultIndex: indexNames, @@ -79,7 +86,7 @@ const TotalUsersKpiComponent: React.FC = ({ }, }); } - }, [search, from, to, filterQuery, indexNames, skip]); + }, [search, from, to, filterQuery, indexNames, querySkip]); return ( = ({ narrowDateRange={narrowDateRange} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} /> ); }; diff --git a/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.test.tsx b/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.test.tsx index 3faa96b436de01..c0cd2e351298e4 100644 --- a/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.test.tsx @@ -14,7 +14,7 @@ import { UsersType } from '../../store/model'; describe('UserRiskScoreTable', () => { const username = 'test_user_name'; - const defautProps = { + const defaultProps = { data: [ { '@timestamp': '1641902481', @@ -32,6 +32,7 @@ describe('UserRiskScoreTable', () => { isInspect: false, loading: false, loadPage: noop, + setQuerySkip: jest.fn(), severityCount: { Unknown: 0, Low: 0, @@ -46,7 +47,7 @@ describe('UserRiskScoreTable', () => { it('renders', () => { const { queryByTestId } = render( - + ); diff --git a/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.tsx b/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.tsx index 9f782b7f286623..810525d4f1ca7b 100644 --- a/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.tsx +++ b/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.tsx @@ -57,6 +57,7 @@ interface UserRiskScoreTableProps { isInspect: boolean; loading: boolean; loadPage: (newActivePage: number) => void; + setQuerySkip: (skip: boolean) => void; severityCount: SeverityCount; totalCount: number; type: usersModel.UsersType; @@ -74,6 +75,7 @@ const UserRiskScoreTableComponent: React.FC = ({ isInspect, loading, loadPage, + setQuerySkip, severityCount, totalCount, type, @@ -210,6 +212,7 @@ const UserRiskScoreTableComponent: React.FC = ({ loadPage={loadPage} onChange={onSort} pageOfItems={data} + setQuerySkip={setQuerySkip} showMorePagesIndicator={false} sorting={sort} split={true} diff --git a/x-pack/plugins/security_solution/public/users/pages/navigation/all_users_query_tab_body.test.tsx b/x-pack/plugins/security_solution/public/users/pages/navigation/all_users_query_tab_body.test.tsx new file mode 100644 index 00000000000000..98b69d531c4dc9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/users/pages/navigation/all_users_query_tab_body.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../common/mock'; +import { useAuthentications } from '../../../hosts/containers/authentications'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; +import { AllUsersQueryTabBody } from './all_users_query_tab_body'; +import { UsersType } from '../../store/model'; + +jest.mock('../../../hosts/containers/authentications'); +jest.mock('../../../common/containers/query_toggle'); +jest.mock('../../../common/lib/kibana'); + +describe('All users query tab body', () => { + const mockUseAuthentications = useAuthentications as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + indexNames: [], + setQuery: jest.fn(), + skip: false, + startDate: '2019-06-25T04:31:59.345Z', + endDate: '2019-06-25T06:31:59.345Z', + type: UsersType.page, + }; + beforeEach(() => { + jest.clearAllMocks(); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseAuthentications.mockReturnValue([ + false, + { + authentications: [], + id: '123', + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + totalCount: 0, + pageInfo: { activePage: 1, fakeTotalCount: 100, showMorePagesIndicator: false }, + loadPage: jest.fn(), + refetch: jest.fn(), + }, + ]); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseAuthentications.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseAuthentications.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/users/pages/navigation/all_users_query_tab_body.tsx b/x-pack/plugins/security_solution/public/users/pages/navigation/all_users_query_tab_body.tsx index 6c494c9752c4fd..8fa963ef179f29 100644 --- a/x-pack/plugins/security_solution/public/users/pages/navigation/all_users_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/navigation/all_users_query_tab_body.tsx @@ -6,12 +6,13 @@ */ import { getOr } from 'lodash/fp'; -import React from 'react'; -import { useAuthentications } from '../../../hosts/containers/authentications'; +import React, { useEffect, useState } from 'react'; +import { useAuthentications, ID } from '../../../hosts/containers/authentications'; import { UsersComponentsQueryProps } from './types'; import { AuthenticationTable } from '../../../hosts/components/authentications_table'; import { manageQuery } from '../../../common/components/page/manage_query'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const AuthenticationTableManage = manageQuery(AuthenticationTable); @@ -26,6 +27,11 @@ export const AllUsersQueryTabBody = ({ docValueFields, deleteQuery, }: UsersComponentsQueryProps) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [ loading, { authentications, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, @@ -34,7 +40,7 @@ export const AllUsersQueryTabBody = ({ endDate, filterQuery, indexNames, - skip, + skip: querySkip, startDate, // TODO Fix me // @ts-ignore @@ -55,6 +61,7 @@ export const AllUsersQueryTabBody = ({ refetch={refetch} showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} setQuery={setQuery} + setQuerySkip={setQuerySkip} totalCount={totalCount} docValueFields={docValueFields} indexNames={indexNames} diff --git a/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.test.tsx b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.test.tsx new file mode 100644 index 00000000000000..6b5ec66f864bb7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.test.tsx @@ -0,0 +1,81 @@ +/* + * 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 '@testing-library/react'; +import { TestProviders } from '../../../common/mock'; +import { useUserRiskScore, useUserRiskScoreKpi } from '../../../risk_score/containers'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; +import { UserRiskScoreQueryTabBody } from './user_risk_score_tab_body'; +import { UsersType } from '../../store/model'; + +jest.mock('../../../risk_score/containers'); +jest.mock('../../../common/containers/query_toggle'); +jest.mock('../../../common/lib/kibana'); + +describe('All users query tab body', () => { + const mockUseUserRiskScore = useUserRiskScore as jest.Mock; + const mockUseUserRiskScoreKpi = useUserRiskScoreKpi as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + indexNames: [], + setQuery: jest.fn(), + skip: false, + startDate: '2019-06-25T04:31:59.345Z', + endDate: '2019-06-25T06:31:59.345Z', + type: UsersType.page, + }; + beforeEach(() => { + jest.clearAllMocks(); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseUserRiskScore.mockReturnValue([ + false, + { + authentications: [], + id: '123', + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + totalCount: 0, + pageInfo: { activePage: 1, fakeTotalCount: 100, showMorePagesIndicator: false }, + loadPage: jest.fn(), + refetch: jest.fn(), + }, + ]); + mockUseUserRiskScoreKpi.mockReturnValue({ + loading: false, + severityCount: { + unknown: 12, + low: 12, + moderate: 12, + high: 12, + critical: 12, + }, + }); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseUserRiskScore.mock.calls[0][0].skip).toEqual(false); + expect(mockUseUserRiskScoreKpi.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseUserRiskScore.mock.calls[0][0].skip).toEqual(true); + expect(mockUseUserRiskScoreKpi.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.tsx b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.tsx index a19e7803cb90f2..a479788ce0f416 100644 --- a/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { noop } from 'lodash/fp'; import { UsersComponentsQueryProps } from './types'; @@ -20,6 +20,7 @@ import { useUserRiskScore, useUserRiskScoreKpi, } from '../../../risk_score/containers'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const UserRiskScoreTableManage = manageQuery(UserRiskScoreTable); @@ -43,15 +44,22 @@ export const UserRiskScoreQueryTabBody = ({ [activePage, limit] ); + const { toggleStatus } = useQueryToggle(UserRiskScoreQueryId.USERS_BY_RISK); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); + const [loading, { data, totalCount, inspect, isInspected, refetch }] = useUserRiskScore({ filterQuery, - skip, + skip: querySkip, pagination, sort, }); const { severityCount, loading: isKpiLoading } = useUserRiskScoreKpi({ filterQuery, + skip: querySkip, }); return ( @@ -65,6 +73,7 @@ export const UserRiskScoreQueryTabBody = ({ loadPage={noop} // It isn't necessary because PaginatedTable updates redux store and we load the page when activePage updates on the store refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} severityCount={severityCount} totalCount={totalCount} type={type} From 7aa89aac3bb5f2ba972aa4349259c53845becdbb Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Wed, 23 Mar 2022 11:52:52 -0700 Subject: [PATCH 55/64] Fix typos in dev docs (#128400) --- dev_docs/contributing/standards.mdx | 2 +- dev_docs/key_concepts/kibana_platform_plugin_intro.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev_docs/contributing/standards.mdx b/dev_docs/contributing/standards.mdx index d2f31f3a4faa21..cef9199aee9243 100644 --- a/dev_docs/contributing/standards.mdx +++ b/dev_docs/contributing/standards.mdx @@ -69,7 +69,7 @@ Every team should be collecting telemetry metrics on it’s public API usage. Th ### APM -Kibana server and client are instrumented with APM node and APM RUM clients respectively, tracking serveral types of transactions by default, such as `page-load`, `request`, etc. +Kibana server and client are instrumented with APM node and APM RUM clients respectively, tracking several types of transactions by default, such as `page-load`, `request`, etc. You may introduce custom transactions. Please refer to the [APM documentation](https://www.elastic.co/guide/en/apm/get-started/current/index.html) and follow these guidelines when doing so: - Use dashed syntax for transaction types and names: `my-transaction-type` and `my-transaction-name` diff --git a/dev_docs/key_concepts/kibana_platform_plugin_intro.mdx b/dev_docs/key_concepts/kibana_platform_plugin_intro.mdx index 195e5c1f6f2110..417d6e4983d4f7 100644 --- a/dev_docs/key_concepts/kibana_platform_plugin_intro.mdx +++ b/dev_docs/key_concepts/kibana_platform_plugin_intro.mdx @@ -153,7 +153,7 @@ plugins to customize the Kibana experience. Examples of extension points are: - core.overlays.showModal - embeddables.registerEmbeddableFactory - uiActions.registerAction -- core.saedObjects.registerType +- core.savedObjects.registerType ## Follow up material From 55e42cec93228a447b624c2b0001696712257efe Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Wed, 23 Mar 2022 20:26:00 +0100 Subject: [PATCH 56/64] [Cases] Allow custom toast title and content in cases hooks (#128145) --- .../cases/public/common/translations.ts | 15 +- .../public/common/use_cases_toast.test.tsx | 135 +++++++++++++++--- .../cases/public/common/use_cases_toast.tsx | 87 +++++++++-- .../use_cases_add_to_existing_case_modal.tsx | 16 ++- .../use_cases_add_to_new_case_flyout.tsx | 14 +- 5 files changed, 230 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts index 5c349a65dd8694..10005b2c87bce1 100644 --- a/x-pack/plugins/cases/public/common/translations.ts +++ b/x-pack/plugins/cases/public/common/translations.ts @@ -257,13 +257,22 @@ export const LINK_APPROPRIATE_LICENSE = i18n.translate('xpack.cases.common.appro export const CASE_SUCCESS_TOAST = (title: string) => i18n.translate('xpack.cases.actions.caseSuccessToast', { + values: { title }, + defaultMessage: '{title} has been updated', + }); + +export const CASE_ALERT_SUCCESS_TOAST = (title: string) => + i18n.translate('xpack.cases.actions.caseAlertSuccessToast', { values: { title }, defaultMessage: 'An alert has been added to "{title}"', }); -export const CASE_SUCCESS_SYNC_TEXT = i18n.translate('xpack.cases.actions.caseSuccessSyncText', { - defaultMessage: 'Alerts in this case have their status synched with the case status', -}); +export const CASE_ALERT_SUCCESS_SYNC_TEXT = i18n.translate( + 'xpack.cases.actions.caseAlertSuccessSyncText', + { + defaultMessage: 'Alerts in this case have their status synched with the case status', + } +); export const VIEW_CASE = i18n.translate('xpack.cases.actions.viewCase', { defaultMessage: 'View Case', diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx index 9bd6a6675a5c16..517d1cfdd77b14 100644 --- a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx @@ -9,33 +9,97 @@ import { renderHook } from '@testing-library/react-hooks'; import { useToasts } from '../common/lib/kibana'; import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../common/mock'; import { CaseToastSuccessContent, useCasesToast } from './use_cases_toast'; -import { mockCase } from '../containers/mock'; +import { alertComment, basicComment, mockCase } from '../containers/mock'; import React from 'react'; import userEvent from '@testing-library/user-event'; +import { SupportedCaseAttachment } from '../types'; jest.mock('../common/lib/kibana'); const useToastsMock = useToasts as jest.Mock; describe('Use cases toast hook', () => { + const successMock = jest.fn(); + + function validateTitle(title: string) { + const mockParams = successMock.mock.calls[0][0]; + const el = document.createElement('div'); + mockParams.title(el); + expect(el).toHaveTextContent(title); + } + + function validateContent(content: string) { + const mockParams = successMock.mock.calls[0][0]; + const el = document.createElement('div'); + mockParams.text(el); + expect(el).toHaveTextContent(content); + } + + useToastsMock.mockImplementation(() => { + return { + addSuccess: successMock, + }; + }); + + beforeEach(() => { + successMock.mockClear(); + }); + describe('Toast hook', () => { - const successMock = jest.fn(); - useToastsMock.mockImplementation(() => { - return { - addSuccess: successMock, - }; - }); - it('should create a success tost when invoked with a case', () => { + it('should create a success toast when invoked with a case', () => { const { result } = renderHook( () => { return useCasesToast(); }, { wrapper: TestProviders } ); - result.current.showSuccessAttach(mockCase); + result.current.showSuccessAttach({ + theCase: mockCase, + }); expect(successMock).toHaveBeenCalled(); }); }); + + describe('toast title', () => { + it('should create a success toast when invoked with a case and a custom title', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ theCase: mockCase, title: 'Custom title' }); + validateTitle('Custom title'); + }); + + it('should display the alert sync title when called with an alert attachment ', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ + theCase: mockCase, + attachments: [alertComment as SupportedCaseAttachment], + }); + validateTitle('An alert has been added to "Another horrible breach!!'); + }); + + it('should display a generic title when called with a non-alert attachament', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ + theCase: mockCase, + attachments: [basicComment as SupportedCaseAttachment], + }); + validateTitle('Another horrible breach!! has been updated'); + }); + }); describe('Toast content', () => { let appMockRender: AppMockRenderer; const onViewCaseClick = jest.fn(); @@ -44,20 +108,57 @@ describe('Use cases toast hook', () => { onViewCaseClick.mockReset(); }); - it('renders a correct successfull message with synced alerts', () => { - const result = appMockRender.render( - + it('should create a success toast when invoked with a case and a custom content', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } ); - expect(result.getByTestId('toaster-content-sync-text')).toHaveTextContent( - 'Alerts in this case have their status synched with the case status' + result.current.showSuccessAttach({ theCase: mockCase, content: 'Custom content' }); + validateContent('Custom content'); + }); + + it('renders an alert-specific content when called with an alert attachment and sync on', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ + theCase: mockCase, + attachments: [alertComment as SupportedCaseAttachment], + }); + validateContent('Alerts in this case have their status synched with the case status'); + }); + + it('renders empty content when called with an alert attachment and sync off', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ + theCase: { ...mockCase, settings: { ...mockCase.settings, syncAlerts: false } }, + attachments: [alertComment as SupportedCaseAttachment], + }); + validateContent('View Case'); + }); + + it('renders a correct successful message content', () => { + const result = appMockRender.render( + ); + expect(result.getByTestId('toaster-content-sync-text')).toHaveTextContent('my content'); expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View Case'); expect(onViewCaseClick).not.toHaveBeenCalled(); }); - it('renders a correct successfull message with not synced alerts', () => { + it('renders a correct successful message without content', () => { const result = appMockRender.render( - + ); expect(result.queryByTestId('toaster-content-sync-text')).toBeFalsy(); expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View Case'); @@ -66,7 +167,7 @@ describe('Use cases toast hook', () => { it('Calls the onViewCaseClick when clicked', () => { const result = appMockRender.render( - + ); userEvent.click(result.getByTestId('toaster-content-case-view-link')); expect(onViewCaseClick).toHaveBeenCalled(); diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.tsx index 98cc7fa1d8faa0..d02f792d601cf5 100644 --- a/x-pack/plugins/cases/public/common/use_cases_toast.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_toast.tsx @@ -9,10 +9,16 @@ import { EuiButtonEmpty, EuiText } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { Case } from '../../common'; +import { Case, CommentType } from '../../common'; import { useToasts } from '../common/lib/kibana'; import { useCaseViewNavigation } from '../common/navigation'; -import { CASE_SUCCESS_SYNC_TEXT, CASE_SUCCESS_TOAST, VIEW_CASE } from './translations'; +import { CaseAttachments } from '../types'; +import { + CASE_ALERT_SUCCESS_SYNC_TEXT, + CASE_ALERT_SUCCESS_TOAST, + CASE_SUCCESS_TOAST, + VIEW_CASE, +} from './translations'; const LINE_CLAMP = 3; const Title = styled.span` @@ -28,46 +34,101 @@ const EuiTextStyled = styled(EuiText)` `} `; +function getToastTitle({ + theCase, + title, + attachments, +}: { + theCase: Case; + title?: string; + attachments?: CaseAttachments; +}): string { + if (title !== undefined) { + return title; + } + if (attachments !== undefined) { + for (const attachment of attachments) { + if (attachment.type === CommentType.alert) { + return CASE_ALERT_SUCCESS_TOAST(theCase.title); + } + } + } + return CASE_SUCCESS_TOAST(theCase.title); +} + +function getToastContent({ + theCase, + content, + attachments, +}: { + theCase: Case; + content?: string; + attachments?: CaseAttachments; +}): string | undefined { + if (content !== undefined) { + return content; + } + if (attachments !== undefined) { + for (const attachment of attachments) { + if (attachment.type === CommentType.alert && theCase.settings.syncAlerts) { + return CASE_ALERT_SUCCESS_SYNC_TEXT; + } + } + } + return undefined; +} + export const useCasesToast = () => { const { navigateToCaseView } = useCaseViewNavigation(); const toasts = useToasts(); return { - showSuccessAttach: (theCase: Case) => { + showSuccessAttach: ({ + theCase, + attachments, + title, + content, + }: { + theCase: Case; + attachments?: CaseAttachments; + title?: string; + content?: string; + }) => { const onViewCaseClick = () => { navigateToCaseView({ detailName: theCase.id, }); }; + const renderTitle = getToastTitle({ theCase, title, attachments }); + const renderContent = getToastContent({ theCase, content, attachments }); + return toasts.addSuccess({ color: 'success', iconType: 'check', - title: toMountPoint({CASE_SUCCESS_TOAST(theCase.title)}), + title: toMountPoint({renderTitle}), text: toMountPoint( - + ), }); }, }; }; + export const CaseToastSuccessContent = ({ - syncAlerts, onViewCaseClick, + content, }: { - syncAlerts: boolean; onViewCaseClick: () => void; + content?: string; }) => { return ( <> - {syncAlerts && ( + {content !== undefined ? ( - {CASE_SUCCESS_SYNC_TEXT} + {content} - )} + ) : null} { +type AddToExistingFlyoutProps = AllCasesSelectorModalProps & { + toastTitle?: string; + toastContent?: string; +}; + +export const useCasesAddToExistingCaseModal = (props: AddToExistingFlyoutProps) => { const createNewCaseFlyout = useCasesAddToNewCaseFlyout({ attachments: props.attachments, onClose: props.onClose, @@ -25,6 +30,8 @@ export const useCasesAddToExistingCaseModal = (props: AllCasesSelectorModalProps return props.onRowClick(theCase); } }, + toastTitle: props.toastTitle, + toastContent: props.toastContent, }); const { dispatch } = useCasesContext(); const casesToasts = useCasesToast(); @@ -53,7 +60,12 @@ export const useCasesAddToExistingCaseModal = (props: AllCasesSelectorModalProps closeModal(); createNewCaseFlyout.open(); } else { - casesToasts.showSuccessAttach(theCase); + casesToasts.showSuccessAttach({ + theCase, + attachments: props.attachments, + title: props.toastTitle, + content: props.toastContent, + }); if (props.onRowClick) { props.onRowClick(theCase); } diff --git a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx index 5422ab9be995d9..c1c0793fe23405 100644 --- a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx +++ b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx @@ -12,7 +12,12 @@ import { CasesContextStoreActionsList } from '../../cases_context/cases_context_ import { useCasesContext } from '../../cases_context/use_cases_context'; import { CreateCaseFlyoutProps } from './create_case_flyout'; -export const useCasesAddToNewCaseFlyout = (props: CreateCaseFlyoutProps) => { +type AddToNewCaseFlyoutProps = CreateCaseFlyoutProps & { + toastTitle?: string; + toastContent?: string; +}; + +export const useCasesAddToNewCaseFlyout = (props: AddToNewCaseFlyoutProps) => { const { dispatch } = useCasesContext(); const casesToasts = useCasesToast(); @@ -35,7 +40,12 @@ export const useCasesAddToNewCaseFlyout = (props: CreateCaseFlyoutProps) => { }, onSuccess: async (theCase: Case) => { if (theCase) { - casesToasts.showSuccessAttach(theCase); + casesToasts.showSuccessAttach({ + theCase, + attachments: props.attachments, + title: props.toastTitle, + content: props.toastContent, + }); } if (props.onSuccess) { return props.onSuccess(theCase); From 506648c917e44a3941fa166832f7292804018c1e Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 23 Mar 2022 15:41:33 -0400 Subject: [PATCH 57/64] Mark `elasticsearch.serviceAccountToken` setting as GA (#128420) --- docs/setup/settings.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 2b36e1fb661858..23487f1ff3d88e 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -282,7 +282,7 @@ on the {kib} index at startup. {kib} users still need to authenticate with {es}, which is proxied through the {kib} server. |[[elasticsearch-service-account-token]] `elasticsearch.serviceAccountToken:` - | beta[]. If your {es} is protected with basic authentication, this token provides the credentials + | If your {es} is protected with basic authentication, this token provides the credentials that the {kib} server uses to perform maintenance on the {kib} index at startup. This setting is an alternative to `elasticsearch.username` and `elasticsearch.password`. From 838f3a67bf09b6de358e02ce5ff77858f9ed9b50 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 23 Mar 2022 14:25:39 -0600 Subject: [PATCH 58/64] [Security Solution] New landing page (#127324) --- .../security_solution/common/constants.ts | 15 +- .../cypress/screens/overview.ts | 2 +- .../public/app/deep_links/index.ts | 14 ++ .../public/app/home/home_navigations.ts | 8 + .../public/app/translations.ts | 4 + .../link_to/redirect_to_overview.tsx | 3 + .../common/components/navigation/types.ts | 1 + .../index.test.tsx | 13 +- .../use_navigation_items.tsx | 1 + .../common/components/url_state/constants.ts | 1 + .../common/components/url_state/helpers.ts | 3 + .../public/hosts/pages/hosts.test.tsx | 22 ++- .../public/network/pages/network.test.tsx | 19 ++- .../components/landing_cards/index.tsx | 156 ++++++++++++++++++ .../components/landing_cards/translations.tsx | 74 +++++++++ .../components/overview_empty/index.test.tsx | 88 +++------- .../components/overview_empty/index.tsx | 37 +---- .../public/overview/images/endpoint.png | Bin 0 -> 86401 bytes .../public/overview/images/siem.png | Bin 0 -> 345549 bytes .../public/overview/images/video.svg | 9 + .../public/overview/pages/landing.tsx | 25 +++ .../public/overview/pages/overview.test.tsx | 37 ++++- .../public/overview/routes.tsx | 17 +- .../public/users/pages/users_tabs.test.tsx | 14 +- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 26 files changed, 447 insertions(+), 120 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/overview/components/landing_cards/index.tsx create mode 100644 x-pack/plugins/security_solution/public/overview/components/landing_cards/translations.tsx create mode 100644 x-pack/plugins/security_solution/public/overview/images/endpoint.png create mode 100644 x-pack/plugins/security_solution/public/overview/images/siem.png create mode 100644 x-pack/plugins/security_solution/public/overview/images/video.svg create mode 100644 x-pack/plugins/security_solution/public/overview/pages/landing.tsx diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 2fd412eb357b61..cc64b7e640f1fa 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -92,36 +92,38 @@ export enum SecurityPageName { detectionAndResponse = 'detection_response', endpoints = 'endpoints', eventFilters = 'event_filters', - hostIsolationExceptions = 'host_isolation_exceptions', events = 'events', exceptions = 'exceptions', explore = 'explore', + hostIsolationExceptions = 'host_isolation_exceptions', hosts = 'hosts', hostsAnomalies = 'hosts-anomalies', hostsExternalAlerts = 'hosts-external_alerts', hostsRisk = 'hosts-risk', - users = 'users', - usersAnomalies = 'users-anomalies', - usersRisk = 'users-risk', investigate = 'investigate', + landing = 'get_started', network = 'network', networkAnomalies = 'network-anomalies', networkDns = 'network-dns', networkExternalAlerts = 'network-external_alerts', networkHttp = 'network-http', networkTls = 'network-tls', - timelines = 'timelines', - timelinesTemplates = 'timelines-templates', overview = 'overview', policies = 'policies', rules = 'rules', + timelines = 'timelines', + timelinesTemplates = 'timelines-templates', trustedApps = 'trusted_apps', uncommonProcesses = 'uncommon_processes', + users = 'users', + usersAnomalies = 'users-anomalies', + usersRisk = 'users-risk', } export const TIMELINES_PATH = '/timelines' as const; export const CASES_PATH = '/cases' as const; export const OVERVIEW_PATH = '/overview' as const; +export const LANDING_PATH = '/get_started' as const; export const DETECTION_RESPONSE_PATH = '/detection_response' as const; export const DETECTIONS_PATH = '/detections' as const; export const ALERTS_PATH = '/alerts' as const; @@ -140,6 +142,7 @@ export const HOST_ISOLATION_EXCEPTIONS_PATH = export const BLOCKLIST_PATH = `${MANAGEMENT_PATH}/blocklist` as const; export const APP_OVERVIEW_PATH = `${APP_PATH}${OVERVIEW_PATH}` as const; +export const APP_LANDING_PATH = `${APP_PATH}${LANDING_PATH}` as const; export const APP_DETECTION_RESPONSE_PATH = `${APP_PATH}${DETECTION_RESPONSE_PATH}` as const; export const APP_MANAGEMENT_PATH = `${APP_PATH}${MANAGEMENT_PATH}` as const; diff --git a/x-pack/plugins/security_solution/cypress/screens/overview.ts b/x-pack/plugins/security_solution/cypress/screens/overview.ts index e478f16e72844d..42f16340e6ac65 100644 --- a/x-pack/plugins/security_solution/cypress/screens/overview.ts +++ b/x-pack/plugins/security_solution/cypress/screens/overview.ts @@ -144,7 +144,7 @@ export const OVERVIEW_HOST_STATS = '[data-test-subj="overview-hosts-stats"]'; export const OVERVIEW_NETWORK_STATS = '[data-test-subj="overview-network-stats"]'; -export const OVERVIEW_EMPTY_PAGE = '[data-test-subj="empty-page"]'; +export const OVERVIEW_EMPTY_PAGE = '[data-test-subj="siem-landing-page"]'; export const OVERVIEW_REVENT_TIMELINES = '[data-test-subj="overview-recent-timelines"]'; diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index 144095d0aa528d..efb220467c9d06 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -32,9 +32,11 @@ import { TRUSTED_APPLICATIONS, POLICIES, ENDPOINTS, + GETTING_STARTED, } from '../translations'; import { OVERVIEW_PATH, + LANDING_PATH, DETECTION_RESPONSE_PATH, ALERTS_PATH, RULES_PATH, @@ -84,6 +86,18 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [ ], order: 9000, }, + { + id: SecurityPageName.landing, + title: GETTING_STARTED, + path: LANDING_PATH, + navLinkStatus: AppNavLinkStatus.visible, + features: [FEATURE.general], + keywords: [ + i18n.translate('xpack.securitySolution.search.getStarted', { + defaultMessage: 'Getting started', + }), + ], + }, { id: SecurityPageName.detectionAndResponse, title: DETECTION_RESPONSE, diff --git a/x-pack/plugins/security_solution/public/app/home/home_navigations.ts b/x-pack/plugins/security_solution/public/app/home/home_navigations.ts index 0b06d02d46464e..1ae5544dbd7408 100644 --- a/x-pack/plugins/security_solution/public/app/home/home_navigations.ts +++ b/x-pack/plugins/security_solution/public/app/home/home_navigations.ts @@ -30,6 +30,7 @@ import { SecurityPageName, APP_HOST_ISOLATION_EXCEPTIONS_PATH, APP_USERS_PATH, + APP_LANDING_PATH, } from '../../../common/constants'; export const navTabs: SecurityNav = { @@ -40,6 +41,13 @@ export const navTabs: SecurityNav = { disabled: false, urlKey: 'overview', }, + [SecurityPageName.landing]: { + id: SecurityPageName.landing, + name: i18n.GETTING_STARTED, + href: APP_LANDING_PATH, + disabled: false, + urlKey: 'get_started', + }, [SecurityPageName.detectionAndResponse]: { id: SecurityPageName.detectionAndResponse, name: i18n.DETECTION_RESPONSE, diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts index 2e0743de690435..f0ebb711f1f38d 100644 --- a/x-pack/plugins/security_solution/public/app/translations.ts +++ b/x-pack/plugins/security_solution/public/app/translations.ts @@ -22,6 +22,10 @@ export const HOSTS = i18n.translate('xpack.securitySolution.navigation.hosts', { defaultMessage: 'Hosts', }); +export const GETTING_STARTED = i18n.translate('xpack.securitySolution.navigation.gettingStarted', { + defaultMessage: 'Getting started', +}); + export const NETWORK = i18n.translate('xpack.securitySolution.navigation.network', { defaultMessage: 'Network', }); diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_overview.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_overview.tsx index 3f34b857615fea..6a83edd7442def 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_overview.tsx +++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_overview.tsx @@ -6,6 +6,9 @@ */ import { appendSearch } from './helpers'; +import { LANDING_PATH } from '../../../../common/constants'; export const getAppOverviewUrl = (overviewPath: string, search?: string) => `${overviewPath}${appendSearch(search)}`; + +export const getAppLandingUrl = (search?: string) => `${LANDING_PATH}${appendSearch(search)}`; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts index 0a4f12e348eff7..b1903ef869d3d3 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts @@ -47,6 +47,7 @@ export type SecurityNavKey = | SecurityPageName.detectionAndResponse | SecurityPageName.case | SecurityPageName.endpoints + | SecurityPageName.landing | SecurityPageName.policies | SecurityPageName.eventFilters | SecurityPageName.exceptions diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx index a00ea4b6bf5208..601794dd25917d 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx @@ -125,6 +125,16 @@ describe('useSecuritySolutionNavigation', () => { "name": "Overview", "onClick": [Function], }, + Object { + "data-href": "securitySolutionUI/get_started?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-test-subj": "navigation-get_started", + "disabled": false, + "href": "securitySolutionUI/get_started?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "id": "get_started", + "isSelected": false, + "name": "Getting started", + "onClick": [Function], + }, ], "name": "", }, @@ -286,8 +296,7 @@ describe('useSecuritySolutionNavigation', () => { () => useSecuritySolutionNavigation(), { wrapper: TestProviders } ); - - expect(result?.current?.items?.[0].items?.[1].id).toEqual( + expect(result?.current?.items?.[0].items?.[2].id).toEqual( SecurityPageName.detectionAndResponse ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx index 677632d20e718e..14b007be4764de 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx @@ -78,6 +78,7 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record) { name: '', items: [ navTabs[SecurityPageName.overview], + navTabs[SecurityPageName.landing], // Temporary check for detectionAndResponse while page is feature flagged ...(navTabs[SecurityPageName.detectionAndResponse] != null ? [navTabs[SecurityPageName.detectionAndResponse]] diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts b/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts index d8a2db30d4a7ec..3b319b810a66ef 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts @@ -31,6 +31,7 @@ export type UrlStateType = | 'cases' | 'detection_response' | 'exceptions' + | 'get_started' | 'host' | 'users' | 'network' diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts index 559dff64eec4b0..e5ce8e4105cac0 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts @@ -94,6 +94,9 @@ export const replaceQueryStringInLocation = ( export const getUrlType = (pageName: string): UrlStateType => { if (pageName === SecurityPageName.overview) { return 'overview'; + } + if (pageName === SecurityPageName.landing) { + return 'get_started'; } else if (pageName === SecurityPageName.hosts) { return 'host'; } else if (pageName === SecurityPageName.network) { diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx index 86dae3780e1aed..d82189ab1e3bb4 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx @@ -25,6 +25,8 @@ import { Hosts } from './hosts'; import { HostsTabs } from './hosts_tabs'; import { useSourcererDataView } from '../../common/containers/sourcerer'; import { mockCasesContract } from '../../../../cases/public/mocks'; +import { APP_UI_ID, SecurityPageName } from '../../../common/constants'; +import { getAppLandingUrl } from '../../common/components/link_to/redirect_to_overview'; jest.mock('../../common/containers/sourcerer'); @@ -39,7 +41,7 @@ jest.mock('../../common/components/query_bar', () => ({ jest.mock('../../common/components/visualization_actions', () => ({ VisualizationActions: jest.fn(() =>
), })); - +const mockNavigateToApp = jest.fn(); jest.mock('../../common/lib/kibana', () => { const original = jest.requireActual('../../common/lib/kibana'); @@ -48,6 +50,10 @@ jest.mock('../../common/lib/kibana', () => { useKibana: () => ({ services: { ...original.useKibana().services, + application: { + ...original.useKibana().services.application, + navigateToApp: mockNavigateToApp, + }, cases: { ...mockCasesContract(), }, @@ -79,19 +85,25 @@ const mockHistory = { }; const mockUseSourcererDataView = useSourcererDataView as jest.Mock; describe('Hosts - rendering', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); test('it renders the Setup Instructions text when no index is available', async () => { mockUseSourcererDataView.mockReturnValue({ indicesExist: false, }); - const wrapper = mount( + mount( ); - expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true); + expect(mockNavigateToApp).toHaveBeenCalledWith(APP_UI_ID, { + deepLinkId: SecurityPageName.landing, + path: getAppLandingUrl(), + }); }); test('it DOES NOT render the Setup Instructions text when an index is available', async () => { @@ -99,14 +111,14 @@ describe('Hosts - rendering', () => { indicesExist: true, indexPattern: {}, }); - const wrapper = mount( + mount( ); - expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(false); + expect(mockNavigateToApp).not.toHaveBeenCalled(); }); test('it should render tab navigation', async () => { diff --git a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx index 1407bf960843e6..23cd7f707dfe8a 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx @@ -25,6 +25,8 @@ import { inputsActions } from '../../common/store/inputs'; import { Network } from './network'; import { NetworkRoutes } from './navigation'; import { mockCasesContract } from '../../../../cases/public/mocks'; +import { APP_UI_ID, SecurityPageName } from '../../../common/constants'; +import { getAppLandingUrl } from '../../common/components/link_to/redirect_to_overview'; jest.mock('../../common/containers/sourcerer'); @@ -76,6 +78,7 @@ const mockProps = { }; const mockMapVisibility = jest.fn(); +const mockNavigateToApp = jest.fn(); jest.mock('../../common/lib/kibana', () => { const original = jest.requireActual('../../common/lib/kibana'); @@ -90,6 +93,7 @@ jest.mock('../../common/lib/kibana', () => { siem: { crud_alerts: true, read_alerts: true }, maps: mockMapVisibility(), }, + navigateToApp: mockNavigateToApp, }, storage: { get: () => true, @@ -112,20 +116,27 @@ describe('Network page - rendering', () => { beforeAll(() => { mockMapVisibility.mockReturnValue({ show: true }); }); + beforeEach(() => { + jest.clearAllMocks(); + }); test('it renders the Setup Instructions text when no index is available', () => { mockUseSourcererDataView.mockReturnValue({ selectedPatterns: [], indicesExist: false, }); - const wrapper = mount( + mount( ); - expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true); + + expect(mockNavigateToApp).toHaveBeenCalledWith(APP_UI_ID, { + deepLinkId: SecurityPageName.landing, + path: getAppLandingUrl(), + }); }); test('it DOES NOT render the Setup Instructions text when an index is available', async () => { @@ -134,7 +145,7 @@ describe('Network page - rendering', () => { indicesExist: true, indexPattern: {}, }); - const wrapper = mount( + mount( @@ -142,7 +153,7 @@ describe('Network page - rendering', () => { ); await waitFor(() => { - expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(false); + expect(mockNavigateToApp).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/landing_cards/index.tsx b/x-pack/plugins/security_solution/public/overview/components/landing_cards/index.tsx new file mode 100644 index 00000000000000..d8852d86035188 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/landing_cards/index.tsx @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo } from 'react'; +import { + EuiButton, + EuiCard, + EuiFlexGroup, + EuiFlexItem, + EuiImage, + EuiLink, + EuiPageHeader, + EuiToolTip, +} from '@elastic/eui'; +import styled from 'styled-components'; +import * as i18n from './translations'; +import endpointPng from '../../images/endpoint.png'; +import siemPng from '../../images/siem.png'; +import videoSvg from '../../images/video.svg'; +import { ADD_DATA_PATH } from '../../../../common/constants'; +import { useKibana } from '../../../common/lib/kibana'; + +const imgUrls = { + siem: siemPng, + video: videoSvg, + endpoint: endpointPng, +}; + +const StyledEuiCard = styled(EuiCard)` + span.euiTitle { + font-size: 36px; + line-height: 100%; + } +`; +const StyledEuiCardTop = styled(EuiCard)` + span.euiTitle { + font-size: 36px; + line-height: 100%; + } + max-width: 600px; + display: block; + margin: 20px auto 0; +`; +const StyledEuiPageHeader = styled(EuiPageHeader)` + h1 { + font-size: 18px; + } +`; + +const StyledEuiImage = styled(EuiImage)` + img { + display: block; + margin: 0 auto; + } +`; + +const StyledImgEuiCard = styled(EuiCard)` + img { + margin-top: 20px; + max-width: 400px; + } +`; + +const StyledEuiFlexItem = styled(EuiFlexItem)` + background: ${({ theme }) => theme.eui.euiColorLightestShade}; + padding: 20px; + margin: -12px !important; +`; + +const ELASTIC_SECURITY_URL = `elastic.co/security`; + +export const LandingCards = memo(() => { + const { + http: { + basePath: { prepend }, + }, + } = useKibana().services; + + const tooltipContent = ( + + {ELASTIC_SECURITY_URL} + + ); + + const href = useMemo(() => prepend(ADD_DATA_PATH), [prepend]); + return ( + + + + + + + {i18n.SIEM_CTA} + + } + /> + + + + + + + + + + + + + + + + + + + + + + + {i18n.SIEM_CTA} + + } + /> + + + ); +}); +LandingCards.displayName = 'LandingCards'; diff --git a/x-pack/plugins/security_solution/public/overview/components/landing_cards/translations.tsx b/x-pack/plugins/security_solution/public/overview/components/landing_cards/translations.tsx new file mode 100644 index 00000000000000..51da2e72c3bbd0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/landing_cards/translations.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const SIEM_HEADER = i18n.translate( + 'xpack.securitySolution.overview.landingCards.box.siem.header', + { + defaultMessage: 'Elastic Security', + } +); + +export const SIEM_TITLE = i18n.translate( + 'xpack.securitySolution.overview.landingCards.box.siem.title', + { + defaultMessage: 'Security at the speed of Elastic', + } +); +export const SIEM_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.overview.landingCards.box.siem.desc', + { + defaultMessage: + 'Elastic Security equips teams to prevent, detect, and respond to threats at cloud speed and scale — securing business operations with a unified, open platform.', + } +); +export const SIEM_CTA = i18n.translate( + 'xpack.securitySolution.overview.landingCards.box.siem.cta', + { + defaultMessage: 'Add security integrations', + } +); +export const ENDPOINT_TITLE = i18n.translate( + 'xpack.securitySolution.overview.landingCards.box.endpoint.title', + { + defaultMessage: 'Endpoint security at scale', + } +); +export const ENDPOINT_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.overview.landingCards.box.endpoint.desc', + { + defaultMessage: 'Prevent, collect, detect and respond -- all with Elastic Agent.', + } +); + +export const SIEM_CARD_TITLE = i18n.translate( + 'xpack.securitySolution.overview.landingCards.box.siemCard.title', + { + defaultMessage: 'SIEM for the modern SOC', + } +); +export const SIEM_CARD_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.overview.landingCards.box.siemCard.desc', + { + defaultMessage: 'Detect, investigate, and respond to evolving threats', + } +); + +export const UNIFY_TITLE = i18n.translate( + 'xpack.securitySolution.overview.landingCards.box.unify.title', + { + defaultMessage: 'Unify SIEM, endpoint security, and cloud security', + } +); +export const UNIFY_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.overview.landingCards.box.unify.desc', + { + defaultMessage: + 'Elastic Security modernizes security operations — enabling analytics across years of data, automating key processes, and bringing native endpoint security to every host.', + } +); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx index 36ecc3371c0569..db157e9fc71350 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx @@ -6,71 +6,35 @@ */ import React from 'react'; -import { shallow, ShallowWrapper } from 'enzyme'; +import { shallow } from 'enzyme'; import { OverviewEmpty } from '.'; -import { useUserPrivileges } from '../../../common/components/user_privileges'; - -const endpointPackageVersion = '0.19.1'; - -jest.mock('../../../common/lib/kibana'); -jest.mock('../../../management/pages/endpoint_hosts/view/hooks', () => ({ - useIngestUrl: jest - .fn() - .mockReturnValue({ appId: 'ingestAppId', appPath: 'ingestPath', url: 'ingestUrl' }), - useEndpointSelector: jest.fn().mockReturnValue({ endpointPackageVersion }), -})); - -jest.mock('../../../common/components/user_privileges', () => ({ - useUserPrivileges: jest - .fn() - .mockReturnValue({ endpointPrivileges: { loading: false, canAccessFleet: true } }), -})); - -jest.mock('../../../common/hooks/endpoint/use_navigate_to_app_event_handler', () => ({ - useNavigateToAppEventHandler: jest.fn(), -})); - -describe('OverviewEmpty', () => { - describe('When isIngestEnabled = true', () => { - let wrapper: ShallowWrapper; - beforeAll(() => { - wrapper = shallow(); - }); - - afterAll(() => { - (useUserPrivileges as jest.Mock).mockReset(); - }); - - it('render with correct actions ', () => { - expect(wrapper.find('[data-test-subj="empty-page"]').prop('actions')).toEqual({ - elasticAgent: { - category: 'security', - description: - 'Use Elastic Agent to collect security events and protect your endpoints from threats.', - title: 'Add a Security integration', +import { APP_UI_ID, SecurityPageName } from '../../../../common/constants'; +import { getAppLandingUrl } from '../../../common/components/link_to/redirect_to_overview'; + +const mockNavigateToApp = jest.fn(); +jest.mock('../../../common/lib/kibana', () => { + const original = jest.requireActual('../../../common/lib/kibana'); + + return { + ...original, + useKibana: () => ({ + services: { + ...original.useKibana().services, + application: { + ...original.useKibana().services.application, + navigateToApp: mockNavigateToApp, }, - }); - }); - }); - - describe('When isIngestEnabled = false', () => { - let wrapper: ShallowWrapper; - beforeAll(() => { - (useUserPrivileges as jest.Mock).mockReturnValue({ - endpointPrivileges: { loading: false, canAccessFleet: false }, - }); - wrapper = shallow(); - }); + }, + }), + }; +}); - it('render with correct actions ', () => { - expect(wrapper.find('[data-test-subj="empty-page"]').prop('actions')).toEqual({ - elasticAgent: { - category: 'security', - description: - 'Use Elastic Agent to collect security events and protect your endpoints from threats.', - title: 'Add a Security integration', - }, - }); +describe('Redirect to landing page', () => { + it('render with correct actions ', () => { + shallow(); + expect(mockNavigateToApp).toHaveBeenCalledWith(APP_UI_ID, { + deepLinkId: SecurityPageName.landing, + path: getAppLandingUrl(), }); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx index 023d010ec9a9b4..91395aa21486ff 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx @@ -6,39 +6,18 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { useKibana } from '../../../common/lib/kibana'; -import { SOLUTION_NAME } from '../../../../public/common/translations'; - -import { - NoDataPage, - NoDataPageActionsProps, -} from '../../../../../../../src/plugins/kibana_react/public'; +import { APP_UI_ID, SecurityPageName } from '../../../../common/constants'; +import { getAppLandingUrl } from '../../../common/components/link_to/redirect_to_overview'; const OverviewEmptyComponent: React.FC = () => { - const { docLinks } = useKibana().services; - - const agentAction: NoDataPageActionsProps = { - elasticAgent: { - category: 'security', - title: i18n.translate('xpack.securitySolution.pages.emptyPage.beatsCard.title', { - defaultMessage: 'Add a Security integration', - }), - description: i18n.translate('xpack.securitySolution.pages.emptyPage.beatsCard.description', { - defaultMessage: - 'Use Elastic Agent to collect security events and protect your endpoints from threats.', - }), - }, - }; + const { navigateToApp } = useKibana().services.application; - return ( - - ); + navigateToApp(APP_UI_ID, { + deepLinkId: SecurityPageName.landing, + path: getAppLandingUrl(), + }); + return null; }; OverviewEmptyComponent.displayName = 'OverviewEmptyComponent'; diff --git a/x-pack/plugins/security_solution/public/overview/images/endpoint.png b/x-pack/plugins/security_solution/public/overview/images/endpoint.png new file mode 100644 index 0000000000000000000000000000000000000000..073318f891fcf3e828ed3148a0d368c724825001 GIT binary patch literal 86401 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

&|faKRtvI|3faPilcT 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

W058t?hEBrj?ya{= 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 literal 0 HcmV?d00001 diff --git a/x-pack/plugins/security_solution/public/overview/images/siem.png b/x-pack/plugins/security_solution/public/overview/images/siem.png new file mode 100644 index 0000000000000000000000000000000000000000..e5d6bee86a6cf90883065d5b5794b0f261ef2af7 GIT binary patch literal 345549 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Ă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

1g36m(;5aplTVvH7?(eAkO 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­emE;@dU{)dQ(OtbyFhm?$H z!D-iWB+|e&Hm>H)IWp@|&^u;5%R2eXFE@95H^pbVT>8wc=d?NFoEZBJ5#iuvhKhXTg|46 z&i-(C?*YBNy7BqQK7HIdBN=z1XVDAIP5DvM&VgA9nS)jZv7toV2GXqYz03bLr#vn# z&DpF)Gmm;~&)zV8V#0A@#os;=m}pq_*`eB)SSHfmJZA0;XP=%;R2pU|5Y#7J>-k=X zWAOQ-yuK2fGWMoUkFL_$#~6+IM(#>QdYZkurDp?Z_Eyi>*^E&~cmr5lO=^zAVQf$XK&rif3Oy{;*AFm} z`(NJ^Vxecl;RueuLeML~cD}0S0OT8kX%T({Hc2xt023KVNC4CK8Wl8a`dz_CTtJp# zo?@GLDWEuk1{TzES4ORS$Tz48L6=yRpO4b@xx-;{=z3>zAc11*LNj_`TtW{p#(iZx>+T@gs&CRXCaVdWvSCq@ z3-3rL%N3W~E1sY@d|hlWd}*wdDt(AXLiiqEH~C5!a;SBBBGI8-`_fxVaY zlqS8rk^9$rhkuSmGVa#k5?h=)92HopWJTq)ZsX!oZ0(zz^wgG>;I2MF>rB^4fh`g0+kzlk%A|d`?U3-_wYv&cV;h6MDtKB zrgFsN?s*5;^DspVTG8x31<-xrRe@3;d2svoZJmB!$f@F&pF*Vd~-c5uzw5f_p%C81eu>z#Xkp*sQT!PE+M3A3g5>Q|B%Z0Rg@4% z=e4ci^hJ;OQH@0RBv)~c3_8i(>M*CV*wOKg)j82wxJ}!nk|*^X*o%%i_pwoStnf5wJk)t8ABuJo z*o;bMj~eSJjT0S(uUU$Gpgyczc(wCu?4)&jq-64!_tPKO5T>GVtj5cqQ(wuw&Wu$x z!($4Ln&jUbod3O~qsTQMnV2Z~{yRfGcaZF#kcJckEL2v8hJKI~3fU`VuU3g4_e(*k z4Imd_Xw$4c1_HF)Y4l788#eQ3sD_vOR9+ zwb*kr9_*F&r>_I07Rrd@>cmf*1Q*hA!$sLfZGV29k7Ok3DyVA96{W_XITZU^d%3sy zgCnzZTe?M9lOxOG#o1b_Qw!!ja}kQzDfc~(o&H8KcvO*CZib6;-xBNk1Bp)JC}lP^ zijFUMq-p{U*$jnxus=&k2h-^gN71cM|=2M{@km+on@~22`yB z0>Tcju(Gnw$-SzNsHV9})zm(yhpKu^A(qszGore;S8)I8hS8`aoh5&W zg8(Ud4N?y%Y$mI%+1S|f;N;{{jD(z;-_SJ%34ooP@h|7~FWcS1n|W|;J`e;Wz}us> zsgn8N5tb!}%oB#{O`(ry0TQUmVMGbiQA8P!i412OXwVCS^yE3G#NB9Tyrb8J27VpT zoT^_N)}-_HzJ58L%QmZbzD4C-Y3{+S2ixc=>mq*kQ}Y+py%&O07kgxrlH{F|n$bo1 zt4}=c1i9{+k({ni^BbZDygoiIPii+a6N&in=AL;xaXep=2m!95pcwX40y9WHxPufXPB85(t*TGCia@k8J1<6+(?RSm~q z+ggvFg0X-CYNNxaf1NQZG0OM6K5B=ze6>H zYxI68J5tZ8N;~d*q1E2ugo6lzBd(#Mba3JPHRIDL*1C3O!)3Vw{;)5&NE?BbL`Heb zHQ0C%r`-ib=o^s5pVroO+{X=j`mb*IuN&z>>Y_6Z$~smTW!fOAC{ zs8T6Z%ZWmoP+z=wQ3kRP07&|w1|A-9ywal9c=X2hT5_6EfBPjQa7|489vBwU+Z7hp zMYMnY4@ZFpt#+*FwPik8rKdSwqUXZd>$!EBWrnJg0j816-9^-+#YF9T0o!}qU5Qq+ zrFx-sQ&hK~jdo$~TaInH-x4$rI2@7a`NpGpeM&p z606tdx*ac+P(Jmkvw|G8`AmSQ4fVFJZJ~j2I0iH;YW5MA%?V+_WYl~O^y%K2IKB-n~|($JVP{OsG2A_!LoV6FfGA;3@x%&CAc1fR6*@dY>|; zHMzoNbL7(p%J}eH{%`j?mSm||UD)n~&&ls9E!drk>0b;F(# zy$V&-18*@gzr|Sz?GVguwV}U>MAkA<`~{P&{+wyPO! zj5+P+Uaw~0ZgIW;#$tGdWjbE+%~tAnnA>Nn1UStTVc8XH`r2 zorS^YM0#$MQXjP$Iy|r}nQ<(4CX!n!v{>kgy6Lj$Irxm+aFomrcN2(5M1JI}-o1PG zLq%YIld)!n_pdhZ)k_;{4{uTpJ-u|`F~I50i_>s^Pc2LRf7)Dl2%Im(2n0Mi1MC_D zkvtGb$r@~0q1+|EegST7D|6dTOC6*lH@)^x<|i1tCM7W6^59A)8`(yhX8w`hOc`#P zqKXQIHlvU5IHYsxu?P0dw8a`)(Kc5mqg(`unWx?<6-D{0ITP5jq4aTBI z|K(@1a86MByz<+?J~s9;tC4;^MiRruWSjWjtZZ}p76xT|e#7$()T$I7@v6>OG4icw zcWXxfTlByVY=Ev(dOtU!(b3TXu`^_;s+olq6_(lA*<3basGsg}yEK4AfD3!d!Wg9HAY+}uK^`(0FGo}?f= z_AMmB@?F{gC2Sq|kk(f@s_$pA=U9{EtsA^4D}tRV4dVl^!Z-Ufmk#Ci)liS`Gku78 zIZmj%r@6|9okZ&{RGCIzr%gF!eQ(l>($>Y7(#>GvJ-h@?Dy1I#St~uqtAi!#o*AZ39d|v-qo;G z=UTT(PCG2lS%PB3y(;{?ee0KWnM{!sKW54b9#^}1_Z<=Z*~@DP*P^hqNNIULKZ-tv zwyAZg2Ws%-q0J=zX62n#=1573UHgQ{zwZ{td3HRwJ;0P)I4mVd z!4Efdh1Y4xe-NN~FltL5A$LWLLh0}f?EGLIu_)HMBDbL-$n;U?YOtGH3kW^9`_3)Q z9@G6(1Z>Sxy;-{^^yP}6+^%q`AwlD*6KsozK@5ZLfM(p?~UHJmOSM0y~g@k?eDLr&$11Fc(hfKBzmav)-2uY z_V_}4!s=lsER`4c^v8>x;;m04!KSGD2Rc~oi$8G%vAyCYD~o~zueaUc{`uhr{_fA+ zau=RnpYu(7ebZ1dPzAMG@W+KlHi9`2cyRTrymX4T8)#tbNqfRZJZ={$U^^iNYAMQP zFwF^VC-Q;A<7)AhxR~lQJ%=!%#z(GTsu2$vQlbkItgEaK6v!5pO7Bn7V3PrK1onHl zVk+Yk8C+{vmTm=+9^e{8;vbx0O@_EMzx^Nf%$Wqw}SBZoJz0oBX3cm*dGr{iX+T zjrc|MWXNMJYE&xW)1Wkb&6z&sYE^Avg0sQPolFvrX&5%$1r|dvA|U$i?!TZp2f{Ky zpcn|u?YKHU9y9vEsAKBFC8Wmax|qe^xdJv;`k{n`_UA^xrieZT(KcN_ zgdRg(R|JX(kP-uNmd6$XwpD;Px$hCMkZegy_hg7Hag)#IxApTi^KNd(7Y6;0R#bcD zb7ZTZE0trX^6fGT(ZKNZ1$$WCL88Kqdr^jUG)tzJCqk@y6=G(m=T`FSml&CnO8B*- zw%-P+C`mqrT+rUpmFlvuP%?`ph2|(`*w^?!g4g45lGr>*@EvvSi;{oJ5jg!@I8Appx=xt0jfdQSV%DqRg7?sFgDIajvcbCr zw~6m&HU5XoL*_wqxj}VZ`W;kXCaSv6%lUOWxl0X$Sq@mUxpes7v}BJ`cNrSqw+Gn? z;%q$8&K=Z*1SDv6fj?<?skoC*x<{UZ#YI*3e6+(|2gw|Tx+9`*V zJ~gH?D(lbMH=C;67^5Pcn?#V81ZXe(R#H5iTJ&-3wBbz?Y9`xLWQ_UJvNgQPr^_1CxP;WwN`nUdN z6_%(1Sf}%q+zX4vf#-Uu;Q9n9-sgs)G;Bqv7|_6x1{F_{87h>%}+q9p44`H|5}jp|Duh5VvN&ioFr4JE~&5 zctZH=#|eHxjQV~nzjclK*_x61`1#0YdXJ|Hb#w5I$D=_qx`Fv{8nALsgN2NtM9v6MC zDLc{6HlrVp>wn-_aoZ)x6}(F+2%LK*hAx+{p`x)CDt|b0Yo@VWgzcCeQG2c2xEYq- zrq!!bM9=v#z9{))<&t?|z~rr4zr%5!wHdUoderRKu-aM;OB@54@U^{5n=R+r4ZKV%NH>e1_W&zw{S{Nha1#T0mhVEdOz0l z{kn}aN=Q!Uw=MN5H(~~DLsY9bOxr8aLzmQMcr zA_Ahh0F)1FgsbgxDw}pQhjJJxt223w)a-M0YgoD;pG#|HJ$NUAZtKRFPNu{2H(4jU zjFww2=}t=K$=$*9vggW)f=}3pmQY`h^(Rv~Xv+yRw%zbJcXwJ{llXh!petmSc;3@( zc-1Ig@V@c-!M8%_fxwdhx*C{-J3o_f5U_QCoau$$>Bt}!LP2S4`RdhwW3hPMFE)BhrfKFcO-%!zJ$r`uin{MHBelstt zN{6%Jc1lM@OeDq8`@@z3HZCWo8b4?E%~GmwRg+Xdax$0i#)BhmS@7d^PThFn4=TF% zuO0-MwupFG$8X27n_nen&iJ|$c|R$bW10K9YjP$}s#fcb|8U2EOcVuA^-CF@`oq;i zy;1M^lcYq7F?)%1>1ORW2EB`s9_8HAc+FLsQW1AZ6ED~EivwHxjz;==Nr|PFFy`r( zifQICX9<-jEHo2d?9ymGum*M zssD)R4Rp8q=UX18`mivRI%89PdsRQx0d9(`<8C@GLPOpUhk6XO!I-#`r%ifD(FVa4)9sl`Ll7 z0S5tGnl)g7fxwUz2#Cywx&o>>=wpEmnlQX2bKK@Oh{!<`$wwY$W2K|_+G}B$QXO|X zeR(K(*xc$wKg9 zOgr`CqI>GEf|q1h-mO!a-C$i2%up(iH@orUZ0c+zdQ*Y}zv*6n{MpE+So7PgEZQ6L0~pn#IJXvSpPoYRd1=b? z$od7TC?`<)Gp*@LF^=(l4e5*eOuAn5P4<`S z$3nHgi-p~XHhj+5;I+&vE5j-<;BL;)%>|Q_p}YGCRJ}lGH?YUS>Xwwq0MIPfVh8OK z^q4=VwZT)3=vibBd@LsF?-$UF{Es_y>%tfb1v3Yd0U}el*r98cW?ujMNgBWdmmf+D zH<4QMuz>%z=j#B4b0o_1gyeFtvUh$k5O zgRm(nMCrMjGk%)^{q()5sma@#MLeY9v+g=W%V|uc3sH~_mzbD=bFSjajD z7XKkVa+-1ZEkWk3uIpGOpDSM13Cy-dZsc^mP&%{1+E)Rm&488YS1f7ve7llo`p%|yIy*auK4$E<7x@JB95&Jc(eQ~qvq0v3>X>JfU3mkB(I(r6lCx@8 z;-y&(6uUMftp2iOA#?ZZjqXoPh%xm_~?$?dndSeRcb?hQ$LTel9XmBgLt zZj`dr7U=n$rXF@=ft$89)=#qJ`FRTOiRQiURqO`_+N2Ecb|tnj(6|2z+`O9uT0d8U zW+f$yc-_{P)ZVbykH_=DPKXPrU%J@Un$Em5yVn)(09-N_nbcUP3+K&k|Z7_3*e z>&N{ma7e)iSb2GUg`x(uVW_?q#n%yFt{_g4INlQs!5g787%QsgIH$o|S;TiDSBlL@ zF`!%T*ciCKmi8mB%#I4#TKc`AVy&pGNK5g|!#5n&FN4*5`hoF_6YD=6DLL=tqE;hU zBi3cb9iAB{1}8=x{8Xk^pUasPZFNqwqR(nIQTJl;D5l;X%T+30EBj4k^>oXM^j3hu zw{e>rkKZ`s^}6oplQ#GM`tou(8QmOzzCW1zvdf+1{Esjbj!NLss40CXEvOZ*0SZEs zc|h~Ym|n{py=ls+5>>om^4@#-lQnG;EYtA7x9`-VIzmD@m_no*v~Y@;3TnM?AOl&X zk26X|=kHVXFc&zIPCS`#;Q{qOx)!7;)h$!9n0FV5hwfa=s)L1c56AzHnvU zMA4V>*XD4!x!nalkbj{0HUq9Pey%!j-U@mrH$6_4)5C0iPDde$0DglGHVLwU06mXR zP6_}jL~4i0Ky!BRh4@Y2PYVIH)I>fxry+TN(pLRm8xD?mm*DwGR1~be{FI2C=l4R@V2+NUMdC-(sberLD;kA?to5l;Cp@~j! zU1mBo29K^FDR<=J&`FN8dpRXJ59FC{7mtwHp4Z96*)0A2H8x@}tw;rGa6a;T6jer$ zCopFFm-c;rb#*uRasV13aYp=#kwP~~(67e%2r+$^VlWW5|Ag18M%DP_U?q zCBiV7E|X;_C5;ibem?UnVG+D*Pg$Z+L2qVF$khyn_3{)R<<-Fwktavj=7rh)gbh1& zNZS~WQ{2@ZN&n#fTI@X3ZEdEQN`P_KL&BYQ)=vf>=!-T*`4=vJS>vASR|@}qN9||9 z;HULdqqTBb>Dxa#M0IY~^i$FQq5hGL?y9s?zRa|>yTX0o?!Oa|Vdy}O4;4lWgB6=Q zSKV$@0KU?eZ8Lr;cQd$1x$eE%QcY`Nq&qL{7aaYZ0ax!WuF8b`g3`R~Y^IyPywS5C zgu2ghlm)W)(4YFfYs?ZMXq{+yY}R6nOB4C)$2DI|71oiP9c>>@X9nMoQn?lEX_Ij7 z+!u_4RAr06nrK27V@$d1cic{eGByvFEri*0E*BFJrC>6~VkrY4*JZ~yq{0=^d zO{54olBpE1F=erCpD51j^BC!RG_fhy6qxlzMaVShP!!uP@%i6#kyhIT@ym7RQN_r6 ze;Ch)!DQhC|TwW-|fldtF5r|U!u1YotAMI(H0W=3~dHoti-7Gv;rgIl&W zu~PdcdHVGR(uZQlRc3SVQcC1FI%F!$Iy-nMcm%mwKMudV92etttu)u3jsHV6R?ixI z9&vit)Ar|dca6$K!eg_Jb0XRjA1U(o^P))E{2KL=3N}631iPZo^PcAyt3LUD0R~(h z1vm{{*#I2ftKLIOCY-z|NPmAn@l_Gz=B z1@=TO|ChMtmm}YsTkF$2_M8A}1P1{uTbIg#Tp4HJg9_MgK<^F0cYxdy%p5c0gGsJ{ zj+aI#RpUgZvJy8EcFH>a-|x?CEBcOJpJl2Mro7>C`ZcLtBl<0JnZSMae9crD_o=S* zmxmE}(_g6XvVW{S-!T2`vA^p?oZE5m;}M-?b&WTFlJ=qm>A@x*@)|L)kg~3Bu=%|8 z&C`=erkek!X}TfqWhW3&3&UiX2M5CR*7u)`qKPh2 zSB=V-J$&I68|frP?HU&`(p|7?Qfn>;K%T{5k&c@^5t$ z9aGE*r)#&;uiweNC81R>Iv}?t@C!|cXK>!ggBU?I;Fw_6qbL(lnt1L;Sf5hQ5aV~K zE{)}J!s#jRpig`^u7DcHjQVBgOi2g}78OaNV5m;+$LtTmCKnbT3?kdTR<>(Vbl>qZd$cp762||n+_`Pa z`qjh|UBrc3Bmg=!IJ>x*gXj^ZM23f?0(0$~=d#m%1OZS438^Z{zqf23j}+gsC&;ui zt#AC;PQfH}_a3!~%&vsjl0&3INa%a)%UBAbN2S!bIwr^U6i^w<_M?z*lychE2bs?@ z5K*naQu!r%#+$rtG#eXoqm1Bsf|G#f_C`;sEQ{>3+cF-PK_1EWWBNw5U#QME4}M#2 zC+#R02>jX%z-dq4c{H2;y!mZKm~7c7srSVn*MjY7)+h182C8m zkHXY?>7<**{Pp|GNK`|J?nDfM}pU z3}=mHu@)pzT?r!dZ4k1pxj`N_i`!ayD^}99 zF=Lfnncu{ev0qzQ+y$?LX&gI@7hnCuyg`eN_oF5*oZ9@j=HLglz0Q%%typ^@CSIMr zvz5&mQ*kt&XT^j4RmXUyZq@QVg@^Rz8Gop%)mSa6yg$bkM-OxE;?ZJvq7T`!?+)vv}h& zS*iHCTTqVjDMA&irJLdZki z__ju3mMa`V*t7KRAj_Gjsm=OhtI>m<>|1@!%f^mI32EfylC@s9hV685MLwiHpO~?0 zSr2GZb#i7~H!};pal^4+<8vH{4$MSoX}?yWrw0#W`Zo_~^r4A?fk2KQIE|n%_-8;t zYT?F*{>6V++HaO9ASAWmT*K`QWZKlvY9?#e6oH`M;ScIxLOsQlL1Tz~8?bQ07=qNG zf2x2yH5OxgTF;B?NS6r_ByXH|%B#E3Nw`cW(FH48J|On9RK+$=-`C~iJCX2oRu5R> z+Lya~68pOsq*K?n-cL3pa*!c7A zDZ*eCJJC-txSCfHgvrohguctF(|sfGTUlFcB7lP6YKGKDm|yNzPin_0W~NV-tq>eV70sAat)=uo5Hru#1`e%Q$yZP4}07-?34xuoAwqRO{^z38*B8jK2SW{dY(Q)(g zHo(;mP)ZX-fAAfz0{ntVGI%dv!cDZ`Dgx{)_El>sp2l>_*|6^>q0k(6Y?^$rR@cz@ zc+61al}gI}B2%ftd_Vr1~CAM1(K2wTld=sHXCnl{qST9TNIc=^Wlf z;%??ZLGz1SBo-0uDfLzYIUb)9H{N2>woq%G1A+eR9$ytItWS9AixtyhXM%L#Y2GEqeWn2f)QN1KarZ6a6h!n>_P%PlI8Oa z?7=ztwn&3FwJuuh9S%iC6-LMAbdC4;0;F^Q4(n4jCpbhNsX)60?mL_oP6Le>vPGAq z!VXF>Lq$?D8Q;CxO5*(v7v7VdlhQ=SLcK5P_#z=FCYsSGNSF+o(i$UDdxNvm7mUwv zLOcK|Lf#!tq!%X3WWyT|?8iJSso@uoenH1xaf`70m#lZT^aY174ZjUD`Mse=C*}Hf4JY^6N8_P_Yh@uAYnb9} zdo)9DeDp~J@(xlNIyA6vNSxNX5n$-D8$8f(aM|c%x8MjIU$X#1<;5Sp=50w6BN-X# zXa5b4GgR1mmU+egM`jOGV+F|wnUoU{4bh^8QfGH!3qJCltt}NLMa4EhKQtJVMTe;1 zTO(|#DUgB#ASJu@GqX%nXTKp^Tw?Q*F?H5sz!Y>gV8M5$k1t&~Q%)plLqz^<7E2HTDq( zz#__*W~`~^V(^muA(pO|)=MP5XORp!I5=8@dBn!aiH0P`$J^yRBQl`zWX$ z#6&}L^Se(X%smUdFp|Z*m~fQ&-D6wc4bdb}Nw(Kb8~SB7l!hf`E4jQ+-n{xb1Vv03 z1`Sy(M#go&r@Q?Wy82S3cxCwRJ1dWaB)=23?N9rT8hdUGWq9rHLiG_aO=4q~Xyshl z%(g_rPtU70baiD)M^kM|vv9g!T3d&~mc2$m-3p^~r1iY^qj0{iQc#GI8&Q2h3ADwp z4|JYUi`t^?fNd_o&z~`FGdo;p3bJfFJG)w7tt%@l*=%fV{1@H&4!yo4CZ@-5>hTbG zzCnGE@qt83^cht^pOE?cdwz%F3t_Tr5|mT?6JKmY;^Od>bQ7x&-TS88Q0I4UnXvNW zH#YD6o>y8Gl7Zgo_f7@ z@ea+tPsS2H1_-wIBwm@i|MEX(8rUY=M1enh-da(J>18re(@wXWtpyAf)&HlDFY)%f2cMT1a)L0a=fYRLH zHF0rvJ`(Vr6}X~Qva+&@%FFMTmXt6%rO}fp3{M-r=4BQZA%&za_-$)r!)Ws6{M?Iz zf?^AT8DaE9FnkzlZAeJS2i3lg7|yo`$Jk%G81=NZL*v7&Y^CLcT1hRH?j4%Pd)=11 zJIjk+r0hH)Fljr%$4x3SHsipBoSehIpd6HyWjQwHMD;1KPTw~qM4_|0x!I56 z+aR(05c9E+?)Zf1g|t)MRnoV0G()5@>FE`K&#-nXCV$r)wupPiKzJ7%ITvcQdrc2-)0EX)zua8 zNfk!>L681qIjhd-9*4KXk@Lr*JQ8!)?1c7*L5+(-hyO_q`_w8%)JxiQit&jAT%b-U zCQ4hhJEfB_cV;(9Uvkxt3W`urg>3AdqTLF??`oQTy{TZTQcX|QF($Nh>cUDIzwDVl z6xXI4xB2W`XIo=#W6NMBDZwq=7|-d%QCNi7O*3&fT7|^6u!mn#JH?G-RgXIBbm3x> zC!GLE`1*}UWUY<{e&oB*I+{1X&n=ow)c^93XD-?H0 z9DAmdU%vAo>p=k2;q6vu10zacinPm zgJIxe`TUoLin>6vAZjs~64|q6usPRKQQ`>4gX?XPmBq?~A$2|6iEAc8j(R*@=Hsv zbk8YTy19dG>%tUbj3P7XVGXiaeKX9m%Bue$X**HC57O)1xuiw$XBG=?k7?+*1Pv%E!g2eoQqLw< zNdq0+R+2Q2^tnXUA2ROdQKw+hZk%Q2xS3%Ph&-^V&@Wlug`ogk>h4E9p@p%bH(cx$ zIzOdTf4OGFOE%|F9%zE*d%J}itzfv*uLl!RNr?5t8x(Ed_(lG7SK5xI|J;LFJis&< z{+fqKfT^xnqDP+TL}0b|S;-8y*kV^PKVelAQ*jrXW@;8^gtc4W!R3iK@U{c_=fed3R~^Bdffxw*N?0~WvaxxUTM8+)Gb4;JO+J!olZ0kw}* z^6ziuQd(5wBiB-($~>98t!biGsW9p>@H^Ux~uMoz@xl zl9~Efh`wt=tv;IJ#i81I@ZBXzc=LKVJQUN*Q3*2k2hFvMpuhR@W~qq^VU3 z3VPz$RVW0W`XA5zq3*3LEJQ<=oB&Y6J1Zz6(zvrzAR;1?0v1wx2M1JS&5a7yD_1LL zrYVHP{>*6qc^p^Yg#MSr;S|v!>QyxH1VM8(|L{YMQtsnC8h_VYf$mkfe_Zj$IyMfz zGkqiEB;VXkjp&cXCmMc?F)TZ^ysI(QyxsB|-&(A>+nHbxqFE(kVON{0QEXf?&9Lvk9*)pAYHs^w~p1CFhxO;jsVaDXK5N zFQ{eg931FS^o@=0u$1QK*Cn~|`0Nin9~v1^4KT*2gP=Z&3{1b*m`Hkh`b%f$=$@XQw{$j2 zq-k}n#PphpQk`evFg8+*{m|gp&aMo<(VdjsUlM;_zUo<@Sgd#U$@&!hBB19%Rk`(U z=kJ8|hUO-GVXKy_n=3+f;;u+wwccP24VUDzo7cGC|3UFIKlLF(6QZH@kN^5rx^Vch zT2)?NZNlPqef=A#wN~aN;A~gpi17k<)o^h9e7zkBA<8R_M8Ad>&`;aA8fe=-`6Z2w))W{eROt40WmOyqH(lC#J+ky z^zw4teBv)FyG$SA;ZfhbInA>7^q8x>dWGt`IjtSs<$$En$2L4el7_xWJ;9mBH||=z z`e~ljV!3VeW9}|HS)IDRqu!G|ZXLR8*_&AfS?zssaZCi##`pvT0wIlR=O1Q+S9Q8h zA106|3=R&?*X8>f_5Gg~pzigKQZszxkPwNAg*P)gHWoS=aNmV1FdVD<+nQaX{<(o( z?%?a*7jBTfD=aJwy6=`k(;hQp<{r*h|6({J=X6EMn5q%q*_d!9yL8hjr zgp}wtlI z5ejPR{`U5FHE{62d`dXam;gjwL!dAYhQLdRDgYT$rf#mGqa)Ejkh(hWUbRs|hewSY zFEKnVmPD2CSgQRS8;X?e7W~Cy*mRzDb_n9>>WYPs;$SfNLlgyu2_r3f*3;(=0)fA? z^}`~JV)?9&Z`!g`m$x|Vq>QxK9y!~&#`HD}kCkZeSeB0V!zBjSn8srVBobHRbIOq9 zaL(7OXSzXcd)uMg^CG9$&?tjWbVz zuibmgk9ulpDM(3431SD2fZ~BF$fU3h**acK?C!jhPKy9h3GZ5*-jT7=o>q<6p zdVM>gKMd703-SBfQ|3KJx9|P*X`U}^bI}%V2|KjkDG1wLI_^R~Ct=o72>f+JuUUBB zwvw>SRUgl%pCzd^FnM&pY*U_q7ZwGFn-?S+7sow5?_m=`VL}3Do7bZ7WF4$1utaEA z3Ez<0h>gHngXd5dF3LmL4s(9NK|*V97%XFp8G6tsEuzHFp+^_T3- zF4&F6$R?X9YUzz^7V(F-^9JLzd8>`~x4(~kt~J^`uaF$&9L|iRF4guyI&#PM zFVXE(^CgwRC$mC|{K+RPI$^o?=e!(=iq*3tLN=A*k%P=Sa;qMZ8-m{bW}&NvPl`7B z#Q9H>kCZKUnaR@H7;+MdJZ4=J)pX6RZ^v+kJsYX2*i+C-c=9Tg#qvNxE4IkfR=B!( zzo*QT|Mm4i{ zve^EgHR@OU*Te7P1L^~CHRCi=wZlmZ2AGhrurRs)n3xz81n#7n89IQZJ8=TGM{8@L ziwg@76!|?c@r@97Cg1GzORop9L{o+`jRJ&XG$oW2Hn?7OF%*Z9_@1MVD0g)&;D+Jt zYd$GcOp|Xocf8zKP23=@r3lwMXN|Jw>$LxwaA~7m;j@u=x_D7F=_9#{Plflwh2+=5 zbJlkWv9da&3pc_Zy!or&ON7{Yd9i~P8hkyQ>+@X6HL%JYT(n z+)>Gdgap-c?S+R)weZvI936dzB=z;p&0QESunGTx**t1JAO`^MQ92|CqJvGocw46t z62fMoDtf=9|4ZEihTbyBx;p?u4M|I4f~-&mvq$*H1LFT4bNh;T`}@ z8=`M&DhnQ(-nu%Jt8KNg(2RzfyaOeY3RQdcW{65`=77a?nZ12kbmXswZvL>JvI)i6wh5d|{E(`ssJlmW_vS|2iiSQ_?P^Rl-9(*+HHXZMP` zg8KQp)2p2H<>er2)+fJ|Mh{-38%E1_WbI_!hZ^E-j;K4;ogFBrM6J@8cIqlo^F=*(oz*mKCx?W$(z!%%(+FcJ^LLlD$_Vd&?$!CE5GF z|L5KF|9pR5&nrE~-F08r`+dI8^Ei*=IGOAOt^xh@0%B=vOE_gGRPFBQ_zl4j?vT@^ zZffxAdss4tgM88C+|p&(Ak5k*p?6~HpD`OhgicA_=fK~o*Y5~>DanV}F3pzmnqRla z(P%L}d*Lzvx9FvP`t{7Eno^1aBBBHQsS;&$wOR2u!LU-@=okOa@f$0>27}T^U)KMY zKCKM3bL^$BH4o`vBk1GpUR|{U#xc0YWDKqz!c;*xAkZC!rxyVYgeJl^BqqWV5^{wj zJE>ys*vQJB$27FLar_|KU^4)3mIb-!dk(S2RR({+LD&$5U%p&MboBLG_V>%}zGeaE zAKEs&2z)n+Z|)0F4dUpil;P$o3>Q^ZQQhXu3WNLmQN;N((5Aao$Hfiw^|RqCkYsbx zVRid~fI~UmSnv}hKT*;W8t!TW(di=#s+~gLgIldLK7SImIR}xBNtr19cg9RQoBMb4 z&T?dCvpYDm44*&gOrs*nb8Q>}XQFu)U?S%y=pHz$b^z`(;R3;yA zd>FGsTJttf7EbE%3g3F1T<}vOUUcA@=gK~-@6I``Pn+I`-Lm6KA*8>Y{`>Ei)Pahq zwSK0{Sj(vmR+Ww5wpt>N;?rASsw=BkO|8;HylzpnXgEDTq~uvmy4~Y6Mm0f0N9Hwi zfq(vR@BDFkKrsn+ypqLSENfSYPeWKqaAKcy)tp!~EhfRj;@tf{tV~fjt(UWk- zZS5K7Jiwh0>?Q_m4p^&v4f_a-H_1Fi=`*_5Wc^-km;NN|f_$NSs|qkvh*wPI%|3sF zZQ-swx9=k?m63t3anMvu*n%4@90He+eua;q`odVf?aYCkV=sknulZ_su`&Tu9$R>3M~D|aSw=qsPf)yzZM7@^P+ zb-u5Q{1c}0#(K2&!3qo3{+hFn+IlKJiP|BGt%MUGa)Br5W5! zDb&34k>DP0KoL(91GA~Q%0&)yH=c(|56?U&%BCy#8;EO(kF89-Bde28qG$9!*7f($ zwr-w}%$4D_wB({%g5rzw| zvA}KsKp{PL^5;H2Ju_(30XP^$02p|N*@x({)Uzitn~C=I5zy;iajls<+F3@y%<5f| z!&Oz^AbQl_fWTwxvTO0H@ZfeEEZi5*PMhgb&n+XY@=YT>Me0$<*2gengbIn?8t=+n z6SP9@=1OWEuzitE*SqzwW@-tNhy;UruXf4PSz@hv_OM&r)S>EPX!xm?!Cch>bD!hE zR4FR7OLN(Fq_*1Z?YKL$-8BB&M&ha49=OQ0X%aDs%t6Y^ku9PFTtOHnCSUj73^4)z z9O!req$A~Izsb!#XvD*C1qY2Ov-&0{X|VjFHGoDxI52SeY!+SPo6w0Deep@1%6g7( zlEX@ov?A4+wZUZC;7$>b(D}>Qukg}lWwFld@*tGeMBH<4Q$OG`aHbw6OFi=aB(o?m z>h^UwFP2y^Tw#1G-|Et)T>Zz|^J{d?JUSdANnN>Xij-`jUdZKbg`sTjuVo@iRm^LO zv)qgn)PzhMrp(Q0*$0U&FsjU&zpd4lbr0(a6S~g=yD6BYo$sEhr5Dw9tcDcd{IG)hNO>Z*p;Dj}y1I#R=0|%I-X-Tx~yPDs``F#Ht zm-o1zx;|ooMa9DJ^*L)>^M|Y13ksU1YwYa(?`7_&l|5@m zg%#t+qe%yQ1no~RNf~Sq4yV30-7{Zt|GjINJG*PRe71jVPui*TswKt zSGSor0(IT%{Rqk6%jCrgEh~W+WP?GZ>CqE-M`^#o~;p~nksQ+d3hNH<&M6d-rs}z199~4 z-(5g=I2CQ!5q&*_+L*aIbN1&25SHIIR96Q<)d>3sk9G}UAn1M)2_cxO#Guyg9+I{l zXPzqV*}C@PS{?M)(u_MrgNU^Z`o9iaFZu5OrdqGf-AWr2>U%RwL$bAhks9+Q(Wwr(0w!5Q~d(c+z4*R2L_`Cr>v-@xD{HY@XMs%X#hB-=2G| zWeNV4&>z8&>u#LC3dhjy-iTo^Ai!L=Zp}fO3FXpJJ$Fw}&qtjCDMb#OfNiYycGgeM zUAo$d4ja7R4$fA>i&c6rqzURbc{?@b=B|FtR`!pM@p^WA@@v-4?a|)U+aB*y7tK^j z^4_T3lRj2+%l-%bGe?Sa06%J9Sj$t*(cPfdo8HZQE6$^H-!53eYIOWgP^+z*({bwi zFjaIEpX$#hX%X@QtvgnIRDwDL=${ckKh(TmRzH@+XKk0f6*h=ga6lgy_F_?~8riYZ zQ$_#i#(LJBYw*3{PQyLlU%JbF)wZ%qkJEEiJUcvb~D%8?lcUdUo=H| zDCIBjDRBgA@CqHxe&XOs{xKldGCboT==YNTTC`#PF?GWk{aHZ6`iX>l%8x2N743Yj z`uh4MjXaq;16Az1d76>XFqae&$>44{)i5wLe3+*>2Z>BdLP7=@=-j#*305`jygwBN+r(CLO~ddbaxaixbSI`FbrYA*^Yp$sJYxgmA!o$CutHj??BPnF)7Q)w;O)zf~A&UnRdK>T(m>q z@+@0jQ023yQcLZ<_1H(P2V~rjzJ+@C30|z>5ayuj>St?d`fJ22c0Eo1hN~3a>uM8w z`C-SHPsexhxcQirjt2sy>v3OWIX)pCG?H%*9|#+ddc5U-iZgGxPICL_w|Wcng{3yG zi0Jfb1Jj2YKC&Kpx`!c?ORlUsl+Ayp=64P4`+nr19fZ_PCON4daHLD%1eGR6458m$ zq&CSEmgG7vTrbr^&p++0>GP$uDjH}J5&cbCxLUYYB40^fan!UYneHfk(7m2^1h2JM zm4_bZw_rMZ)Eg4Fw6QCxo%bRg{fRa{M5yQ~A4%tf(Am6<03YITyd`8O7^klA&QRvW zdc(CSxn*B2eS&?@L;RDDOgUAo z?FeV#G(9W*n3BI!RT32PK!^Ye0IPr*4`m;Pr4{MowM>?lOka=q z#W{>oOs+>AUH2=H=kCt6T-u^;*rGo5Y&h`5_+B6Yubxjr6%7WM*I}|!xZ4~j=x71!<}owxpYH5}E>oG)qo;rI8@9Ezy*m34Y|C}ik1$yw zI4db~ozUBXLH_Q|@5*;bIi^W`>Z2dO4Y(u{G{DS{!y?%*NTGj>eXoCTU26|($;FYX zho)0n@x4UB+*hA?x3v8878+OYO>l4!+iqpOmbW!{CCg>S_sB~zufo_(oOaA<2wNj+ z@Of&BBWr=Lv-_uV#|$p1qm_Bqak{&BSThH=r`yxI)!oZJ3ZyT{k@qPM%IaGzF{$E_ zhORcog$GJRBo5`@D^p($5=je-C!5!>9Z0hmz`{$vnknDLI7ZwoHsSVeEcAzaX`srA z=GQi2!Fw+q9=8uB)ej3_ml7zC*4sNyM)`^z7tS>$9NXI^48`HKex06a{EGTPfX~R_ zEjjt<;jWEQp8aU}N<(91!7%AG!$rZR9K9&V!9C79+L=$IuF;d@u~o)SGUV7^we`*> zxaeU>dFhm~s?{+h)mPDu{C#fPtvg)A!zeY)Ukl=OhS6L@B^Q=>{|Z+@?s^2^f;J{vt8T7FhXU#5qDMAW<0Y|i985)!0-Bh&r* zPhLj6dhYA>rpd#LW^2BaZn?0VW@1oLYEi`2(l%~&(abzzsrjf&UqW5NB}hfjd=HLO z!*##99*AM}CW<-hC)CPG4Jxyo9BMlf!W7<%0U_E5Z%9Mrqm z2xeYtdvkNMW;u*Ebg9u7Ec}rUx~Bqd>JHCG*D5#;>eC2T4)hDxQlZ&ped2PieA@um zgxG@NeM4|e9RKSKTJ!SY}izVZMU$3O1`FXR7#ZA~)*&w2IJK`Se0-z~Pa-=hfV;{1MW&Na^O%p8ErCxyRnMsP!Z4Yf|h~EpiIU zvHiz|BQZ9?d~K_zS!F`rsN5B*%Uyx(C?ksn$7wb6%0@%?>E`atDtcl?(&_jy-5vDP z$a$~FKm0?>V4=Q7Q5S1)<944^ylhuh%#ktRPn`~9z4_;eDXRUQM&27Q3GSVvS_Y;! zbOj_$cMm-4=&=62dk3(|H*SMk62wd*^#>F_26Y~96BDUXze{@gz)}m6n9Jxj05Z3J zkMSREFFv?20g&lkVW31~s&ozky}{L0jy(@?>(>zWA&&_V(a(L#6x#_TkojEI)QpjJ zhinbfj>jUOlJHv$brM_eZ5h{4bWmCyxQKZllXDx^mk#g`{;Tzwj_7D?zG8YRfq^axxB_y~bsH#@>yyzyj()g0NlfxHuIhBrcbC8{0F;zlW z%bO}heWQ4BaCNOV(N^fkYlDhEN6*UDr%Y9@X?LIhOf;yL$4#>*knFGg-a<0r^hVW+ zV`|on&FMHnXZ7T+r^U?rnU$`YN@PxjR^gzWu8`u4YowOBQgYwPIIHNHK?38rpoDnu zgWcJW4yIAw30oqGgalc~qZ*4zC|}Xt8NpkvwhXqXoV^T*sC7QCeGW6Pm+ zj#P+0Enlnx``kUQS827+mcJS|?%;Sy-f^{^_czHW!)^||=9}~5N#pBf_XvVG-rXe4 zVw|~ZCA!FYh7ZXxLgH*rrz}BhWNKZs=ecd8jm>LO7rf+L~eX$zu=}V+}d?>Z^yQS+bmM7Lc&sq6{=SS+GZ!X$C8?944cblE5Z0pnb z=U(*P*5Ct&Ko`rE+p5x~kQE7?jr^QyZ&qXT^q+V>Qf#%}!>&vcfGZ!-xyVCFz#}BY zhJm7hYGGG#<3V<$5I_Gs*p)fg==ZNWGb3J`o-@tRj|F3Wpl<6yQeQC<1C$joyn!#( zx@}1TTgeVO+IgCX3kx{F=~MFskOZp{_Q%ZRq*~3C1;Uw(z^nxst7MgcJ;$|_jNG&t z`!XGnSRO7d$t9(vINpjS0|SN_MRR$BPc+Di3$(#@J;Wu1g&B~*>fPcX`oWL+E7Oyc zm{)1o69Iak=KhKS=t&OMZPmBIqvsF+X6b4!tuu($QZbUk!oR`|?J6dqZ=MD_@mB2h zmK%sWaBZl`f&fOWMu-AwsizBUkPX6Np$fD(4@v?6I_1pwMmxy; z>%VRo(%q0CxJ4^*Ri=}5uaU^K?V~5aD6~*t0-sJMP7dI2=4@t09C}(>deZ*WGA(l#OJWAs>3<}~3)xQ2T+KXg z$}uh9EfrkbRl5Gm)@ZGZoz9LQS590~ZZR>d&dO_Tl~p_!O`nU(wY5~HliSMqa(}gf z4%@WygL5X~5L?-^*M*0SgMVLJE+{Fe7X434cIyLo3$3*r1NK`K-}HOXr||iYDA`Kj7Ftq+>K+Gd5e$V(7ND z+?CATKBo~*HkX3E;A?};%`7zMNvVrM(ze36?EQs!_I}z_&3$uo9ZxK5N3PN4qVa{# zR6FkFAM_mCXNnHdzdMrRGQ9Hal8g$PULGBFWXqRCkoog_;Qoq;(Mmw5+;jFBgssrv ztN^{=fD%OmkfDG`khrz^fRJ(0M$jF`kz%H$ovP$Er+V*Gy_3go%uV&_C^V?c;7KV4 zo>PW)emjtoB*evKVRo!fgXC@{Kp{ZJRcH`ICmapgGvKMGqqVIpPzr0xHIf&tYS(H* zGOaRs+v^-Q1_qT3c0Iws9f8I<^4;H^fu{#pIX>G9_|PsH=H6SFJXZvI;8Ng?cJ;cKCZN1r`5TZ7XM5Ux}5WK4|@_UdNX?| z)K&DXVz1D^=cDgjGACZ2NfgnKv=E`uBDSQlt7U;@doc#FW4JpaTm*b>>1zAZXKCY? z4cLx_J_{9%5e~NRHjZ>smnu-EKXs;Kr{a|N=Y1X1&m0->;e}=@DWT{7#P+)L?y0l@ zA?o3v^qA7N*X>QV_Hp;yy(#Z%Lj284KaepAuPD-Is?)9D(gr(7=TetH>DOHKIGts@ z#%im4-=%)Y?K)$|mb6UK7T23-zI4@Ug2A5%rofbZgIX^h5l=I{yk8bIi2jj{E_W1} z5)hGg#y4u`I96@nr0n9JDcwJgbQoTpU(An1ug;upq7r@MdY4&9-b6v#>vJnM)3NF| zSp>9f#6(3qot&IdV`Ee>00<4;&Tz;Jc0m^n!vZ7|yER~at*=`jwNMxyLiQmbF5V3? z5I{~oOKGxScf#1V?w)wp*VZzN>#Gi0A$}0} zKPDtZu>~ldtVC)oxJ7(>jg#KN{;~KI?XjU23VIK7&2kThv&Z{w1i@$E4av7h<}OcG z&5zF{@^^bB(Mi2MJ^rmRbN3s59vzlVRtiyO>(FVbe1a6tn?JKz914Y-(2l@B!g{2D)gu8nRkQkWH{ zExb#NN3s2FR^ndk?Q?1WG1dW!gYw$M*OwkxZH~YYf_@2taZ$i9gC1#Xp90YfsLDi8 zvcv79ri!^@L+$PqMXD@CrsQaq2%ZP(8)aL`Kd=d2Pmsb%(+5F?u?nks+aGm+kUd134x-zP z?h^2S@{OK}L%(vjbITMgB{jR7jB7tHs>o)W8+!P>$xPzPPwDx4iT1-5T0)<21WK!Z z@=WtYl&goW2e`Iy(lHB|O2?5DrAfFEFJAt8^_JX_Y+I^boB9PE4pP6l(2BX_=BZC2 z8xkG)`YYR^QlBi-wdN#nx_YCoG)Vh@^?R%tFfP0W}7c_7t{e^DQtR3oN|c-Fn&)d}Vln|L%yoxrll?&2AE}FV>Z*zfxJ% zZ2QrgBU_u5;=ExxRLpxVtG-^)$T?eSSUZuoNI=~%k5W$+KJ&FX4wDZMsJ~)792>az zwz?(ZF=aSsD#7}Q1=euS-*9Ut7Y3)(Nx$i-$fmQDnZb0L@8d_P6oytE#FxrK^#g`J~41mQ8E~!J0z}p_gXNj?Z8_ z)MoiK^m2Zep}Z!&sRMq}Ng$!#TOR=vQPA80$}@X>oS>PCeOEAX5w;gNvAP{?v%r>H zbFGPBJODx+vI0K=JTTzve5{5o06P-25j_?q{oz&h^{i(?Bvf8h2c(B!PFug|e4p`vnt;G75P$js(BqVD<(p}xmxm>Ab?Q8!2--GcxlD#SCXiC$sCy38;!+~+S@r& zXc*jQ)IDWK0qrHg^1!$yH)I0#2>i;VT}7VWtxV8~XKT#b*B{{ms|_y6JhPt!YVlfN zBXYKG1K?!@YMa|B2&o&hbvYZ+hF+ErF{v94Wu2$fbGHhfABm_$5Ms0d8y6VsmH?f? zCpUY0RU{2~>kxRb5H>b8Y-DL^$3E)N_)YSIF*NI^+9rJSy+3g zB^9&z2b+`1qR7YXSpB)<$7R{dyth~6ICmP`M*T>aLr321^9m5CL@V!y8u)t6)Kcuw zg%R(G^5Wj~z8xe~vL5MuFG8Kg_B77)o?&s~nM``jZNZ`JMcO4_N)@z=`<=6}!ekVi z`JU~FZ;WIYK9dZia@;(xjqc#bdb(}gr~ceju|nRb+ba~lC%z|!D3l}s%1ScO9%uqw zgW}?OAPjxc&abShx>xi#8qBXON}ez=#X}-|dYUA0v_N);k^uTEaw*NgtIUoDku#BB zcHC&Za)-+UOqmzfFwN8!O?6F^L@1Hr|hJ&VYWRd`Nhuz z0Jf|yHXP1f$|LydAwoh!@NpzP?NH4Nh$Ip1W?7#M?+ssqIX{7_Dk^OUXCH6E)pT`t zw;Q6iWAW?B3l|-!z?%TLva$^`PlxGRq~S19V!aMTTIgi!amF0N79pnxgvP+Yzyst9 z_0Q&wct~KG#U&@lKqU(-DedwJTOct2bJWqo0vEt`Qg8FapYxKc%E}IuPEX*wusI!# zjTt-O6l?~P<)M@BpI!x|vbJ_Xr+_skwm2iRHU=_!cW_pjbvIpdOa6tA=AlgCprM&2 z6Ly`r@T!QSJB5?S-HneECRqckQgK#6Q2}}G<)viY=z-;@Nnnl$+vz2(@!OFD$t}KL>WZ z-LBoFeKr;@&=c2+6XlbX)4M76Aml{)cGUD22d|Zlp&hw9p~bEq)ieR_x6MsZOqvk&DpuLr>`js%?L?b8InEkDhhn_T+oBs)Z$< z0mD5<%a4-ep|dEVRkroMwV;Hj9sCab$Kp28)L(k-YWWG1F-b_`hVwDKy2~-TXV+RVH@iqu5dJ>;!^%Gdf7cEZ(Q^%GPu~N2DCj^|yD!$;P+3Wm z>T_&m_|^&U!2<@oC!md-hXn1;~dcg5CwvzUWJ=|klJvk&phPi6&ELDA*-_iC75D1)84)X zAJu?)R@x3f>K=Es7;d4;sc>p=CpRC@X@Sz(XJp@rT z#rm3>b!d#ALx_lpt&ENDu{RfQp3DLIr`dMNf{19b6OVi+UYE+Zo3NUn=072yAb~Bq z^r*u^t(+=&Fr?EW;ZK3-4D*Fa=DLtM!u7AbI%isobD^0A6057^o-r&|@znEXoqNuS z5$*N(6*H;>ggZOoG8uxaiH~9oXDo?Fj1PhyHgpRTdU_HbypD`mb;z^d-6*v&zrXmP zyy>S(ojzpQOMiufegv$_=615(64GOh?-eln=&3Lrz{_=?+Hk*|c;pV(L)?gW+haV> z2qt#9yE_Md3MPJGcV|f@9H+eaqhU-g`Ta%(20!p{PtL)7eldtPqn5klmshXS+VOse zL)Dyv#jCyY&+gWC?96E_dVU7&iGBn=R*X1LOUAGefCCQed#~9Z?XC{qtC`y8iw{W! zdn6=9!h7w&LmZogN))oPC)xOnS$xl-8+bjoQ&s@rH#liw+SuN0@STy#99dVt6&t*= zGIEhqnJU$(iUw9H6z<=&!#>SqVV=AuXJ}V!=i+Bu1kq}^*`PS$`+(!^v)r+(`na6F zuqF%&?+2+SUnmLr^uWG`GxvIG@6Pb>6`)JGr{sn-foGlilNcf#x}o7`TAYOoHi9=K zPk!^~5$#zECT^e1pe3Mazvq5~{=XJXi%I_l6Jm3QeGOcamWN9`wfUfk6J}0DX#zr- zL4sh^KOhA{7{q9!h8jTYG7josz?|hd0^{2cyzj|yWs^hflOlFcpD&j)0m|(^o|jT( z=!@d`9~Lq{B@YN5mMJ^cGScLbD$O>hhO>r>ZL#vL#^kI~?s2%(D)oi3G*i2IKeV$8 zv2A^C=40tHx*(6A5 zwbHPmA@;}=&}M1T$>=@bV|pL%sCELc8KCW1jcvHXQ#F@Jy?_9W#8U#PTd^8@8^{G9 z8o;>(2ccWaeb|$FTvxO0RzTqknwOZOq0>u7ZNj2f)!ok%Iu}QG#Hy>R0y|b+63&c2 z^dBkSFwN)R(CJS|NdaPIthAinUIh&B+&saT@M|mrV}N7$>bD)S0m9QDyTXx7fy^3% zR4cqkbc@HGNX&hzrI?pT+uN7nVAWcAA8wzLni{*|s`~+GBhDf~U^s#f9?*62&z&H@ zptSI)OR*QtQWfI7F%5M7CdMR2;E)0@w5jy6SYr!_8_FhZ;o)Qc1}n%NA))JFgFZ42 z0)#<~_7d2u;7rH4AX}w(Rnm8>Q>ZHBZeBR;r%^$78Kuy8GYqGUhKBC46|T8%{q9Ut z&I9+Al~yzH!tq_+3rm-FNSKLS%^2=A8OzW1gkqVRKJCe6<>tI8p#D`th)pP<@hhp+ z0lCt2uuvwie)9X!9}BF4?fD;U{${PLriu~UcLfmsZFY~^P#O5@ZBf|C?yVPB+;=o0 zbQYK+C)T=0yz^|HR^3An{ajaRjEEXPa)Yt1WN>Zdf~;Om_J&156^eh+etmFZc~arY z*nrYm-$YJ@&>f3GvpAah1WN*+`R(SjL>ga(=EPg#YLO{ZBr5>V`*N}8qAd~z(Usmj zf9lSi36a~q;|_g-`|YrUfFxWvSSsK1*9E$chSNnM^spq%gl|H7!?|xPkI(VumqB3w z#+k5K?^fqu2CVk>NuzsM5)iN-r+!Nz| z7QT4`2{%3-(bU6LC<-7a@|syTDZ(>n*ESbTCQ{6f)HE0G&D6W~1F{hHJBf@xcf>$t z$owYULnn9er2)uMg55TpMM&-8w~Il9U;ZR)PQ6SyH~%@%dSliPPRu$K?=82;i2{jr z&F?&C>T!Ws>RS@NkciMGqee(k;t~=fJ4AKdB?A0kjBOr3eQXK|(7uN_upS^EH*bsW zGUJ15Cxv5BgDQrbo4abvh7VbhGar0#hi- z=cK+D{4-)T02+y@(j)5P0&(SAUl>&5SU3{(&p%^iy&bqF5OyHdPDn_Y1B;saO{a

sPYFb%?qj9u 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¤`WifSX_us`U3IaG}o&x zJm6$tlu~AJV0RyER(3051)_}p$8}XJBvgh{$>ezA1(x|0=Z#&(k z3ea#BzO{BL@4yiDVPo*#x{7$v`5S=&YpZb@baz6%Wj+hIta0P)9@}<|& zV(XI|9}c;sm%l?Hm%z=jQ|GW$mv*)H%gH|?G7J(p=!+lZJxU=Fh?BbQ{KvpVHb$_KR9F?pb3wN73JzB@k)opm|4DGul;J}(;KX(8Rys7w`N*Nn1yJe5#ON(dbZQyeNb+zE0 z9&21C3}Ic<2^&f&uj^78Bk(uB4_p!BXD^B397VrK2dC3MVHs3W;^%oq+xgrKxn`7_ z^@nvutGlbrul9m)FonCIUr*0w1t{{=&sx{9f3zwQ{iw=M6_;T$1|QRY?mg>u?Hd;Y zXm~M~bx4c1Rqw?=%9ydAKFXT_M0{&mk~xURqven6~tfQN?%fl9Wgsqk#ASi`N& zNq^2q^1-u7dDIz4ZecAxLsK6`9<`Z>>hGjnfp}S^Yni4;8RU*xbS?2E$?FR8ko0hr z(RzHG2IlgPUGg)s@Rr%P%p59*glG0UWC8+_TxPS6uXFhJ`Rv0Sk8nsD+p0AVT1u1j5Yjw zdK7^!y7=vzC;3NB!oib%yf%dUnQCN?cIN?RDg%+fVqk7^6^k|MtC zt!iI!okP`}?br@mKNKmtR#w5FvD4_!3C1=MD^?!1+;w?3r%#UgyV{z9@T?T$_e2pX zva!V`(R0+I+e9IFLCAK0ZA4v;^WYpr%4a*j9Hq$e!U(Lwx&ld=A8{URs}`Ki$KV_G4l)_I(fle?!7g5 zGs0%yr~c{bqz$3UOW+G0nnXDf)b2IMJRZ~y(X^QKo!)Oa*qfEtKPusnq{L$u#+|*u zi%EX{_S2Ed=gc}DClrpWySnU>JEidO2k8NaE-P^g1N$_ZV;l7$IMzH@voFOzc00VJ zbFg`B#^0Og^DS55M0L;p)%Kvujg#~cJt(1KcnZ8_At71wD_9DpRX7Cy?83^4u8PKIUHm#f}yf#{*xRX?zGSE$?Yx!ekO&eqOcW zCG}lwn&h4<_Hc1(j?;uLwD^Q7i#hDy?Ou7@MKEJcVt(2It>5Bu)m7sMB96aGp$R;g zq5Iv9DKFKgZ}9j6?42kKq!;zziOHQ2+E-R*$wz$EeszPunMU~35ub)eGrz#-Jo>kz z6J=1dmF&>^^q%Hf#+qx>ECc!xzZ<4u8T75}4u5S89Em22CA@dK_*e)<7O=QhgN+@R zYx1S&4$G?TehG(RDmy&W63bfbAv+!IyHl*Hmga9B|W{Q$XXatMZl-^yNn($PAdX3TO_Pv;TRVdJq2V=@H1=h3J@Jfva_$RF! zcF{=7p>0)xQ=Q=OD$j9yEg`(h+Fs6pSTxNeTVmjk(FZlmyVB}Gk}CTVr%xZTKT+_h zpU&6K;`lf0hOBE+IP!b(miuO)LAX*hu_#L#um#$-t3Vy!xmpK91f(&5H~~;MOoxH7 zjOZ1p0WOen4HpolC|ckn2nh)xo`ty;9BLg3FTq<~it~yX=at5vKd-?eD0-6uatQD` zQXv7r)P)dr<5ysl{)?9{=?mFy`}+DC*Y|BdNMah*ML(1xW=8y)^ywKH4MnCNBRms?~54 z3dkljlyqNkZzEZF3SIN5(JYUsXB>fSHve^0T4KOCe38gN1u}dEe09Y(BeH{BJ)(>M zlhl3p_%$lvO^D!@q;7#)_(kl8uZbDh0+fJLRhWztkCS75B7GUuDaKaqeP+vmavhFd zkV6626L`7cFP?@jIQI8bpkFAiMh2zyYm^JBP`*ZtXk8Rs-v-dnesaGLJfDnUD7CV@ zoD6jSd%(^x2bKpWtm%ve!DwDF*d9!!s1YbXCCe|$$+iuBzf*2s`v+>AVK+R9F31a4 z#m96}Qy%mDu4FOa;&jKl8qq^$HJl9n!ngDBDz#>py*(p`9(p5R+b;d?-HAed@h)CX0-+#cOF9WVu$)bFkQonvdA}ms)Q6EU-JG;9%kGm>>Xu@=v z_mN3VYH2A2BO{|LAN3>17L-&}jITvJ16yVTu1g7aZ9k1PN=D&BUp~{;a>=*h0dUUv zVmcl{jDZ0LpiM{6TNlX2Xpr+vUQ<$^NPvyf>F$4Ty^5T&w~bGt7FCkg(Q{!9bqr;c zp*XfODxi=+`m))Ub^*_M;M4xC;f1IDlKBpJJ+^t>1iL8k=z&DSc1aXJTnS1?vR49e zFzO9J$dG`sRTZ1^*ea)O;~Q6l>KC?K|I&iAC@CqE5a9umHf2Q~m=FS|#uIkbUWfyN zg>-|~nwu~kFNoV_Ll|5#N9LJGz4 zFY^r*u@>5V7a|feD`Zi8!6K1E9JlS5!o&r_w2Suscg7MpWSGvIscf6@KHhZ}A`^eD zlc$PGdv6pCuP$q7g(Zy)C>=nA0nH(>Xn->Z=@;4xRs_=YPn&W`=Tc7<50$D5+-uOq zZc1Xd)FCG~w|8zX2XYO%(h?Yd8yFZ+qbHqNDEtpb>e(%GHXsKAwHBgRGy*M8#(;^q zK-_$P39DtUr%7O7nrpOh{j^FiW1Jr3v+WUw7ZmvzKwdH7O&Sl zbWuj!EN<|IpSuRpLo!Wi)bL8sqvh>Gh%7H)7a8WVuq8SJ94{$`4+sLNWLb1HB_cB7 zTP~e!N%p$xqu=qi$H`>^Y#NK@@-@*Gbo+o#q>35)gUUjzbdhWUCIu==i3=17-!yRU zmc{abNJQ}}=HH*NpSD0zxF>!Iquy_ru z*@_8^-kzSM42z3E$Xns!jMVICMx_D0dbH$|!YvRvg5v_SW*FdcU-BnLbJxHQ0l#Z| z^hd{!AK#Yjl;HeJ`yWV4ohgt|f{Q2|U znKVN6rfB)rTgf1$T(U3>7}?Vx#DPuV+6(M<;661>|MU#?-2#y%&|zzwwo*U>hE|l3 zZ)Q9xTTGcf7l&;uJO9On!GLY2pW5X$Npk|MC*l9QZv@S0uu2bC%~1VwKr(UsLgsQ+ zVn7?P&S3cztsn!h!vWZH6u zs`1_V->!e!a!KrizC`gG4B=Kj8op~|o%=yLCf7B|&$R|uL+A1Gt79*L-e~p=(WLO| zo0xTpBW|yier<~z+VZ3DNu&PvM!b70o;Ls8ni^<^*Fk{kO`XUMDKB85EA;XJ=MY1L zFPKrVixk-bxttj^_nZ=S$h^P1`*JrY9T^$86kfc~&XTsczMc+-a<0HXMUiC%DSU=d z6Wg`$1lSZn8?&mM&=7~QQd{AVaxD%l1gPiU#2Nt#xqVhNQ@=DG9RA7-L69L=<2z=N zhg5YA?k9A7{HGsECPrtPJ9PhO4HTSB$%iPLPaUUqB^4Xh)J_w1{hXSj>nq7o!SHZC zTFWxfIULfZkBG00KAxe-6>o`T`?@+6C**5j>89}N$C>?5RCdYPY7P+o6V?Nj6qlsvKR4QQ#1<<~T+YL70Od*_hJui5Ds z_34g%P`;_RHskNB&(QBVEB};fqY2N;l}Lxl6Jmqeg3wo3o)o!_&5(_{?|i7VgX-Yy|JTmq@Of-go|b5La0bKZN&$Yh1WrQ2HbGT1APp+DDf3KdXop)DAg8IzPQ&U z51dChDR+Ld_PhH|5Uf86iM%|qTBz(sByvxs$&mYLM8Q~xve6gvXAfv(W$lMox4p_5 zjB{BHxR)rtKGAH!4xyn|ov<*Xd=e+H98_BP13RR8*(IKljdtB;-Yj#I<9TP~oyoL$ zGxb?*{m$XL_2>iq?+wlT<(fwlKl`;QtikxTLrVH@AA+(tm7L(-$Cj8>ycG}Hr1nv$ zcXRwH)EFXqMffj9e)cBE{_hOKWqzPYCW=ch5El$I9tdD_PPER|X!irq9sAH>{D5T+ z^8spf)&5i00zceoNZR^#cHtT4irSn5``lTbZcIvf2vw=PH`eMP`qI>VT5QZqZf|dpk-(^vk5kzaRxuOOj+1q43ssMz?@u zGN@j9Ua&)(Hv+Ybkva#yS0Wq<{+Nz7Ku<_8AJ_}T*$BjahC4OC?Nt7^GDEAigB?&` zO^q=Ai>q@Le#GiIsji<`sR*!p#>TSo>v1v9EiNzD!x#in0Aa8f+yQFr*a=-Y2tERG z|7^&68K%sJY0rk~L?ZyHL2V3sId+g~GTY&yiaaWJqV!|;Keb5fOtqz}hWzD>!=L_{ z8v#>p2i|zZbHRzVoeP`XO2tz|AS0J_TGIwo*A%a4jT)O`| zK>Xmg=yPB7zlO^l)~W!S%81@Dvm}3U?i$f${&tPmFslrh|c+ z(9}TyHX9ljwtHT1Fq!F9Sfl!TrbxXYrwgRY$g;y>n`+`BG1 z#;+gG?{gw>t^vFQZxAvBG>`5^Gx2ijmRZrGeUd(6eDiM+pRnQJ_xlm<%Y`MsZ1o|D6^=E}Pi7nDE_ zcR=eMvf0lyWqw~f)6B>EE`NJn)BP&_#=7{#DQeFi%c8qN%4>;>Zt=qO%nR3Ls@ymF zW;G*oN{WBK%1MBb1G?veU16|G#gZn7pe^3|mQV8UuiWzTvY$$J+R}RpOoWw z-K)JPAP#LAxQ7VoYK&BSqNr4Qma*Ed|@9$tnR5St{I) z><^dzz9;G>`6Lb62!i(NtgNU2_6Vkk!ovNd-*>=&OhH>)_Qd@@yxWY7^^N1kMI{(UaF=h>(9z)_xG`I@^HC>l3%gZ&nE`FAf4_?A?3$*2S2J!+cvSu{*D$?_V0I}Z zi`3Lz2NZ}L!K3`79&C|~uM_aGZ`Mh6vTF-EC?pyaSP5uMLX<>W$ZF_z#b58KL&*W8+xbt2Pd(BL2@mhCp z&C=dp*Q#VPSJ7W+HGPrGHJidrWo>3F6l|W`G8y8{U&-8lbTqE;bc-+@5fINaj&5lIbNscgyS`4-UHuF2{&s7a*1fJF9vxCnI$0zZ zy-gUeM;)(sE+_CNT*f+5nyCEGC1i0gKRC7gxyllEWbcMwDjTv8@@Ml`o3!8!QV#LX zi{d2u{IM>DB!M~yS0)}Ra;rvTAVjNFo?{jWh7a>c&wB%kswzrDG-y8lh}xsqt^4K10U zGcTwud@*v0$z69084*28RRmsM<=8j^4|SBE*@WQ`h@tC|Mg-4_CxslI81{_R z!1LA5#ABK&1owZb7L>AG^P7sHvg)L38r42gd!iYzMz2!QOe&+dn(N-}K`iJ_xbwE) zB!iA3q5OhN#kpbU;Xupu75U7n|DBE|5tbXEq3lh}Nx6T*@|KzQLwGBPrV`sCo2Pk67FbZa30 zk?`feUlAiS&ODO}pN0(2cr^)}T^CP&-RWol7^2~D*RGhfZWqVx&+mN~brX^WMId*nk1=^GNW ziZTBv+Kjq1JqE_Xl7F|fLzS~h*CROn3f1!uGWgTw&}_?K)4ao;knOZKhDzglgfs~P zHY~)V=#SMY9#WPl?z3L1Z4!XfV7!C2^S(gAkqSO4{X;|BAhG@4xALQD%vMKuVyxl} z)P;anJOC-;7ZMV_D9pZ!3V~4RwD-YD(85-eGQe`Wk~ECojEg{W5ftMdK}=E9&}eIS z5SP(SmvyBb-1EKnd$3bbu|KVxEe~XM(5})HbpBj z;L^jlC-I|)8y0ej<*c@oEY70qOZa+8+96T$It?YSIHGTQiJ0GcnGZ%sZ&cH}{O$dm zULC17Z8>`3!MB-=?~>KnuA|T@h73#7UW)q;K>==omLk=8XRl2H%f)$C(h6UuZSr>W zyDx3lF1Hs}b_CI^n0E6M*RiGN;yq*H8CvM*GaoWoX>)opa)=jM@xu{P#^9)u6j^6( zpjiRmm~p|ejQ|cK<)Iwm=;AHg#HlRP^Ppu2+yD2^#!j)?X`0JI4FA8qTn! zL8K2?YHQKIU0=u^ci%&JU}cvKneR%F2lp0ATH4Q`n22uw&WSi%mh(5rysdhV)+_3` zf4tEk1N$+3qoZvO_V%HlKQnQGT&*B&$YO_#7RNINVJ@z7aAGj%r|~GUiS%cwB%%{<{FA7kH&arD@Y>GNk4~$)Lp@;R1DMkYMmd=jfM5>a2L`~;BRcf zoj?q;7lqTjQ`zFE=4T_F@&Q?lNivbGcH4d}(!T|rE_>t;dl@MkLj*~J@R3X*9N$~H zpD;A^vsF1_aZjRP*N@^E&kg_TkylA#3iij41p=v~X?u85m}Woey=&kaIgbGrgGr!vKAw8?6+YtvyzVQbt=YfY;89}sdEWJ zUe6v<7H58G#7(a@e6;;Enzl|2Uz6yFi-)_0k{I#WJW~0ROV;@7QW$Ls*PR`K>C)&~ zr*}auoFuY8w+1-RC85@=Mv{ie?@L5q00G>)SD_kb-gKmfm9d@|*WOkk2yCSx8-U?z zRoMAMI1oghs;B@sSAV|#v#b86(7&WGIlOzTxVr-qM;bg ze{|6$Dfz!IE=?H+T2Wk#1RVnKm2B)v{`RA5Lthck`kWBWXZ#>Vzn~xvTm-5<%E>}9TI$~yq?~>WzPfmU##sxW2|g8uCUR`gg{jTip31mk2YWL6ut!^V zoy~A%Wpl9P>Goz?mACWPf1?QZux=mx(8evICLbY4LRwM0^Kkog z&pz+rzf$*^9H+uJ`CzXG<9NQONp1Pka*y$+ua9wMpS5dJKr(Us<40lPOFL~xuw2JJ zy`QPPy>VRmu=lR7^FznO*H_C{ori{O+n?xd|=#$VEq@i&M^%G=j(qwne8Z+Rl4 zb=jRFTx>3|1_@;$te_-Y;fvSzqB)N!KaEe(B4{qKSM}(O5#@bTTRrsl;;wP{#~n4! zDNs&~in#JY(gxR?nHX2@XYwsi@qx};;Td20mH=hF@-~T}LBX0fw{HnIjSO0%PrpNg zgOjteq_-6IoT~*97zf`G4DTDaza1`qg9hoIoD7CMbNJa+5P{qxsjW?O^1G-@^3|)m z;G_+kc+hDiBDf8l#~^V6-bG3k5PniqQ@u%e=G$wt%R`3>dOy2uc_=?q7ys|ei09)P zwHR&T<5p93;q~&k;cml+UBrQdP2@|%vo|~6aS}p_xN)p_zb1kFvcs3W*Bu$JJ5uXr zZ^|K01su$5A3pj6&Co&}rjAC!lGBM=AZcD~6_eX!|4bth(Gj9hHCe*R7~8O{5g2}I zS)+<0LhKczU0**1hv}IB$L5N;g_-==atY;i7adF6*+Yku;iDd0T630`Pha0d4%+BF zKQJ6a*hDX{O3+F_fAXinf@N*{lSbh6ol%*qEJFM~v#7IB8{YUNhsYST@oT4(xFJFNDGnz zBHhx`(k0Rj0@B^^P44gB@9*A13QSuH6x#nS2az1y$O9t5Q{v+y5dgaR*sf1QY!%5MP8$&Myl1_47t8lwathVYSR@Q~7qk6xUSW ztXt!mmTmn=WW2Fd9?1D5QJ$M35>J*922{ec2dQc>xboqESONO=%Z%GFyzT68_7qP4 zl5!m?(Dw(HS5)94h{2{71igSV+ych<6c*KMSX@A@kOGF))g_CJcv+-xI%I*9H_`>f z0MNxSnFP(R_kuw9OA@s((UEY2V_hS}I_nu@QQHoo*V(k|g-; zEyp=6KgCPrMZ?A1j6yc%!B;8us9`mVHKukSirx!ezf{xD(eXlVJ*&tVy8Fx+J*;Hz zG|}tF>oQ{Y{-b3=Y_>CG>92g;Kg_RqotgT41pMwti#!iYGF<$zO4cUyobfNeBPIEa z>|@`;C+c#PB?V)?RIdasaS}$|auCJsYLBjZN>s!K~5Ao?y9H_o{n#vZJ z;;rh~XdePoy^hAMh5B<2+`XXJX1~+q(<@@SPWHPwBX6n?_}{P-^F?>Ia@=jPa|=N{rE4x`N_=+T^=L*RW>^U^ zwv@T9JlZ>D+6d;%MXr>hkdUr}Rr0W@j|msBE&0#!gD&RQnvQTM!W$ISd3n*e!wIp7Q5XqVHDK z3bLbpSpokAovOWlRaw{_`Oxg0hONfthjcl9XfogG+EqmJQzpDndJ(BBRuDQ)CxrX_ zO$a-YZ1FCBM#+NI>)3_);oVQ6@6i-}=~`|T&AWw&xjOqNlmz=t7^tiN<&*W`!$(2d zai0|Td*wM3?!WD{nl@}<$i~H(!sZuXWXOoivn!XqXR9j|mg#kU$o4ANF8C02&<*dQj-YkaF}CY>)TSw8*j+5$yU_d|(_*r%BCo z_dSjpQ@tbA$NFGOx<{6~+n!M+v2k_>?vXio8nS|d*Bj@{9m_o3*Dq*hE870%Bvdw^ z-;+Hysrszy7F*cg&}Z^n>2xVH{1LqY?lUtz(;Tf|Hm4QCGW3GIo|~EK+iDe3GeY#W z&VBW#d!3`Av3n^@{o>a7+$q_z1{S)CZ}rm4ORalG3;&(;r_SmUU(0&E`M&;pwb zGQ7W8ie(GM;9b&WfrNva^~!?Q{O1hK#x$gX)?0z~R14Xjh_u1iz|MSiucu(?<^+H` zC&mFLV)i8Q-Xjg48}T~)_d-=e;~5xuPzFmfAe>ilkjxxZMxgtP3sL)pMMT_3s84-lBgd9o?+p$Pb~06LRAkKB(WR*0 zD$VWD`!`Ks81!sVLhfMW%#-0n%B?x;Ci!!i{1lhM*z3lC2=XmmEs?*)k*K=E&B~I~ z?ckEsp@{f>7H#!4!q8ytFL|AB2Ok-C^=g4{&TJfqK9-S@1)o+ zhSr?A=GjA@`k);5p$YYyU(|(?=0Kq&wek|!N(dbVkXz)cV}Sn$bpPMMb4NgdBsmph zdw#SD$*`9qdM6FnsjG6n{17y_!6$VmD~8_hBP7Kf&ge2(swSs<%g|^Gos?0aeG3Q- z1a|o+q$$*X%84$VNMdrbvH;f@6ONPZApIKus3b&pWzEQu_B%h7o7?hH)aAd|4Omwg z_4-Upfd^_B$OUmxsrYm;8G=LjMY&!3pZhEeWDh?n+4YK~B+-M?)mO>g5%Y1ZYCSqV z!DfPu9+N&>GJ+a)R}$mBvij#;BBqDz<3}W;Unj-7n5})DX_xXOm0}Hz{uM*`HXpM5 zWjl;SHtj0WX{y{1Ae@xtl$qCFrXg^#`Ofn~h>iKVV&d|vTV0pbl(La~4qHSb_$=#( z5!ZDqBfCBli;c=(e%13io;gnxCSUyCHQ-CNNLK4;bRgBD-;eOYc&CiPROao_qdEyt|zAqGfeEh zo22sLLp`<7%zV>p567AFW2aj;85$a(VCk9|D;F$(82oA(0&INnbs|r`_QRuQD=2EG zsYQA>aMn-xpc#-y^mkb==!B{YWd$h1vUa*UjW#whewqy#N@QY9{&_*&>Tq(zhK9f2 zQNtm(>ksIBdcHd5>SX-{sVDj*c+sL|)e(3zxrm3zanXsqda_EtiyA;Ux63I3F zge>2MK&W7t-4Go~$^4jCY?Fezk86r&xXAbwpU}bm z3z|0T2dGG&)ng0DnDiaI{D{dn-hf-}tN6FOI;Km?*QznZsxfH8fho0L_n3*b+<&n; zbW`R!LcW%6nC-&Y?x-=5r`vjt4_iR5FtGIEJu>1oF>o8#^+8EE=PUo%#G|h*qcn8jBo3gJ&c}wN#Ff2hVA@p z>fFZZLJ33cv_ozF>amg_m%`f%^EgJq`drE0Tr1U%MuDr9p_6_gUVf!S{HQw!=^_ z`J{pnivGFj^107NPjoCLC3 zw_uqAT5MO$76eefmm>InEkzK(4)P)}pzMT+T!Fr$5c3?5K))cUsb4H?7+|>f?;ID#**o z;3U%e$JvsJbJUC{KbLNBcaC>fCv3aX{?7E(YI&>4HD6!}YUC%2tG-_{bbcOhYpN-E zU`{P0kInlqlmd_}DsUKs?)KLe+AvKxN}6^R(kMvx!8ZQ46%Sr&e;_m+PmGO8{};-d ztg?+CQYhkgAo^a`8s?d4&e|gbLZA~i1rHLGH8%~WSQ?R3rOyU8fCjXlu9+qE()rhm zxr^{{a74|-C~2X7^@@9{>S(<*8-w2ngZymfKGQX|?K?^bP@wNiZK$FOsUO&YC3(OlwR z!j((X8KqXTsYMERs7Vf~z2o9-ORq}1qfbK;g*JQ66Y@P1!z`Cf`^we4>y$+jFZp;M zto7wPcRs*$5Nus9>y8oDkFn}vkuW21JU2Bn=-cv;^?I?qzbzU(xOOzU=$@o?yuh^n zn9`4f{AtAIuF+7V)A-`)k)hIrV3n61K_%sbE}+hqxLwT-ByHyxG9=>#pvzX z)vRX`cAjOoA(zEh?#*XdgyV-p=8szU$&#$bD*GN;kKu)76$3Fv*l0&#>2kQ3vGHUa zuwjXA=G3_cy~{D61tlyZR{@MPDza)xh0Vt|z>IE2H2n5A+||vdHmnh8T?{qgiwr!% z`iU2YNzwBBa`mX}hwaPj7-vek&^_a6UHQ@KgzTxKFBV1(%dtxMyz~O_24@O!bFtdi z8?WFxWwxpp(Z@wVPGv3bIhY-Hj(;`NC+U*CE2YOA5=~I~Nylo$I;;<4vozJHYuly( z>vI4lQS8@gW67*c+I`k>l6OYvOdDo=1!d=Rl2THpZ~m|_^?E4c$NJnvl-|tHoaMy3 z2qI(|`QqU9BW{YMAS8sW{MmYq4_$i9eX+5RN>edxUdiTKaWb^`)r&^WJ)2&ttJ?vh zVlvgUt02Py<6v;O-?tj?D!r=Ix(Q)2Kl} z0>l@+!(K9SLTz%q$%23!LXSCSp?{Ntki*`lJam7B8c52mfQ9&>LTB7vSu}A=11FF z{KldF#*BWN*qEDW$;3fv6_xll9)HOlUu9xVONf69WX)sRZF#7l{DZhD;Mr5Bh8cpO z*AkJUAyR8XRTP9XbV8M<%)pb!)#XT)9QQsaY61GqZ*-L+#B7YkH5}8r*7>Czlo{-E?^M(&Vfb1%V{*B6jT2h~Su2ar*q{K1%*0G+H&TG8B+4AXT2?C@ zI~+x$j?hOKS_cUuADOu-rAvOQWtFh=!gbBhA_uB!Ph^QuYB@aI86fR{RJA_san%^rF+XIU;*403KFB_fu2G9c048%Urn*lMmRtL?|fZoII0#!IDnmC1o z@)?<#7r~7&x3KW9)V<{%lRG|>ce=(ybHJz!lxQ$!>?6pup|@%PzNo%@8Q3p!9tJxT z1p(M8w7A2r1;h5A|Cz?C0=f#Q0l}jRPs0hp$i~+HD-Xb6qz(ma{8Ji*l!zf3D~A$Z zH!ms(fOhz>KQaFj(9?#lmXoT-#Wxu-KXnajvt##_=8pI$OXlR}{ShOSzS`JA1c~Ro z#`6?k`y?dPvYPLcm#xU4v`iWUEzBxY`RniBof)(B^>*f-pZRy8Sig@OA~DP#gN~Q7 zqu7c(ktti)rJ2q6Pnqu+jc0__MKTA=j3cW%^Ox=S&}VmcM_szu_;g(lJkPynHc4}x zPn))kw;eOVI7yhbn_A>4TN%nQqSGJ}6E1LtiJ9ZyMHljJZ_%GZB=;tKm2q5^?MJ5$ zzvdD^c#Ht7ESYS3JEvy0lCW#!~n2_c7g|E9?qyb8ZV4N13)%R!;I+s@T66-4|94= zvy~^B?)mEUfJsZ$F&2a`>@Ob{Llz28wB+e$g@L5}(7zPv!{jcTN*rbwC9zW@Oug?E z_lnD6K$tdYMyVkAHi#lVcu{*cwxE1|oJ0JoT2o)d4GS{4r$~Nd_z|`?2n1{`L!~4# zV3K>+K4N`A!A&L(lhz;3cmNSngFW(t1>5o;ZA{4Vv8F)N>bzy`Iq7E5~ z8J&9`xW;Ngri+)!xq5KoFBU`nB&_%F=5(spL%K;}tES)OX!N!8MGxzIoP9$aAbrp9 zg&AeFs8X@uVl?uD-z*p`8f39W}!1 ztX%w`d|k2%?4%*l5AkHM8<>p{36(g)Isokg;q$I$%SGxb*${L?Lx`R<-CNBoYLEpX z7m3)|{J^F&h{0|_82p}>mpi*net>$FkCV3X>}%k)+4l&LR9I@#h0P8*|GNT^J99qM;+x|%-g^jbv_eGz72RWevS7ld( z*oa@A2%3<;YsA)Tvl7V7>}llmLNM!B>Ae;~$ma%aXg}M@+?Sfo9DY->l=t?V7Q4^H z>09gwly~^|savz{(%8ZvhRolgGvrd}E0Ao`e9S@|34@dR$I6u-Q+*+J{y0>7xuIWO#JeN0o=gL=YC{do66}s?s|Hpz_&R z!VMhx6{slO&pN?F4Dv1hc zxqa$?31JThQuwHIYz&9*@ooq1?29@q{2xKaD77s4sbxjtDXkj$Uw&4{u5%xzp_1rF zQB1Hd1B{?IT8S>2mQ`TCL^BMOS^&nu6xllnx+omvXON?rxH5*4?e`LxNh;ge+R}q^ z4&TCd0m1-K9rN>nlym@?e+AMOAtpo-2%s~hR0&<_K2 zcE}4T$QDt0a{`Z)cux8Mqx^{-&rX=-=I7skATB0GT}MJ+C&jKq%_a$JB~WVXO|g|# zmt}=EYmeDOr-}8X!Yw$uAUUvT;RmhFliD+=u|X<#qZiY+#VeeKWM&Fm_;a2^eb(N{ z*ne4IfPBdIg377my2o5?AX5<>ZvLh_CcF}9oG3T0CTG!72MqE}odMx@M^{^SlRRur zdE%j9t3$Evc6&+hAxTM8ONn~Q(9-iTsf|jqVk`61^6K>IZ%xs8Oj!p8{BBsikGX&t z15uycAzdVyHP;&uQa+`2{TPaDStf@=aOLI|j{oT^Jt!z0*?qGf(k~t%W^T_mQ^$&k=at3@pJxcm~6(64L ze}EtQSG;KC{}eDibf**nN-O$<%vyZt==Q1R*DifloXu8UUWZ3!Nql7u|I-32+-bK5 zZl5&>(S+v&L_TN&2*mGsa)-3Bd4~Brqrg+WK^eue`Kq)3_-?zn0{7Y+;JW9iqoL7U zQcnh)J4s6Z!js+?W@8HCU4k!%~1dy8DzR0pbA9q;A3%E&8l&KGa^*J zu>nLgCS)JQrXxaDR^{pD3?Vs@u3QnJM<7yCW!bQ*b;*`^3(0}l#|Tr_rK(MDqK4V| zrXeP-$Nmd;lXu5T-%wQM<2R@0D|l2GU|_ucd#9aXlIA?&L~H7<-7Y4job$G(!NcXt z10y!jA}y*iP`7>6_>bf{Q4VmW=2xuCW8S|ZD@o6oUHBZb7;o&l)N6Ec5t)qevtxet z0o%O*jT8%sOet&8YTu9;&E>}i19Nr7e!I?l2)nexyMOs2*zbM-&Gdo$yo+d*v@)XA z5NAdm1kag(yF;LarqDsqe51S*T53yne3z^u$3XC}4798=FEO;WwQuY#5g&n{fRs3# zcnkV2IS@fD^M(jRATkidh}Jh1zAJ7DwD+muSi74QbGj7We=#x=%fCz2)>vkRgs>wI z#7x2E>yj_l>7_Dpa7@aT&gOG1@>4b*YI$PoPSztI$dL5&kq?3(LUn9}Pa-5}vvXf) z%=P}2<*RRchJj6KAD(4)R>!Ggq^#^F@*+aV5rHEt73aO=X|nnQ|0j7Grk*8#cA>K- z%7KcOCFB}$`_`?FUGPf7)E&M@v?JAmCr{Oy{dMOa4OA{x&%$Y0oUxzb81t7uuKa2C z8+3{{Rg~)?{N-FF8EwCo+vhJIaJg%HKJg`gDWXsKxK%>(Bn?5nIqkYX*hb<8l^}m5 z(|CrYGfC*$1r-mje{Y6tR1sCYNeEp&}MB2! zsFC^vmvv(~#RvbDolDhR4%Q_d>aodnR|JK1u=mk@6@Ci3lFl^$tX6P_yW2~|UI-49 zLKj3nyF`=RqzbZmBT({?_wO^->!*TKS>3pmB4Vy(p53-W+wK47ks)OY(|Ga1{_yZn z{jSU4=;-q*>q(w9dhuV$V55Koqs?W zM@6BwW+2PR(ElMvh(W1o*EfL$y{j;hEer#amEvP7gs{Y*Z_o zq372vQ~2CWhyxbgM5Gv(t$B1pRJ+y>&rCq`i}v9?I8D|y8A=DIdxG7@kJ}Wk@3674 z_r$W(lK`j@#P80=GqT2Rf9cLp#OXw~09pq}<&2p}bsnODUQ3FTMvI zGC1O|Y401s#AS94j`zlgyBpL0r1spnn4{$FiOC|H-f+x1fbqV2rl{|n!i?A3I0?oX zrzIo#z2KHpzr`eK+Q_i5h115__tWP`A5=rm|4fBGpP*G_@}5(w4kt z-m}5VbMQR9Zq{qCJ0s&$q&%)8Ay%~lV`UdTrND6htfXOpfxKELD1412q7^U->}v;f zO##PP>8&LBpU994ncD9SH>t%!uRS>uqCi6~x|8-6)uu0FyRu@I>RkjJEf|RB5#;HJ z0ifwIIuD+SFHB>Y(FSm`ubDoajVgwTNR=?>8$ufrBC z3j~9%8U{N;i>6axJkJ?3BV%Hv!gxl;7<-#5B5G6}hC`oO8bDH>yI=iOz z1ETMS&HMh6`+VJe>U4atFrZ_*SZ!X_yuQB(>&5k=KH}KPhnW>z{Soso6ivh@Nh|`e*Cy zdp>%Fw53z~FMHX3wV-NnC$?QNy#3k~t`o<*G0?@CX87^`<_Jh-%H=JWc?lED)C)^fYv)NP8Xb$gDWdpl)s zA~7iR1w$mWN&`YroD${Cth`eAdgSt1_~b<>>kh zhr(wAcVnB+?{Kz;(rll|a`>YN5JC0 z8PCapkBlGi{2O8f&eP)05q0CNTljIsnOybfwU&jNSw4$B9 z{4%)gRkHin6DYO9HTjDYo5Af1bJPBXf}RGY`}%`omQZ-^W#k-MOHH>Mf}c0+Q$;kK zi@>&-YMrvhsy*K7*s4Fbb*~w}TyL`V!aF>y;C&K#g(-gNe02n7>AEc1!itHPu(JE{1Dyf6K!kY6-9ynKLz-=Z=Sd^ z{cR)&X!K`#?>E8S+4;B>oP~xEE8Vm%9CK#a4GOQ?WCq);_ilVqT)`oLwN^Wf@pXN>!`XG)!4^ zNK*xgQE8UnHWG^Rds@`(A;L-FpH=P}NtpYk?QcrP1CoHwRSw<-5@PhQ1vou4uYdBY zq!4cGmuxk73{E>z1kSqC7wzZTKRcixO<7{t~R1ml@)cqQcYdo>g^m}uMdO3hmQJ; zgOgNJp#n4Y{PE&^`Sa~x(unECO$qKU<5OW))f(dmOo!XzZYk^hk%MCB1d6xoX1W<% zCLFtHLW0G$EbY^M#7Z8AT9`*g{rpDT$9k!a$7C{#2k$3gm_j8^%H`PY_bA&l7bV-7 zy-?>rK3RnNsRsX)_M8iIon<}0y^0g!oI52!G zARqwRFR1|sxbf48iH~ow;)(8#XTu9^Lx!u5TftZs@Ww!X2YEE|J8*sgA$S7FR$y4% z{)$h-av-#=NLlC8&9>6vgfyG{ZZa8oZ^T*e852aW3ZXxJ%ILiY*V>Zsed0DY?^Col zZ*-*}sr|6*{DOGbU+lUx4$VKo?S??X^>uK;xH<4n#`_vOC@*u) ziyhi{?HZ0?dx0NQ>(Te>xWQl%4#GtH z>EPqOpDu3bFas7973(n69s--7&(W+AYuQ}ah^6cP&+Dd7?P!_>gco~07fQ*l*C@Kx z4p{Kxa-T`F%hwkhLS4^Ko`2I>$I7-xHNCclp&?_({c`NIEcMl4eRq`XqupVjGm_?c zbaeCx3Li>T)QZq8F_(U~i@le&#smBOpE~gsVFkmHVcfP?J2)!}8@oQJlm+pLx+@s0 zj76K)$(k=t_w5c&&OZrf6l}wx`=_+@E^N!uzSK*X)pCK{qWT|;2I<2Vxag?6-3Il+ z0cUMD8+)Pto*kqScDorwDsf<6EGlL@dn+Bg&Rgg@Iv!$0@z*s)7M&VYjl5g%A*(#` zp}5J|QVqPZ1{pBp!cD*GeDeWeB(^tpT=;x_G^HWS{hNbC^y3G=ycd|hyj*@vuQXMWtK&3S8UO=6p#2!Y`_HWJQ7 znH)>8Aas2!XxFff-1PuEna}N6jjfdxIop;imdGp)d@MCSlcBFV6lE@#ccQ9FJZ6jF ztosQmRsFH3*s-lgiOptx&@-)zO(hpYS{ziQrr>DGcc!o4s0$PPPL!6Liv(|`S1w*G zr`z)x33`~USJBkk&uh1HKIF`&EEZND&-CFc(A(3vh=bjD$dhksdNR9q4i1rk0i$0BBx}~Bx#BGlK*LVm+D;8gatA7f=)oYdYw|)-3+FtA$ znGX~4K3HtLMHX(vy>IDYe`7Pk&RINRGt7ZI*1R?lG>8rTc%8c&f71 zxCE14F?n^#Ers4sxj>_PUq}i1NR$|tk(5Ce4V^?Oi+lhWHGt?ETH)ab0E+}?biWi~ zk2sK4M1!m~U#sjHe)Nhw&T`_OGT~C(K?cKv=ZUtqwxftSFs0%yN!U5F1lT<`HWvEd zo3@Wpnn^-2tpVBjpV@*ND}*bJ^i82vg2N?@@a;gr>PUuLNGq! z=ePd+hFLvgF@g|ehv#OSl%MDE@&TX#AZttFC zMoJscL!M=SFlI8<150P_w$GX7;1T!l7nwdk10P;rQM%6E^Pyah;9S#P$(_x(=AzUn zx|k^MVi0dNJ;t|o3QSWPRS)%V^JQl@3#;mQ$Kk7)4>T!88_(xQtdN_iid!gv+ zEa%67D3vSS{rS(a-Hq?nD8JUTTk4M0+xk2^*IlLjnOC&gw&$Te#8W(Wu=)_Em9^>*2K&}UL2eJ|@GllAH?{fmR`6e)NuF{+@^ z<14af$WPm@9+|@}-fR@rXiM@9==n{ILC5!A+J8z#qL8N83 z`*%=!p&nYWDU?K5e%BKhuqBt=AB~eX<#$@L&fPj{w8x1o7iI`fyvas+gs zuCt|FaUq)5&jKUsK0pqerXKepT7B1?za3P5I&u*5F3}*I6 zq4c)B+sIEZPKtbdmyf4d4_(YtylrIz5x9-KU0|V>t z-k(ujQlgBUsxj{jVZ}g+?;j{HI$vq&51a40UOsf)(V1-xy)2rKs>B+q-{a7jRStb= zFd6zfCUyq>G^ueSEgWm?s;I{kM)&s9c1tHTy^_i-qeh}#K;kS3B=o$K( zH-7ds5kw_3WdDd>naPV_?f6L>?b%Q8FLg2WSZ>N749Ylis@cv!Qul}J>f67RckUZ!u?p2>VG=Fs9{@~B`yfELzHl;~Wmjg$2dST7# z!P2I5)`OtzZEMf^qI*4lRbNKeZ-XH<9RFOA=NC68JM7%FMjssJcpns~Ykpo@`iT${ zlSsvc%WxQI#Yx4OW)WxajrMnc!Nw0AH}M@eX`Zi|c}9iP>aQf)U0l)NZ67elfbq#6 z0xa@-s?YI?Hff^}zP2d@8G}tno;;R`aWEU#c|FXP>Vocb@dazhTm?aR?$YuSWc6Tt zBD3J?Eb?UJv5eK15ZGq~k%|+vaalP2{|YI)HVI6%ekbU%C&{}WkDWtdexgflQ_fJC zq(EoU!xZZS(cPY*-t8;j-?CR|w~;f|M8r6frv&2*Db(=6ND{YO{&e~emjrk`3yUyiSpFE;0(Kou_vu?(=N!HXx$A1O}T zHDkilv)gaw3`aCxu3=B9)FXQ3lISG7JOzT##5ud@L=IFl5_*wCnLKJ(k6g%m_(bj# zOQB>_&a61+jLkmG8X(>)){mxU`x?zVuKM3z@0%~UvWzm8arXgkp18N?Aoh*>vpyfLcu;w_k%a{#_&>bF4oaqzu#36u zwbyNbGD;S0LdzZkh1FQ`m8g-ic+j}p&Q7B_dKX0d+1UfViwy&3A_BZ}lh4hz)FHv+ z0tCHsWL$m~WCSW@Ql_Jxw}ny-zEmb8D{x%tMi(K>DPG}I;NyQ-9icyNWtMbN+J4)H z`y?@(WFhi|i63#39u+lI66w#Hp43Tva%GsHZ`^LEIL?@|cz#{((7yx)U)|Y+e*X2J zOU;J5Tix+hCPBqkajQ&zpBNuoR~uR?Mt(@5o*-gIos}=7?_bZY6*I)YIn-}p6!>Yw z`o)`c?7?s!nONcKy4})2?dN~D)Uq8R$K3E)q}tMXIHqo35PJC_FuN`?{O=zU9YegT z+;$i@s3#SPxsT<&&D2JSjruHWyYJ?7_rF3CIw?3;Q7c0LHX~}Qt)EH>FQ;H1mZ(5? z(K0K{>?R4=gY`}F>Ro=Q1Zkl1b?ijgcE))~n6*zPLxmKFm(=oERKMO5x_)OJOJV6j z^Qcuq?*Dan{zv5hll|@^P9qMANUpmrb{T=C(!i^?%mWSC@x^7P^Lopr@fDcMjZRF+ zR@u%f3JM6c-tjzYEbBYWH6NzcNe1omb0}jhV0i$T19;pXR4_N%n5s!EM2r!!ww9I* z0!G|+{`wU}9GD2j3rO{m5bYfua26_JEYnA(C)GbS*s4&A4>Usmk&ksb!+%DBm`l}n zClI|_Z|qp(GM_&@eN-eHB}n$K*x{6SXJ5FHDWOB!5I@y}ZKX~&Mc)30Vz1*wF{hGn z&M@}&GPT7beQLkKz+!=>q{_JJn>uO^{!Fj7XUQ~#m7iQa>y6k+{}DlTHE%bOSB3*F zB`vqBpT90~gD^^tO$Oh~bbm?!wLq5b{oTVWcvf6Oy6fviE$jQ4?(Z?Nu0=hzDabqC z&YpTJ9oM<*Cs5P{4Gxg|;{3q;#oVah==C&e=ISCQYUWC>P}Ey9v%2`b&Ne46nGnYNi0opeBq>Rn<}ox$|V z6~}OR%Cmk$gzwLfLa9v9In0E{PD=%YIzYddxH5Ai0Y)!4T z^jM+l7ik_|aMQ%FY51uxljOd&J=jnqy!6QOR3aqG48R@z`S&<9T$A(lxs+vI6}duw zU2h3-8%giJCtM!;!3B=U$@^K~YXQo>3->l;DcrKyMhULiZA7LB4;NX}?uvKF%=+=% zV%3%R_;!thlC%D(f7a`}^`r6E`phb;X9CO&8U*pO1s)|2M3;22xN8_Fi_fs~M_xVH z6d_Fiugw)0M?GWcuzyiWM6hiF)~Lt`a0f&ddmR68@U`3HIN2;d6?_YDr=;*YP*E(Y zBPJz9){tDv#)*Dk5U#8N{y*UDs;Q|-1;(8>W>aP3tK((npf6cL7&Ql9+0ZGuSd zScI`TBHz`6m7V#>U*s{6N|CP9zho}*)zHz)ub`qtjBI6S#u)O%voh!lUebwpjON&v znc>!7uQxQdb{AbZ#J06s2VM&YH0-(L(VfUX@PWtts^f~py1@8_A6FWBSG<9W$ie)* z`OYYF4t{?B{_EW@C(V?z{oAoHe1Hl8nL;`wtxD3DPrn6jk~wqaq~=Sw>5X2g`K(scgj*5Ly9goC1P(?%rvpy zUe_sP*f==fVH0&F9ai;O8jE752pJ8LCL&SL1 z-1GQLYWpbVna2fqZV)&>6@%M1xNJi~6#qN5DetLJ?gHQBtL;w!?3u2xcg?l=d&`+#!3Tb3H%HYIUe z)UPdzp<#E&<@bvDY&b7BzFuRM%e@qzUG>;NkB4KokOD=IaLGLuWG*&Vij3_ULwJf_ zpY)DfU5dLMU2mmt8`^GDm~d)s)Hir!>Gsb4neA!#z_TqYtTWlQa0MT+#K~{Und}+0 z}Aj}p(x;|*S#3VESQ#x z4RkPV6Ym4xPf-7ZM8FVnjCrl{ZP8P1Z?1i8o2xn7#oCUB$m_#Z-jP?fsr;cuKKqkDR{E}k{%Ei@ zT!q`d&?}f(_t+V)714{OL?rWzMiS-_Y|gZ6yf)AdiM?dT%3!VN2pjs6D0I74PkEEP zydl4fMnz2PSJ|ZhXG}+Hf*93brD0YLLBHBik51}~HRbcRaz81?nI?!%|OYbA>Pyf7&<~QqYQFRxiR(FQurpM@cTbA|b zUU1^Y#bvB+ZmQf$$;#3k_o;G-$FfP5)AusZ-#8Tz z#v8x3B;Yt}mLv9>e{^x&=vK^hIiyxRLn)dWa4ov(j7`F{Os~>XCwB&*# zeL?m2jy^k2K$KLpv(B{=$*i(gfV}CiPj@lW)S2F^l8m0#RQ|YSis4U#;lEHB0~3e; z&wB3&sI=m779DLfAqto*MtlzqJQK+h{ z3R5h*o6(&N9kINQbc_(oJE7)2W$2M|V&+M;Vb{JJoh9y=cJX{OOxR+~O|KLYbLk-ZdYA0Fn=ZkE|0Wt3Zy$O*<~N23@4BQF7g6hX zy1pmm+jOUu-j&JzXnLoVWnN(R&-E9icbY=Mu0j>8rvEdVFBDEzMB21YVP)ka3>7|~ z`*1G+o%(+N{;i0`uVC|j$5$E3lKWZeg%<{;<5uc8sEkRyvNlyoAsNz`{=UB_C*_C; z3BPG5+t6{4j0tpOjtJEl%PN2;8qzzknS-ZTTU*spnOh;_FAw*QJ>0(QBd!%YDj}S&w>+;DbqJr_zJ1}n+bEUx;t4W>I*+37d!hD zEMmBqj0a^cbAu9wqZ-=}M|qh)jrlxmXyn|Jk-R2rBhvlvs5yt58AhTNVL?I0KT)ih zTXowayR-;)l~bm_I|Y@@vPL{o0lNLMjfEtp%RJoggvSfGC+EP7JA z{c^q2ki9aJN$2rz3gI`K^%|7~>!EwLJCb@Wug>e(bQJ|rlip5t{nuU6P**2G{=ykw zE~JeH?5SnvGJ4HE(8{#_hvilK&p?MxtpW`&54~|;2PfiBonOhrpUD8UH!@(r#rB%C zJG9__OsrOzEEu>y5+{K^l3&XXd>%(E2xTdCGz2tl!SI(yGBnV{*i|YYgKi4fQDwCL z+C33XoB53*D`Z^@(P30Y759rOs;_yml-H>*OPNLHmkFlb#JFS+`eyyG=&ffWU1WNm zMJgN4WHvRsXj4v$SOFabY%l4U45r6!&4&c-Sz8kn1-Ww)kKRAqE_9#Op4gQ?T=4k3 zYHHOOS=!lZJDQgAKg;^D`_TVr#q+Yj8ul4uRtz8>kf4DjD>&;hQcDt|z6Y<@Pl`FF zD)_O6+F*I6i~xY;VGQ_OSzu6W88JVMAy|cp1c4#H$nT=f!w{6_W@ID;EEO|z^H^(e zi9PlAzqN1}_t2Fw>&AK_M)k8|4*2n^YJAH)%+@(uePG&LQTlWG%uLHU>6UMASleq1 zn?OWFZCzRUvI$z-C)7rBONqlG$=^Been$#5Ol9n8Lw2=_$;R zy^ECT#hx|YB&N+KZM@=J+MN&wL3Z}Epw1~U8bqY!o} zb4N*ox9iXYL1%`Jb*37)6)DX>MFIMZnd#0I9(oLMw7OxB$YdK;LLS}09K_ca#!k>O ziKNbZSf-pPM0U(2{wsSz*D9uV`vm`L{W5iO3rr;LqGfizJ}F@QQXlO+jgWntu{Y83 z=>InXx3|E#u`fe@M=Rrp)0`+zhm0^QOGI9NyTp*?z?1X{K@~y+Hr_W*chS+&s0a{k z{My+m=GnfyxW8I6M=~;*%Rgt);>$xc!!%fiJQ)oaq+Mni2=F!RoIEXd@g^Ert!AC^nPMp z{Z;4xxz569v5I%b3QhLpVBb9FmJ%oXNcJTm|9dbaxfw~m>s~KSdmmiNK|B6Es5>vy zASsL$H|7pCOpKHMGVLc${lvt>$ml5rOiqB!M@&gc4+iaE9XcRQn-KySVCjz^(#9nT zpG|3^l=ofu$N`11d>!{y?~Wd2dfi^g>qWfBgVup9Hf?^ZTSv;~33m!=HXK zJ#h&NSm4bh&1^dg^LwQghvgMpHTi!}ChRcnK`pUYB<{#n?mvIF|M$4u(h$l_u|Hr0 z#NKIL4Fe3lQ1=HHaWN|%vR<>Y-t_Qp0FK~x0$<+1fW2Cq!<5AzOi|96 zgSmRvHVN|Nef^KAYLaJ^DcoTJ64A&p;LGi7<0Ou>k-qj}P^M-t=SF(-oR#+j*8jcX zaBY3!%a7XatGY)II{q8=4Rsaq@0o&-2-8=rK!X`vhv8s^(O}ubrqct)B?FMRA`HO; z8&PaI`WPX`nW%nyUqV7cXUeyIjPstUFg%2V{nTrr2QVytadGkYn~I?%o0?$gX#ID5 zJ`afNxK*5&2xV5xeXz?%`l+TB^EgU6&DiBV<|e^Q1n^J#9Dy4h^}dj(#RE;==HL)3R946T>yyrIfwu%(`r z4SZYv|A_hyc&hvN{X;^?EIT24mV`J)WkkxJ2^raYuSjH*ootFSv-hTBB+1@8duOlz zeV*s}{{F9*o`;v?jL+x&zTfwK-Pe6x{sLZmE|gybe>VnK{-|I4C#OL##{3LLwl8DB z@x`#jpehB+0=yV2kaoafzlN9NQ}*p!n)NC&Ch;A&5{HF+;8I1ynH4EvKrdt}J4eW) z1zvC2tn6&~BzHtao*>dnN>sG2M}oaed$By*XE4D8Y!Li8?1X5?S2U2av8id=Up5AQ zb^FfYj@CVcdlWZIWmJ5*wDPiRlJke|Fe!@c5;v~z$`J91EnlsfI+V1}W8cw^DV#jJ z_nP~I6dU@`&%AH?_axB#cLDX-m#MH7mXnnK1hV0*19m$O=tKtrc->x10P6eqP)4}} za>XND_rPR@Ef};gCG_-ef)51R3RL?}6<@eU?OJ&<>>?*60Y^{Iq)QSU_*-`1jgBe7 z{4gO2`dZ*?jE8`m4w`Bd6cj;2t01i@1ERf9w*ZY5WT2_P=GA3MW6pKvEbd7y>`7lX zBbjg(T36?4>(dO)`bPP*iAPhvLQ9{5Yx+JRQsZZhV$}Pt`1icmBIOquy7;gXsm^cE z4O_)sjrGnqaOEAU^tV(-=bfRiq($hy-m_;y%nOUBakM;xJq*a?|M@o+sIwu!!vjhv zSd~yWf#|XvyZ?w~){&@nv77AVA)x3dEG|w2%TrQ`sn=9Bk=3b6 zhbci`An)huWtnx!b!AB-(N|;|8n-D%tZ;AuWv_(u5zSCELfF)V0a5>Q-OM^c$&tUK2O&QpC@N;hs0aLm;VcZ*oK$bMz5jlV&;T6WJv)* z2D33(O<)-(&q6kCU6BMX8uYl2v->{f=U>fW`>n|(4Ty0h6 zr8KR^vlhGD)x5$db=798*%x^fKf?SRG6UDO%~l4|NkSQeETn>mY6g$bBu2<;awd(8 zeP4L=W?VNfQ}X`tWwNZG!MSF#?yh-{c8^B>(usCYp_0dAtrv>kky;Vg6@v|Y;Xj`>Fhk8on^#9K(g+Nq@xaLGi`gMRrm59zTk&x z*{Dy_g*@5zk6-P?{}RHie&dmoWa9WbA1SCksO_YgE4JE^O+|luUvg04*ka;juhb-W zv|elBy_`!ety6uBNjqEW9^F{z-&Oy;i$ALu@p!8jG(CxX7kkM=iWh~rF*$s}E05Aj zxhPfe@bEbLim!s6xG_~sXRKWW0Y_Km&!Mu7W8Dr~Pe;=1TU1FGy9dpNbK5^$XZMNDr*nsJ#hpDu-qx*qmRdAjjqMl>1U54I$^aQ#9cHhJw` z6M{UC5;ZRx2)>=$y!VsHDm9nH#8Z42+ugabGpwAoAonZyJy(Y@iB=pN+k(Uc1_yYi zo+e{`;W!yrjvy~F5jt-tl7o%S);HD^v-huLFBT6EG1DW}bVKHrgN)S_UmNngJ+axaTQTQ?4cFVZQ0dB|%T|%SP8Qmiceq?O3SgOyKs&bgAdRpx9(yh1bKCb0hh+M{Z zhE=4qT30Z7-DOWO=u={CFM_Dhx3;pbC$oG(q5{9-z@u|fgRy9DaVX!TK*y!`RnAD9 z3Ck>QAUW-lv)Td;G57m=YSqk^Sd@csoS@|;bXY_m)tO<60txb}3HXrub?zd;JP0yD z^L7Y%t#jM@c6KB+&g+^Yef=+0g1x{hbmeM0&I3fb!lm9 zyc{!dAB~_GG17X+B#RE1At52eQY10Tu%GVJML2-v08ZyhgSx&hzMKXZFFdSp!e9C8 zr;Q=$!aEY=3p^bDf5^_`J5&oEx3rdo?$z-VyB-0)V~M zh6Zu1JW8#G8a6Edc9!jEdFzGhKGE2y+jp~^Pq3IDSaDxqLzV5R{2G)pdsJs($TV-U z+dujIY*D-Lr^RZvO~|>E-MVwW#_u-+UP_6&CrK-M#zPgbi~lTLl%+iK8jGndwK6_S z-Sf6OezkIOZ1Up5(lmm7(UiL`xGm#2&-}m!XT*|FrLRQu*{8v01p13kBEQNqud?)g zf0sA&^%{0{GWw31FanAE5EgPU+v8%CIZZI>a2lole4a6MOR zvNwLwZmK(3NTB=^-Y~*eEDj}li(>1$4t82&a+CT!zQdGAeAib$MP$0XZP}J0cL^dIUZsv2rkclL6+B+T?*( z5D9Za=1@c{l1JSj#9?7&4Ppy+gsO|+>eUX!Z6s3uVY*_UUx4N3w7hpF8W}O{2y0JI z9q`#*gkB!xm%I@X5e^7&Tm3yUa^)c|c&jsk%i0{fj3PiSm_Fe3!~>I(HFc+HR8Nh< zSc2uKjhnyk*1Fd_Gk&>#we*M`RcdT5MYJwiH7KdVuF|0Q;J!7Jswr!{G?~q3ODV2j zHWsX1+D+2WVGvx+Y5@PvsC%M3%bqq>khpYn{A=F=3hGj;{t!Ajj~HQ#!m{75>*zD` z?=naxtFDgWN#r*~`eadDiYXr=JqT^ZJ}XJ5cpkG`))N?sFky;Y_NU5cGjNP38+t;5 zwKvrA*tB;_hPlqZ;crGn94BFeby9c1EP~Q zv&xBQrS75@S-i1wLJrQ<(I(k;`WGjHpu>x~Y2`&X?qZ-GMoqWm%m|UK?nK1So+D_J z5PR#dB<;*srnnN)eWxq9feZdoj{>T{Eqd1mjU6K^>!#jLtvVZ2NPkiLh1a`n`ITeV z&DD_*qt}-r;;#Y3OV)Z9a)_-$H#+E%v5l&2;d4awE`&XyD7&<=pa8uTREW&JLA+_x zC6tM6Py6Ls*H08E-RG@>)_ZgNuZiexK5vH8&Fh7;pY$!)^+~a{RZ`o({m3jse~t(D z>9vW4`~9bH*~~ypb-Da}pj;lcQ|Vzuz1VQ5yGF_ld*ccbZE9PHjJw6)3$tB#DMos2 zTT4rb_P>QR%(|LnSnc51-Vq+zy7Rn+jq!^?Z{ozsohM{h&yyE3<8p=fCM|jg!gUz* znxgtA3)K-`G;~AWA#W=xVwOh zXQx>16^?%sSHi1Tm6#t_P!01vNHG7A2?!)a^7}yL#lglVCJa~rTOk-tuYy+Y^0FC1 z0=N->7NKFHTSy^spZY#|h#kTxzN_ssD1A8!N6LMUnKMrq?I_El%iXtN4W&gFOwk51C$kBzv0w<;4^J-wHdi)f{BnR?ov&R*$`oj|b#(_4~#0w=(991rk@K zP1#REvFO*0G@obb^8X&HQlC6F?5yg%H#DW-bt4oDe9~5~h`IkA(&Qa9^AB=gT<+@C zCG(oVP!;7pUX&m~4K{z-=sao+7RtOXU%I%G`KWAh@F8!((q@F1wi@SccBkKyB2uco z;yq<6Eyv>K<%J2y4eIij)sG{UUi{rt?=(1T5QBt3?_y|Bij&sjz-;vsZhP<-YrceX zQQ@Qb!n&a3^BYSS6{1&DOGKtoyr{|61U-eqRiwVu^k=v_xF3TX-O|SV0~B7gLX<=3 z!}nt+NTK_&tw)*|mps%nq61)gn0&hmn*f&MLsR}@j$;Z0dR2J6!2F5Yag7)XhAg1W zT2ay>jccubpVbTD^UD+q7NnX*=uNLI+By*=^#dg5OILo(G)oP;biT;3A!Af(%7>0Tz0keRC8m^jzTDw zY{2dzCg#rBI{zzqAMfbR++Bpvg3gt`qUP$21c8RcYCosco`2GCLkGwvLAai&qG}H1 zJ$G-eD59*{cxKuTpLk+l&DSoSjUnZS0ejr{qY-9D*W@MBx{N_>dIShFLwye&4e;T% z^Q8x)#GnuIED%_^czIQzUIv521wd^uB(@bdHa5QZ_g{lGI(nt^9z}XyP6wDk0MrBm z|5<*g??J42XDKtn9v~X%#l4%_M6IoHAsWHi2((NQY;hEw1TU@-$pGQgxh5GiH#2r6 zJz0N+HAO!|k@k)=D+7Ow$rDMm%p`$8K zD1TRcRrd(P3f6WSI4^8?C;c#jWc1Aar7X=0)0lvL_vN0xA+6^rx}JAPMgL~V=dUV0 z`D^is)@mZGplZKX`y|xwlQw3w(#6+U7tvl7y@LIOf&B-}-ubSXJqo;!sgw%ONBJSb zDqIX5Jo!v`Y*H{-H?Wvq*kI3tj=Y>^uzvK&9xgj z4Byy-;lqvsI+J5k}iHa*~@bn$*Ytwqwy1&Sx$x2MOW&^n}Y_LLdd|IgG@5MKMBW@_`Z|SjFKL}gtd+f!W0ITX@k)xS`Y39%$b3-^1(mM~$ z?pFQmK(E1!wpYI1GWg?!i7$mi5@Z5~u0%X(gf*xWn$_q)t1X4f8d|modJ%&xDcYzm zifI-sqHjarf?BG|mZ5C0f^{z=Cq4HHeHE0XQcvkjH=_p2ooIi!`G~$=?*H#RAK&!V-&QwWa$3{m3XVVD~FdC}G9toL4 zh9s-SosYi;gtJn=u$%hR*0rYz$lns~Cp9(yJhYLkHl-c?EK}Bn8$q1bt zBH`FO|Kw8MxJv$KfBP_8t!)kUjMxgY8hxbQ7mx6XcLqhJ8@nkffpbtZs=c|J5=&2* zD(%Xl75;eRV0uP{!Y+^0tER9kye~2~&=>)S@+_G`Qj3bqS`%cP&5O}187|tLz3rD< zAD*a^>@9tDL!RzP5!~3B*{?4aZG$919<}Q_)mzcuw*5}#)?qY}bK|Wo*Qr}J436yq zG+uB@FMfMcN>8u7eK<@y8EEva#{|WiA`~D#GW7=i&$YlWT4h0aLXKcHMJ#74CBOeA?VOCukq7uY{OedD)05YFn0PAB?PYG655_MT-rFp zX>wQHC4OSG_4AY>peO5pT!0;C4Z}yRq*|X!E+2|mj45?y$T5;yMmTRR_8XdQq8gV3*ESzewUrux9th^j-9&37+wahD)h8623du zTH@PncHdBotVP(`bNJW19U!T4!5*`PdnhCt-}Z~)=Eivw{~-=)x@enHxc_tT;Dtz3 zQ%Yfj*1q%Py7F$ljW>?9nS?ya)^I0Dhn!o)RqU+})}I*kfs1mkIR*_%xf?c;#l%*e zDnT{A`D#`pL(E^?raT;Q#J1}kDi?b1y3Vvu=D4pHXzI9H)>gkbxAc`6p1dhhpk^e; z@Z|e0M>y+rK~n?!m_PE|q5a|%*R(*pJ$mx=@_g#)UHQ}#8i_R>?|P?=?N|2PqP*LZ zBhTv>Mc1c~ICHy8Houc781)WJVx~NQ;UXJXbWNd!L34VlXrCH49;|Wg7)V>%Omni@ z6Mw?2rL5_m&@bmJ55F;W2#M3Q&DtM-B#Fs3bQ?G~zqLlrEu_~{uu{IdzIETtb^PHY zgJR#dA1w_nZ+Au?xgvK78Kc<~w-Sk%fw>*NF~7>M0d4rQwvQPD}Fsc4y`G3~{5*;Ym@VPqgqHW7^a{P+59t#+?R~>73J&7@ z@8sDHf@lrC(i&CO|1uS@Gj+nZ6;bDX)-jA+s?tmUo7|9-_%86qZLf_63&p^_>_ zaxyKW-Bs^Z!%Q8{o-R0DuE~+^W0#+c>?>DQ#}_5+24qvk~kx6ea6B9-0Y;$nw+ZUd|4=)c}WbT54U%2?;^xH?pT=75v>9rJy2W+8rx3<+T zi3#^%2UG%8I?sT4aLs4UR_GJjdJE1qVAPvI&yJ3cF793&#Wlsum_%Rh5XC_Whzzo9 z*OXIz0Ebf!dZtc3e+z9K+78j;awA3>bk5t0^A1CQfzDKR-xWD_b{V#zh|yk!#u!8C z!k8hl#D7be+dQl^VN2?Mrt6r#+QWz7u1R^PBIcp+*at!LBBr?sQ|h*;&_VJUzM-{I zDfgYodwsu#rD9T$czri7D(?01Wbb~6u&y)4@X)0Un=wN661&ojLar|8%*sYFagtL4cnHR$&!U({UX>_4AnyN5 zMn>j1RPaJah*rIO7)+y0nH7|;3|j?b)NM!2Cx-GhDY4uETW>&wvDiSzi}>HJ?E3O@ z`Wp$K?7johIb5 z|I#lrscuu(0B>A8T&t@q3FswbT-m!+|1&CxDC>g?TKZLv#|tHWMQ>bp)8Wx zEqANvERML*jl#LkrdyOU#Hry%whZcDNMna?v9RJVY~r;=S>3u8Y+Q7rEV-QjZcYgB zZqu)-84xw0j|}t=u!V;~)#aTXK4|!oV5OVCKi{a?i~=`qOa!dYQMQG3Q|YJW^Uv5g zNicKLTVgt$BDzFzmldSg@hi%K87?URb6kwy8Oo@tp`=O z0Yo?B(5}Bx+uL>kMZ^}l3{ciWXShxu?lhhcc-{ki4cZOeZn>A|w4*uCwzYG=>g7h? z{ug`fj^KNJhx&pt>}A1cyKYDhw-;dHOkRV{hlaJ~x&4Xx+j zA0HRZ+zuB)Di`&#G_IfkG{p`3$&m7*=(=XE^{QEq^v=WNvrx`x@__3seaG4U>i9$+ z(Y&FKpPMJPM0}=A*|@jYu}N0(J85O;t1T0i%C62iYmV$T>+|2Q*z;JS(~w-a`TCVY zi$QVoZr}ZDAyFIzg#izh9tx}YjQnVE_CLL$Y-aOO%y3&gw zITvn#^9ZIYome*}rrvlQxS#>zG^t&VYytNpPp-lZZ_%ixGuBt=j>SE5SK&BROZJ~_ zKmcKpT|yXCk-n2z1emUr`V4uaYc641D9i9NL4>q=xC2Jl}gPonLr99HP>p28>O^~VC=nHhx|z30URVqz{1y{*|^0H@X}qgel5dW+#yTrm}a^c;a>&DPAuvG>@c zOp@BorRS@3IH!HCc!=8-?=@ZOW`hWT)^TH!bI~JUP6!V{9&#BDoJ&5+UnL!*V{aDK51ZJuJXfz@poWh@PfKVB z5H-!soADm*?qJLt0^ako^u%d!{;03<;3BxVxX`kVxD2Xs#=zA0CD+dfSr!&zAi%5e z^Qgrwm(oH%f#}hHv6;Ei&oG!fnW!a4*RAixfNM-*c&B=F@Fh22LX!Kr`!KimwuIER zaN{RJzt)@Ay%yRNXL|1=cXp73jy1$Ra)+%4S%{d+GjAiFJuKZGUN4$0x!?=iQY~gA z?wN^H*&QBIBbSzf|)-1qQ~r!%7;8!@k~7SEN4SNi-~N^-x7ekZORL(?gdW-a!V z8WgX{0ycPvPH*ryy5DfRsLL+7EFv;~+lD?Tp;u@;fUuB(gKX>OMGw7)wW&~#bHt6B z8st{l{h?ZD!rHjR=I8jS9eYDfSzs?Q!gxv>vmcpU25bgm$l&s zB_muIJ`d9yW968w?d_vrf+!tq5ZUNDsXzXSSdME8=8XOToQQ&khEyK7;~=Z(2Ft<% zsx@e@0VUxY0uBZfXsxC8pk$-`j*i^YGMagLdAop-1oT`M(knj~cRN6HkrTf&H#5va z>ZV16MW$P6$gs{6CFH+MhXT47yU?8rXI=qI3QN;H_uJcpTBn02)~1^Dy20P?oDXWP zO|I*?Bq(^jB7(GZmNS#YlP{_b+DM+uiWJTv>S(-t`!TO`5W1r{3wH!vY{4F#1yubV zVeHU$Wv@v9>iew}PKK1|t>(x&gpbMhxvgQ>aSw;1d%Nk6#9&|B0xAUi%$6Yz1?Ur^ z(Y^rd;Tsw+%oOq8&mE!!U}3^DG9Y7H0e=T<^04=Ys5w)y%(UG`&lG+{EXolX0gcvY z>c24?1#R{W0a}H1;_SVyf$)K{tMcx!c7eBg^OnLbKqB<3ii1xK-(nynBub}SBA#LX z1F^#6H}d+$Q#ik#lrG#Zc($f~SASwd`}VcUsSEz;b)VBN3W=Tutw}QnpT8FZheTeq ztrZ>DdEA(L)trf58!n5kCnUADucN1=k4JmrdspwxRA5{SA$4}F zT-}ea^2dssHT%$wbUt}5SjH1i#fZkM)XxV&5egO&(4>GX3rK1bkZwS0#+UvV5Vu^` zN2PX;Y9Vw~9n>Gx{qBfn&6SjuV>ip9A4ctMpxbMp}Qsdy|@!&<9o)q>Tyk?=V{xuL%TCDzzGD#oe*9 zKIap>=tNom>8;64ma_ea8Qm+{f810%_>p9+qu%?#W~)Rm>AUISqUqk?!O|rkOMw7+ zfq)I4n;ssRt2~8L({@wm&!D0w>npCaf01!L;_628Q*1L)0lI_r z&BnWA#$BP%eyJ~rm0g2|dyNdwbXXMuwsXzx8(o)d2kWpq?^75YO(dUI` zL+0DPU=Msg(g(zFqkgs-+@WHV;pXMyvIV+@qpj_GP(!eJ{%EDhn56SMb2279Wccv) z_U+ppDCHnsq(uO+@fUP*fL_K2QfgE?OE-yysYX%T~h1?#UDqbPUj!^~$7nQSH z1rf-CMb4#f2{Xj<2nHYfanl{O)ep`Mub<04A7NQx&y9FMbN@vy^0Vlum6h)3sd;+$ zrG*biIXnEuEPa<_KK*euj_XbORP3fHkK}q?wc_IEQ~;id(M~8$xr> z%8T3e6=S%M=^NDdxhDbDTQk-aY@L|f^#%6)_O2ZMylOkp6!)8$U3pv&ivzlcixGc) z)7GLrACR??^~k09xUU7cti3Uvv%A=)@;u6RfKcN>ucZy zMjup6>BcvgiXms_=l^u0Q7|mFS^cXxmL`ca3Wbsd`*wVM z{KqW7Sq2F*=nd)szp$dTlpT320b{(MHhQUQ5-XS( zQ!9=^c7d>I*JmIvsbO^Az_`XeDr>&m4r!!SK-(uR<3RnOX4LoXoa;bjWzj(+QSnMs6z1`!y>V@yJhYr?$b*I+K~#?htQk(IR(sMES0~x9XLQGb zMCjxJNKUhRRRrfw96>`H>n-Ts^H%TON)sb$Zp;XoXD?d&DW*b}HS5ji9X(QU^ytFS zX#_gG7`6zZTylOnmG6utiOUiAH*1Wc;PDzXSz3BkC`fu_4~HCN$5n&$3ycdWh`G`0(9qDB^Q!R$9BP6`Yq)gD zjc^LZ-6Lxid28kBxfVKFp2^!|2n)dl`*|X&G~yp_&i6Q2XDgVf__sRsfvQW*hpJ`AxuyZDGMUFa!4?E{Ku>osuz_ zF;CrMVA>1Pe`up=X+0r)%E)13CiK$SUh?GRr0Cl>2@HBl0!#!%7^e8Hj%7z`VCG)( zfiz=f_sBF|ar%CEW)xE-*l&Q7_k^{e28pd1P>(hIeg)^0W!Rcu=|=Vx2CwvH7sf`| zpS$;1l;%nInUB@e-CWW(WBU4)L;u-oTmT0~}wwjqzGwKd{}ekm5B*7lPIP^3=Zy(!*0 z8BP5=1I;U*)*W=ui{g|9NNq(;h{+FK^pJ)O>mz2q&gVF4*SoDg8*rY5-#qIv?l%PZ zcA7+q)&nCa%j5hp?=yAA z=t~*b#i=7S%DDl~)kXJHp{)if#$L5q*Gu#?0TjKemV?Evp7$QGGeaU6EX@Y2C^_62 ztOxIXiYKJRQnNwjG6aZrN1kYQHW*+cJ|rgI0#(evuwQ8rS{)82+D1R(R3BT=-oW#3 zKRm3gR(=U5wxF0;cuEQ#W(>vr-30kkMK)WA09tWC4`!y|)tv`J2kJ0;$Onzw+^7() z*?qDIVy(ERHg!%L$;l!jLn^$0jU^=qJ#ZZRHRI$vr>@Rfz&t!i8Wf8xOO5ri2&!>s z(DWz(Vy9K+McE?F=X*KX0#?k(n-2I_jPOaFPmtdk@2e`OR=ry~)jTIE|Vt)S< z%^3suCVjP^g+ZCkyE~l@zeWw#?swfjKOefO0@dl+q`IV$Y?K7M+5x+bG32g4e||^F zh33W+i3_8FawtVwdsXmV-5K8jL1NQcBjn#vlDS|O@{k+eTTM6tFkHp(2@D41pv)~E zTZc(2($A$vu*`LMSj$RF+o7FIivkzaF2uC0kQ@kZM#@M_r@69m!uT`iOqTu=#+W^W zWtf@4#>Pe<0Cd4@gmGqI{6qj~!bpsZV3T13*YVjO33ppp_kKgUg`^{bug@{|ou6ch zv8-Xh0ei5*x2o5`iwn~Q#=I1{$T6BR-j@%!M55nqs709c_q)dU+|RCu$9f&}uZbk3 zn=C0((A{?;iHP8ykxuoYh*+E0;P@Yr^13Qx43PXkY3f1aP|{c{i$$gET4MJrSq~2p z=6heRfexYz?co-sQ5uX>g;`e;cso{IC!tY_Q4?~9xN z03aXn^JR+npEA|C@SPBvEKw~vQyt&`->@6c)rYvOU3`&DL8K2M&PdWUYIBjx!eN1s zfX-8g(U%EpK$`et<$4nmsQ3g0v-gy(@kq3~;_x~VkJ+v@1GY#-Nl6c293>T%64B3} zKa~;lu^PJR2-VFiT5&M&5}5HAh=gto7JbloLKvy}6;D1PHcc7$nRsJQnB=#o0?$7< zmBt-+{9J9yFbDGyga580T=}ku+4LWFv#09Hjj zQOaUao57n7BSydpAd_?ZXOd%yi+3KF4zRv}lmmmwUOr$F14g^D`is3V2sg5%jqfCW>6cM<+VA%9YmBS8k`}S?cd!{x5FuPbSb;O3q zkYo#Z$N(c6vO=^h|0rOF?H?ROdx}Nq@Sy5;Iyw0%3DAseu#0J4uz!Ol*+jJy?57C1g3=LZv>@>VMf$b$KptUe@Uq3n*)Cmi(W@ z!EGKQKVn#npf_)%#S;95Af_(*@`E%$uwm5izk#2*ZBXujDFoP2y|zMm*pucvZTBkJ zQh)0!r?e3xK<2;^N+JjeaOamz1jon6X~BczHsE2d$Y*fSQAD8NuLIi*k^ocZiGNa7 z`%59oygzeg_mzAO$UV5O{;10Fa<1{2ekwT4nt6eTH$8RpxbEYk37=MI-8qY|-~^)P zh3Vn2`U6}Ho*7{!@U6~zFZcPqT(+H8Ps+W*6LImQM?RRT=YLrRbvDX=%qnAoTJXx` zpO5@RVbaG|%rnuvb7eycb8k-S5oz~VUGs}Z4tyq5q`6tVy~rcmYbb@jHCR_mi^lAU zr*swTT>qLE*g=a*SLJwNu>QZhOCT)@wKZoH?r-?zC_REryFZ8%f zk&lT}7TFQ=yKWHc-aM0+SG_u(xc;<-yQuby{JVatUO0p}xNeGhe!xt-dA1}22o*iL zL*glhXaw?2LTV~nLIqmLu&QCn19A8ZIR&iRtUfa#T0$+o4Qw?*%Wnl$Ls$Bh$(b7J^mRL(5MTDG;aemvJd{`EP_vj#Wob-y9vivx-VKNz5I zSuc>j{#BLqg@`i(@m*IWY)pLH+tMMdz^a*5@B3o?R%6zmqbgpc&sIZy?N>Lx@^|n@ zkL1sf*RmD_k5)?#&&3N*uL_@!Uxavjt_3~qY8W(VJ)Rq7d-X4J49m0VXlb3DW3q!k zu^Va?m9~8YH@E!Cn#uoh0jOe>d4NU=lV<=6Ms+a3Fr+=m4vh~DRd5u4O2CXO|NW>c z7?^hOJ1Ei)mt@2{&;AHDq=UscOl^VYCg@>fz*dJ^9dseUMYanZV-P-ET{?UTeq9TU zG0MMtd*8s!CuYnoH}eN`GcBHFCnJjqWVX7T}Y4DjDp?t z!9FNBBSSVt&xsCCK>oJ}xp&dHY@gHIJw9IJx?a{t>9NlL40&LPkg%~K@@=BQK+HsE z69@fxxmMPP8=&`)F;bE-vNdS&b*KhPx6oR~*!yt)tJ)xo+<9cXPPhG@8=F~l$F=ZO zwp5SVs+)6dTTkA(VK#1)iGy>nBs??+N%@`f#b#BN%eKMc=Nw0r!YQSL!{#7TMmAIT zWb4UFQPI#djR~7ux;H%Pl1vsR3)IBd?_NUizSC1~%hujgNtxQ}Q2B7Wrv>ezG*Gkv zQAhNeXdg$S-!@Y;`@Jwr{ zCXN;SbzvsWROWE7sH(G}zf6}uIo)aRx6L&+;$bxBA*rDG0@!d$HqmP<# z$-C|;t-now{zwRsUaaj|tQBY7daxVxspoB$OL#`zS*iV1()1XP1KHUF;gK`4s(0DF z{f_(__0dDgOGgXN_^xxm{M{w@MfY8ke(HMu@}#|4k?Nbdy!7ed&qE*O;1oKO@w891 zp0x+jv60>y1|t=lh|w|B%?~s-i97r5?n!n|rZ(bX@%uLw<0iZ})lZ!_H`uKdQkMJr z`b0JwE4Z$qr1@FN5?IY|{r}VW2{^w<-KJEXq#?kDN`n;7ADWDN?tFZFn02}e2>pf5 zSeUUy4*|e&h)4rTje_lzXG)!4i)$46(%TXCRF15M;KL1~1;Ge@ zkgz1xDKfhAF~U1gw84!e_PWN*c$IbS@$u&Np{69W_ zX7Hl>YVX2jHcsxQ`ZFAQT-}&h?nh6$t^h4^tM*Dl0&>!sR%+tWSW&?%?okD&X%QEj zmPHOu1m@cK9*i#^O-#Fy)fa6~HSS8s#Kr0N6sDs7hzK%I6lN|{(j7&}6#ePSo!N6Q za?rp*KYGg*>3WEYNY8VmBIhliwL&Qkb84_Jnk>#K%#>;9V)Z1(?1A*P?MlB{<>d%v zB5C9^6W)4<1A$@7%kWCh8!L^4DWg|_T8!hZ7%-&gMxt(k0V@l6I)~ai=H$)DMg`ee z9#?!=9Zyf13h+LqT#L=sFC(8m+>U9Y~5nr>dJO$)>iaMu1WD7&_;L|Uq-*y;alO;A^aMYC`)b` zB0_u{CiHIMGJy1&`d^`wU%%}!#l8NL@K+?Mu4t&M`~I0@EwZ`5INkY*!jP<2Kzu9s z^X{5%`rovfNEti$oH21cEq6WkXZQCmLa)+Tx1_h&WS~X@xDy325YA%fOgV!(`0&E@ zHBL0R4AX9XANbbSZ?ylIZ`x(yNA6Ih{Sw)>#1&T)*_D{hiTlV0gXc6x?%%B(ix^5_oX)i!uyD^cm?tr zu8(a@1j8WwZcUg1Igm1YY_y4kHa%w+f>h4Q2 z{pR-|5e2zcl32Ox6)7;E2Xe|(R8-f{*(f&`7ZV8U6Js$Xc-w1V4*2wAhw;vQO?`a| zSPw=I4ytSG>JlQ@*x4T_DUl$EAyr0z2+;cGCM$Sd6>D;R)8vA|5Xq5JU=G zM)mQMy|KfgtAd=VUiKR*1Ox;#-cx7X<4&k`bo7w6ezqU8Ql4RXMNF@X*4x?)mHJ#v zJq{JzQ8B1P#>#T(u0lI2E*-7#DNDV233&iv16;>5I-0?$cg4SrUCk~qU}!2Yw+aK6 zfqr40gxWyaH!Xa{gJ1ifUEz)^?JuvRWV5NeBJX@cHC)^ z1$i8ejXX}h`b}$h`}g@JTVaph*EyFGH^&354kp7;bY7JEooccN7A1e?mRVg4P!V*k znHM8zF|i}gQ|iVIzNok8TMPfLqzI<7ztTRb{zwC_?tHp`G$AKSX*g16!pp&_G4oaL zlpx~V7C6z&qv{&2fR{51i5sw^Foqm(EujS_NLCn$Um7tSFY z2!d>!FW%+kSX|$}LoU;&)IaWeseHmiPeADG3nH9+zLAoNfrc{IK{`t`2=MihqN>tXA0mexID5`I9bLRki&3 zRt@?yJN_Gw9J;O`_d-%%qCaqWKso1PNzRHkZSdqRh`9|$xP%afu!WNAhT-bzx^8H6R@WS?sjWPF5bLhicDO4RILjj(XGc7>pb7&Gc+uTp8P_qk(OVc(9eWk-iQK>D&6` zW*ABIn|8Me*sHbfTT;lg`JR~7-f??O5f@Ro@uyVhe*=b;)XJzMHr??+o*g#z0Xn7+ zyp4QQ3{KK+WV)hTjIV241V_h23{os2r)DTpPOcN4U+|_?R1j^38fKENe&!P7UobAZ z+T&>M2hKbHX3qFm<=6pJfZCt^v$7CvmU|tYdD%x(erjrqk=&tGb_6^S2B@omy@rPX zbqp9K3>zFA{JkU)+ns&v`u!GkId-~^kKL8^^dd$_wLyQj*}9@-^WYXI=M!owsu@Ju zw{OIVFWs+TAed2HTn`WX?aEU5_EYxW};K<_~hie9a2dsZVClrc`RRp@*M4w z^PZoZAmX=?s>*8to3I%)))aIcARpBfn?EcVutcm3rJ=8)i@_%kC$cS zjpp2q=`9fkV_wf=zx?t#A;LuD4W3pmuaGgsmr04t*qgxA_;gS(2W)-@*IHBeMbl4& zb&Ij?mczC*8|RY;N^W~w520iar-8SK)vO9F~MjrxdfHoAht;Gw5-L%tDFPC?F zp{?$#vR)!Sp+oFBeKa3LgM&R`mXKEXdBLdmOm@>v6Q3L-Fwp<$QyC1)=JZEk>UXsQ z6EUig{BC+XgP8;OzDEx#%!TbhT9sJ-)rsa zoEce9+0xLt+Tv_9*l-fwI2X0nyXc8LE=$G6jiMv-VaNB_@5FC;(@klrD`Yx$| zUC_$Q5V1a=cBgS?n|W<*{GDxxJWEddEMC^_p5w2nYuQOOm1#gyP-c}N|6Ulkkfq*1$jfbFE4Y0bh=iSB%V4AltC)P?uty2HHoo2JkB@0 z|C(>Mgx0Ix1DUc!5UAF9aC+{63hwPde{yH!UwT(DzDp(()ijV4ZrTkcL$Rc0q26M| z2M#yBf>f;I>XsfjflMAUI-!b=?+XiLu^TjucB-T3Rw)y?V72tQiIP`QO8i2~y6f6!>wEog~JP z(HNHj3$pDm1qJHQ@^lNcySuwTefjcu|1k@Qt%uLCAwWj=I)fjOS4c`m2qI9oswS+% z7u`5;eYu!a-1MT|*jac&Y8xtX7lK+}Xwla%qz--$YNeujSLx?LMTH%jQnU5CpW;I6 z(Zqq>#(4JOe6+%LCrtyYaau9yy(m>t#!d%Ok3_Co1Sd-u@k7MJtm~hXhN&o!DR&Al zH;JUPiM)X5iw>P0#;qO+Xah71#Mnj^%|-FKfamz+8x+K;tgOt#Wy6Tjk?)trRu$FD z30x-1t_=EOj`O!LnH8Z{6MWO$icYIO-(VGAYvsN-6iN~0acJ}RQ0e!4w$q^#&Lr>&Y_MZTWRD8Mx}d}!mk9!v3( zFF{dD>rLT6=vnwkW*iOr`2G_YZP&XuccafSNOJmIw}`ircE*h?ul)2*35C(i3iEDr zI1MBr$pB2!gZ&NasyRTP@_pU>8_8e~8&s2_YZ@g=94-1u`O2GZ?)HoStOA~Oe4D{x+1r ztMh;c&p%E1D;tBKEiI1RSEgXb-et!aWssPbz-35!mQ4mi%iZnm^lol$OB)+3{{H?^ zEi?28Q?SFKrlx)j|I05dl&KEvkO;#bwN>*uAXZKu6mHi zN%Bi3LEiKW6q_Hi`?ndSCnqQCsH>A-mr^q3O8~E=_mOw%*@Ry$UQu~)tLv2wwW;(w z49+;-N4wiZ+q|nb=or0^tgs1fJay@cpBWgMl7!zBW#m!XoNwP)D2Wgg3!^xw^S$so zsIYTtGuhhk@D3u&%ns(_@a{kF_+-F$`)=ff5ua3s$Yml3XN&?ES%KVs^sZamU;ALQ ze&O@iBe#HZ_}fTc-{eh1e}A5oN}rOdu9sT>TNj+cfpLV#0tb`>Y|f`k4^;ED!k{{5 zXM9^b6)~pp(oKsU5HW}tF>zdvy0)WLf~}u^ulK@HR4*p*MP*8McC+M)m0U|UNp@Y> z@~Hg$huXM`i=~k5c~qQ%{nKu3NDVz;;bF5xU-5W-hJ^a;TS1mYQ7lw);%(I*%YrG{ z;v)+kEE9s>Z^~9Ymw3ez0VhdX8vV@ywIOsomC!P~A~01&;`>~&l>y#pZRrL5;KU&j z4B`!6>^!IxrDIBRbXU76jB)QN^_Wd=fX4&q_rjvoNJPdHf{#1Wc$;aNFL(!dxP$(%A@7)Z^zpQyP+3nlK z!rQylI)`aTLgX^uspw+6qUQpyt4nVC!S3Y~f}_dRWxHl`Zm8%fC*2N)KRobjEtl3> zE)L9p>bNiO;UV+G@|_>X#FbI@ptp-1uNxLe!6lLiq`&rYwEqu-MchsxaA0HuJDv9* zzPt1*BV?-0|EWO{)vgJ(6xYmjeeOnK&k1ypFE4#rA8%LFR@>X zS$Gzc6w@&L01dsjq+vYJ0xEgouFBAt2o%EhU|Tv?$#j(%nk8q#z9< zA|g`K0@5uYB_$;yC?zHJ4bS&}|8FftmWw#|+;b-O?Ah~z$M*Km@84-6yjaj8WNxC- zfEW}Kl$S?KFiV9(XE`BPz-?)C4&JjRA&|8;UvyH^#OUevG zFH#Rt-~Ie5xAaANy(Y`s`YZaqZhbVY`|*U#eMy&643eq3wL6ES)P`*a z83LvkxX8uteok?Y0=ace!+gK>Z(clwWMaK8XrY=pZ91m)5~x zH|d$%qMGC)H*039ui!e&KmK`c7=DpFfKkYMVSfE}$=eQt4Eh{VILWQMa*Wr(4vyb|Z`6CE9;5OVU`a z52tEhm_Mb};CSw|`ElF+<39E0JZ9UU+kMQQs1cLD+r^^I&GCKSU#`>`cz^tU5x(D) z-SJPdD~e3W@6%)4PT8VOFY7y0Cz3ZPHz`ADWHwY^kIQ7UMlRZJ9N&IWLL6^*L|%6O zrxZp$lpba_q2k(?1vj}6t7Hk6CRb`b%(a}=^|M+(h5UEe<)qAOgZU~#_wX*Xug_1` z$|}5Py${K(pXWM%RgEW$dyMGY+WC2BnZ?L20zKn7H=zxWWOvdx^=~;kxK-O4l9e&r z+n`DX5ta)jGl7&f1Yo+QsiW(AaDDNETP2`VNxfLp4PZ_UJw1xv-d^j#z`%+DHq>4H zq}PU*A|rcgVW=EB0;xwN4d#owx^x(mMURL1)Jah3rf7&QIP~UzH_ct*sOIcQ3 z6Nq~>yu3*=vKDlhPuS?`gO9%#jEaVaM!%fV$xo&u!^vOD#7+j##TbHD?ZF!|j##Gi$gtM9sJ6{I2g_X@m4R{*j-9~Rklj@dL7FV@8JBv1I<;?ja z#Efc8L-z6F+_xCF>xrb8?tJZL!4ba7^Lv!+g|QN5Arb7^rFC0}2*NL`cg~E-42|$e z=8!nEp4Oml*Ydg}!Pl?fUZO>(=A-Vqh+vve4)jLnND7G{Oa@%+V5wC6L-zY2-6OH? zp67oy=xAKtE}IuyaL*Lu^YB4m zkVPEy?Z(m;Q-Mo40aTnp{TGmD_OQE3q4>xn;+dr~qTuvAi@1?SCKE@7S zDBmYZWMDmCw#a9ZN@S36y|1+1_+M3SilJbkKLdhNbqp#%*c zPsRXfaoz$ck>SmQXt2K;&d%j5QTdq26B;!bexw8SZmefmXuf$0+1jp@>9&vI}LGf{u<(CWC?u9=a(| zcZRJ)Ce|P9^XiBq?MFCF0iAb0@;Pb&yxBZ^d=p^A`-dNIPZYeVB10e;DnA;Gt)!~H zqt(A6N<>kdnUR$8KA|V3$HqbEv3$S@8Hjdm{+!eyW4b|}bN;H4 zjX4~+_VbUIn9rgb_#GAXxQ|RD`hyhC>?=DNR5I)BTupFmMv!}p?_vI`jeP&KZDw0g-pYlJi*16HY*AbB z+`fF4!>YtwxfeY;h26UJxH{Lk)!#@vWam&2V3noq+&6C+I;A~*;9G(oQJUP&O0x;s zS?TopnI_Y0uA>$g`;bJIn?v_{M87cT|NT`8{3nvGgtDB^Qn>huNfv_w9HkrM>wBg^ z3s`d#X02CBW?+3YW?KwlG|FAM0`>m>{ueNEPuO?=jd``MjZz5DrA1Vk5h=}Y8s$lU z@eNf_K4oJ$BDP~`Nj1vjVm>ZK*hP@gtGk2Hu{?Tm8 zw8+wwF@qfM|6OODP*@6R3xa9Bn~0vGC<`nO!8m%`YVwLScHm%QB5$&ojWEpb>*p#ovY;~dFbaLVcMJz#>Rll;l zYz+A|G<^UKpm=w&BN_#P60N1VIYKi^Ai5;1L+Ty#4DAhl??r!%+>f-u&0jJ; zm@8GpiyCw+(Dm8Lv;xu$f4&!hjnSHWmYkdm#2;AZ>tSmKSYN0Vt%H*j9@O?hR3mM` z680iJJsmhV3rLWLPwQRtu&lB*ncGGj4AEW}hY=$C%7imscUUu2SIU)S z6wxgZm}dwS3c-%|;NAEI1OSq|K;+%r+%5n)iT1_(h6W1qSUGYsvLNWU<9LP*7SW+b z4j+vxF;}depF2p>6-TLI;H}MctM@|G zPzAjqFi~1oi392yHa5i-0jjFFJG;9!RqO+UgD)*BuLtEH-Um`yMDNm1S2pIXC$d{H z*mpSpE|IYZeNl#jbNs`H`4S5zKDM89_Rl2ui)RXB`FLH8-XQ<-CM{TRYbl;!a z`Dj|McENk9dhdzC@?q1GYB=&GJU5X8yn+eV@nD= z=?Z*Nq>?2C0LH-3(CVK*f1W;jCcm?@^9s~X;X!3(WxxOUAq77TU6Mv*=pO7SjgKia z!5u<+57Sb?uuvo{w53pdD-<>5)6~?}YF)Irdp5>kWVM5E_J-WAlC2>PxG%9~E1Vs5 z_%QI8+3zL3s`6!slp<=&O*Y+L>|lMv``?XqKEk8e>UE}M47m_c1zhH8s8ooCB6#4F z0P!)H{dwdk<6#CUP1u<21KksMclW4Qlw(|@Oj!IDPk5XpTi{_C;lV)p1aO)>ZNp4q zcPvCrTN{&ggkB0rrwCqjhg3ynxm=@t#@6)L)TBXe69wuRZQ=%;CuT^ zD#e{=y;P)N{^AxDtwbWL5UCb-axa6Q<-ZLK(+N>m32v+Dz|f-0oH_-zD+CaTAky+?TBj&$qt#ypQ<6BXtGZk*{8l4(M z4{Hms9S#nUIL-OUZ}WH)ramJnKMwtyvY1`V9X_wuM{&D)nRCkHN{~i0mSh?32MKMP zA=IGZ+)$xW#4POE7w=b`-RkX>UBePIBL{#YgZ2Dm&4-SFT|j9$It{StNn}jaEq#eZ zJS<5;ASjBFL$hDXO-^)+)KEzYuUPbEmuGqQ?Aa&;D~Q?yEv(JvC)AZ>*2_= z4KY?5EoZhZZ0kwT9g~-tTCPPl>%XWQO@?Zq~5fF|O zAfvk=<;JKvSX~{rmS^_y?56yN8Cf zXqlG1jE#fLt*oRI8F)a+kcXESOe};+!0PCRxnu_Z_Ln*%@g5dy(u9C)xKZMu7SFxN z_92T-!tkPs_~JJ?pH+?`jqveNXmU|}c7ADKrH?|Sm&9g;c#f!{5yzKgR|*8inR!q)&HW-_l&*xj)U9OR(KV(CGjX09zCt z^D#>UNbx@<;WWg7yRH*>LYSc>lQQp0DUin~D{SztF6ikeeXYkA_bsL`D$wx~m_OYwakp!CCT_J3tbBX1j(aRKbKWULIa7;6JdPvcT!kx; zqNip)ta6koG9eNHzhSK7{${d!9qQz?NJRbmqhX(TW5-=&Zmz_VzEo; zGZ975k=utBNF%b^Wi`mx-w-i&)OjP;YFmG|g>n9uX%bfa8*b?W^#!bwOBl0)j!kmiYmU4(y=lXvsydU~&{feDX_iUO_SC2-rH zCMM*;ASv)kr30Oe7%{i7PzT)^kcLJLl9B4|A`KzDqaUe6Qhn#a%oE%|ADFFTGx`R|_9M-DahPRL8cs|$9#OTkWzbd(g}4(IR9nOy9I6GjN5<}F z*8c1np8l40+IefD#F$%YX6$J}7E1e=3H-K_M{R<4h}zjoB&W*!sLX0^G+g~Em@T%| z8VCrs(cRQ~3vB-rt@rhJpY1m)_3GVIpErkSf3&kx4vLsqLGXD2=$b%o;)EDiPFV*8 zh$RsAAo$dYs$xeWAmZ*ajA;QJoaNQkd(dS0db`t>xzdAdh}^Wk_7b(B&*^j;zx6ixByu%kUW*GIJm& zfEhIZnI!M99M{?M*lKGFpSfONQpB%45f6m}e%6P@0jQX1r8rJT8UORI-VX^Bi zU$4t^`=ygo>&Uv4aIzD{7G2RE{Cjn!~DD!A{t7(Q}(h2@FwcUJkrHyd2lEQe{`UMxsMc^iAe#iV3m8@v(08 zAJeqq5uOSSh+ONz7+Uf8w8FvErC~n27&2PaSjO>e6TPi)YT%Evjo)L=rVM_Sp zZB1Suaf^TW;6nfYP=0rQhwDIa?cUL-)0olLYmN$QXQw3|{hEdwS?bYItIz4BHd`Gz zq!VPLGmHddnc}{Z_znB7kL%@!M~R-BAh3BPZ;n~A?Uve zlcLR^Scj@ZUUKg_!v#m1=FS}oFsMFFq*dzJ0SqMIh-VLdvJ?FEojO7jU640NBJ@iv+`l;Rj?V45Hy!%-(G} zm;BHUFH;wN#-u}b)fCV32I9t^pvQULn$PatVq0CYG%a^Ov0*94%pMjZOqMpm4}Lq$ z%6#4Y1v6}deI@>kh_^N`3I7uTB;NBR?3bT>;o?&}IHEmdWCUDX$s#{YXmiy|-u;e} z{VK>)x-VZbKV4kq&7svFc6iNsc8_24tnn%sws<{5lRk;n{h^P;tPt|hT$m!!lHD$u zGLJe`CpRpxK$n>_3%DpWy-FXE!C`$ad4d4^B=zBJ#QF!3nmM(R z+S@n<9&M9>yU%6khFNp_h}-ABp=`vewB6W_Cfg6~vZpT(xOYppFaRzK_=Dn{j@*wu zRGTlle1)8THBvnO`Wa)LA=32IPwBfv>Q5g%UwLlufzPd4wSLe^Fyzje2dbl%pUsH- zZ&Ts$;t?TqU=CIDhYj*0&D;P?sxbL3gca9jWk z>>U_*Ae5zI@wOjK?9w*z-<(Vfr?#84@-aGU_t{9xt&qW1XWR;I))}D|gp-2!h z9-h#W@s2b&^F515tjg^$r{STwvq2^MeoD|{-c0K5$o{MQr%UUty9f3l=9pGb3d^E_v3xNG&(EN_r_P1Ta)&((L6 zsW8wT?cZvA%O^}R_iLu*w=ajspIFqd+RXt#cvqX{-OZl6q$9UbTJ2j5FuJ@j5d7AQ%M1w z2B0DlI?5n!mVriOl%TLMjHw?|Qy#D5%y=~NNg3VTtj)~m!5_#N8dA9nQhr+Bt12i6 zhR_BJX*GVj4rH*Sqa$<#@MtD54i_;vGyyJoXG_cE6@eD=oS^cIR>i9yxKNccB|FBL!^J$x-WR1 zy;Y$Iw=duitqYY{Bz_>-&^SlzP1ldt6`Zt)3GwauUu`||eQWBge$Z@x@v|%_Dt>l| zmXPKZ-*3s{%z5yGq9OK&g$O&wgUl9+hC$JZZ)VeZ4kstp%^GrX?{9EQ)t!|*w8(KW z-zV()ll#Yn_qp(_mtEha`5TlR49oXM9%MOc1&%t-d-X>+xO(EfWIiM=+P6(Qb?p@(bOaFOxMujtHNIpm5{hcqu@} zfa4jRj2=`^Sold5&0U0LS&Ck3Nt)7`-F@`vmN!T-uDMSbbO}*j(=aqt`Ryb6$id;8 zqM{-=1fgnO<%atDTBx}}8LTTMIr%9_Vxc1WEU#G#!4wgQVhmdICBA=uZ8ecGfhBIc z@oAFxO;wUhe1@y*L$`h&zB<*<%<9-YJ0xBHX8!SKq>}od9nwBHOWgxK&)4eE5K0^2 z$5MLN9wx`-n24k{XGO`V8X39o$^Iloi=66C~S6E@$&Wl5H+ zl4O20<~IWwPU~vURPsIbWSS^fY6I8e^tFRWVg+NbOt`*VY4Q^h;A3$%MhM0&bt<7L zjaw!>>!ORwPxOn|czpt7#v}az&M3N_-L81AtXml2VcT=(nJp7%<-&y)oXjjiWg8A% zH5)=paxibM$LUJIZ(LQX$JsF-8+MWq9pCnorP(ImvHm2?;Z5)e8lQJiL-2~BU#Fz3Of}=0b8o2`D15 z(uPz{|BTX@o|%aOQfNq<3F^gUFLVUL$>E8KaH!3~h#?vU0_K}LB11Q?`~k!`;UG!y zly@9kXjoN<-;_Chr9;N=15e^rk#YK?k_W_I0rNTUs$L-zWs&AR;|4F%M5q@8QA<;A zb6m&iER-%>%+C4O#y7O);Ov1q!-+KrV`&Vp`qv zVT5XQcvAdY5M!U4nEL+in%jq{GWM1p+&fD$D>Io&L`LddE02^?Jm(!d&Z}fzp86kQ zudr~iwE$`8etw?W6$~kDAB!LKPyd0`jr*@IRlBCWYusY3_zsR#gNSj`i;L|3<0Z^g zVB`dP5mC*5Zb!i`lU8B6XDz6gHb(xLIA!5O-1{pN^hmY=_iZ~_?6PnfR{ba9D-Rh- z48uh#xeJeB65(hOxHB-+bH6CX@jXrxs5XSZ2>fjSz**(9&6&fU-{45bsI{!z$F%3i z$L%>N;vLs5)&*>KM6Fm8s_)(W%@Fc*|6#f4FfrxNuu!!i-_un&?)2?NUzxQ2dQruI z+=d2r4t$$5YD)?LMo?SB!p0t+osH%6U%OXTS(yYqW9sTeLI|{y!4HL5hEfQ$(90GK zzuNI>qU7@am~O5!WYyb}O93&f6-Se zO?y&$N;&h9#i6v0$>w@#>e2i^jWXOu=?-*F+h1<4(J)!Z$L96ICw?c@eGW^=*^?D)dn8?;}RvDz(A!Qq9WL8WO zV%cxga1bfAkDPkw_UEP~nWywD{W-e6TPL|eCN;Qn!6d0lZOfNNa4tq+qSN&pLDB%d zJrLIuK%~EW^!?K(Jk-Ys9A$PA>@ZMae(-=8n(1lpY`bmrDy>3dW?CR<@U4TUezj?;UAHk; zP{g}_`zs^i_4z#ZY%DU;T6a`@X1?#le|$4;dS8jz*9f3ZP1#hay5>r#H%Z`oxTh~cd-%eo?cbQCzdWwzlGA2!^+A_WqoR8U+MwQB|i`U?{{QfO= zuhmbMB!wdGx3Qxu!@FILY9?wQD>9Y%h)u(Rg42F)bMn2f%EHzKLKdEzbpw9Q{Lx~1efW%^N_J5Ch4?#avdm{4*03W48@hQ%AG5I?NzF?)De zD;p*F6ouGW?jvxbx~=Gcy&)ndZe87Vh6a)<7|2XQN?KyTK?+lRfp(%!^ZY&7Z2 z77CtQ;bXYAYUUK(w^_fNnEY_KS0|mx+~WEF*YKjiQenM)%lBGPrp{CV1D#Nav04a& z!YAv`To%?{)yC<}-93j;^bTcEh^l=vYz@s&s3?gkDVMTT@twif&9uyi{N%5?E8n)gjGK-bXXM1I?oXsZ{%T9uVz}g4X2H%S!0lEekAuo*! zDhgiB^sPF54v1>Ly40CB;?KIN7|@mMI>E|PvL z5XsUPv8~4XaWpGv%r%l4`9-L_vgWVng&5vVqC{PGgM%bBT7kirsxXlyN+yvZA6j}4 zUG|BZ=T1Ux`uYaX@;p&z9&z&WIeZQ!dKo)2* z4^#-?I1fWahKkJ2&H_U~1F9uGP%RC?1BGpU4k2_(h)6)hJ4A!r6~`=TgyEMj&o}9C zyK;9Axv;-0dx3jjnM!2JXKm)?|7b`g1nO4_LC8$8c+nSlQOWLbp%_D)pqc}y(hrXY z1y4_bWlRegN`~@}UIcBs7EBbL2#X6%(}%jc`F($%X?*1wz`yW--*VW83_yynWv zaVoc5Ho~u9U2s=DgHw_9m5gNZi%I>vBcT%}+%ejm0{=1Bf}Pg@{6|L~l#I!|Z-hP^ zXsiOxtXFpU{N}TZDOMMvj)0cJi|H90O!Y-xLFen9n##>Qo)o!M^dK2lMrh|lB+Lt& z{*qKghLXTdLZS2#KShG_8%BkEt>ooIEA`coulwkh#d4M5iXD<=S#uLp)Hr6=?ES+-$s(p$WRAOadd#sqy9D$#*68LYK z6bHj>ABG)6-Utq9r8c5miU#j1Jm6J~b57sQ0}2I?S?_?M*zSmvETuD+hvls93T!pz zFW(HOHH1w!xVReMdKbj4-H$Vw?Vm2+w2@)7Z-VnQ)LZ)ymrV4#Y>O#1iL&uk?XAX` zah6gPyKyQ--8S}h`C|P?p7qGTMjkTIsr26Hr>$_^lpZpI2Lqt z+iFY*sOQFxjXkh;bc6wBJgV6u>d@RaIX!Lmf>y{9h72GM{25SML`S}RGyqTY)x#1$ zA0NfbmoJ;e3O}OvNdM%Q!k6>FPjz8X>Zf~#Gb4uc6I@(h>YMRHvkr}Pc49hW_uQ4M zS_2x6%Xexk)q*z->|RiJS*2-li(dkH-GSw#Gf$f@&l%2V_vHoyYsBqJvvOhJ2j#VJ zU@-YT;o`L^O^+~+p)Dk;8Ye-j=u{z5tfpnb-51qa!04B;rSFqeWAu0d{uS6ciM9_l77s+^_U{yWc?Gy?fU( zf3{zG<>g{e5c>k5o{;m6Y-G^TRQIahQ{xy|cE)s>MbO@g}mG zQQX(Qa3a??7gQH)_wS18xon!u0zK>~{qyP(E7O4f^oz=NW|q>rTdxVTqS_9I`ukc0j&9x~G$crGWW zS*s#>BsCcgQc!XE5&0~=9%AuSq8fH~c8v`U(S?PK{GDngV~fGTX!X7a4kSVK!`58* z>7%3folh-|eU?;q6yKLA`zn9__bt_Xr|(jlEAtLe95LMzx$)1b5~Pe%%wFAwVFD!IV9W?y9>qd}iFJ1|Vc~q0qBePX`ID?oKl$qEM*)Xhw0cL2OgDNx zXN&H0BO4nVpMo#rejXoBh3)_oWO8oqrF=XUoVnP`!6^Pe?TqgG`uegEVZk-Jyt})5 zvfPt|eHF0E;dN#46J}~udR~UrSUI@GL?TtKApuUkIMQVBfnul_0k%i&E?qGtJC+6IwO+LZa?d~YsLz4 zs`~og>FH@5wAm)#iQmWmL%SkitWH)q19f2Ak(TXkZeZq5k`<_^sw!9Of|wzd7F!DT zTY=F=XG1sRoS##>&ZcAm&$+_3PhOnY57ZLai+vzXEm8^U-XocFgT&iW=zICYg}e9TgiiapFpT*Ns_)f@aoe}xyyNDwH?G2=m3^hw0xcm6@0*O6R@m!r^k|eRn``GW{c0?`_URT@1qu!M5rucEs zD9jJ?Z{G?e$Lmd)BsQ8xQ^xfjy)qk@s|yTvp5-$gRBbeR?(~Sm<>huTy^O7|O2*h$me9YHqwYGR70ouVXYVMlSFKyV zH2jF8ST-GFm-q0BdPxYU${}m6COiCTbl`m(D%IMS75ZnLzir|0u3zez!5{XqJ&Uue z*}vsd{FUuQJ!Z*x_o)#wv)pQ`d+YzX6+uJMwgmt>^5!>_Hji~b8iH~>VHfw%z<@K6 zEF$-@(u;R!aIja%)z-pdMb7t$zNdsJYS6%dGDZ;C?9rbd&}Ts`ezqO2V0yDCp9k#s zCx;qHfl}k*;>Zv!XE9qjs~qN8hUM8hUUi|2-6qDoi)3 zo{@(Cbfk9ji!ouBsi`R`G# zfY(7<78Yo4P*c;#-b6**e0*$)UAgT$UP$ln*f9B%mQnq+nsFKJRd{4cjmbp@p8sc; zaM~7*(z(D9ZXRzOi@~EKTmk}Jv2tjq0XaG1fpM^ju)vewYxhxuz|)L6)L1M8EVY|| z{e!;pW-%ipV{&SW7O}3GuKMudE$opz;g0r!R@?Gt=!EV0 z+(2q|OErF=#Fl?2P21=(o3A|oRyIXf^{;<)-&G%U=`M3zzU*=={-3)B!!9ASar_~2 ziA*8UiAeUPHambD@GHQ1bUA0<>g`+M5%)WM8t`W{=QRX4MkkA;jK43TwVWOwRQe^5 ziW$*QxU*2>C$x6ozm6O>9n2Lt1|Cm)ftUKowG77P&Ye4MuxJJaDKN%xj9ygBk^h3= z47Yg7erI?XAHqYo1_LO2b&@URWMxUJ+zwQaSuUaMTs6HW{EyM|%F<^#`(l@6W5Tsu zN!fpp|33$M4b<1kq!n3Vxe?%`L841!C|E-gUNw^7uDI<=AvE0a=f73YH6blahf_+0gFz&VKilAmt4?IW@15O6 zAaDePeLIOdqmr?f{?`-h&V?E#U-MT77*v-VyHw}Qx*~i29z{ceBkCrj-{UD+4kM2P z2BoL|x___=%OKV)I<&-~ke03t4HZj(?^~M1TzL{i5sRT%I1+m4?8gtb_cb-dhzM*p z2(HQH|G28xW%MlEWj}p{1bpU6)29EBo`HcZk*v;>*;xZaU0n@D*173vw2`l)5I(vA zd~~IE%}7jCRJKrqcKuK2C*maX*$)T#+P(i?l!+dTaMacLs^`mjZ<3#k`)oY_@5V3G z8di;)$8Z;FT+y`yWA7$>13scb*HY;Eo-lZIZkTZZ&att%c?hD(if*f$Gaa#!MSfXS zy;JjW(M7zr5U>X|H8pkG>^z8w&u?qFvTlFIh>V z1%-r65ZJ$TygRUc+!Y&rhbkf*ahs z2bvm@EW>uvWA!%)aCgK+MEKzD;lw;qCB&zurY;z^&K;0l+25U8|5icW z_PEDUTUl?nTNFR@|2!!(Hg86&F;YlmSIVkD5Ve$sVhlDATxd)GH(=6x?G6Y)I8#`p znll0-*r{Cr+I!S{{IEVIx8+H5|BR9t0lL z+tYL6j-X(7c1C*o0whE5g)#9Eu>q3CX-oc-DVu7#7| zqBXK55}!VWE@`|dXjw>v5sY&DLDhupIm`eqDk>6$@YkWHuf}oy8y5~E?qF7y6amH| zlhdOoE&yYa_f#Da%5#^$3^xeebnkcP`JFU zOqld2#Ah8vMN`Yf>a&UWYiPBK>?pD30nK^#a? zFK#fSUhaIIYdF}H!z|A^6sG_hYPr0SvL=pa&IVMyi!YVeaN!uK#RJ}G;9p1H z!J3i7i^FmJv*vn1S})Q9i4yE%dxzd4bBSW%li}4h+M6%^ht8`7%=cfeZ?ONFj0M5? zi$!far!lci2X^S`NCjRu8yWgG;vWB1#_B(ZvsB>h;jxdxwW9`x7Pho{96l)5tKqgi zo&L!WNy^dh-$Xg*C#zs|pTactDVc^@OJs6#(%6`R-O=8@@x8F}V!a1YooFK>#wR9< z$83jTP!B{#KYu=gdk!u$4@RZxooG@9PL5o&GcuMu=e)b#l$KuKR+*ceMDVX?-Wi|6e7mbA#zdzmZD{Q3 zngp%gAQc6`Nd86s#LynPBN7k&zE{Yy0w7Jg7aksN&7Xll54{Pr=t8K1hWIqiwQt?f z+SB|HBh}hP+FW}wLVQ;BDIVi4!D;<~(+Rm6N9dNwrhCRhZ7WU^x;jy_121LS_4xxe zwlGeW?+6z`+|X^q4>SyY)w|T6lz*h)+*9J`viw~)zgeKk7Q6CwF!p~@tQb{r>sPD! z(_fy{Bv_ZAO%Q?*^3vOjg5PK``<(=rX)y7^Xzfa!*T-)qktPLh1ydUX9@0`5&hvJN z_OMr9uN(X3%q7T1v>THJ1O(uHn)v!=AYX?IpN+eEkMa!b3O?fGco_=Z68 zlTH7Z=Ye|`<@4_n0U?P!yKlC@xntN1skPaY3VqM)e|OOu0RjkoNrbwe&x3$Q)h?m zT@F!Y%Xc*~MBB)ys4Jd2z8DLl5-ea~XmpaH27wEDo|UzS1f@r&uN@w0Z#*2c9lP_7 z?^eg9J~IvU_6BjU{1Trfzi|O=t5eK2lbyeBj`dk)?nZ60AXPr~Rl}O=aT7{vTk34Y zMm5))`IQKhcQ0R?K43n*9E1K1SNsHTz>;v!$KTXDXU{$UKX1RCxHVd&dcH(`zD<4J z#h{SL7ve~O{urQ=$nh@>3ScHJ-I@#qpf!5F-W?mz1Ex4LSHPy|QC%yi_;9Jk%=_+K zU(<0Bh_Z9Vg-E}Drs}?WIv9@_xPrO{1}iL4EI87?_I!E|`~!BDuiWC`7y)@00L>u6 z9qaGMfiudBKp@rL4@m`z1Q6wV*O!ja_FslJq%pBKTuu2m8c+@prCTRCj={{Zu_HHA zN}YgEJPinS^uA1ib0Pmd&O$AvnbPX^QrG!(&Ph+si4izsvM{D7x*O;ycBQf0VMz=o z$jkZ7%FSaOq!|+WqkBew5+T$BB*Qx}&QB?k*3?wZ=siiydPstC?Z;O^{Zz`^<9c6^{;W>iNfnVHCt+0d?FoZ0=-hY(G|2wli| zl#4YN0e^zQ6&V1tL3ne4wpCYDBvRhE!5UMRbI9bwj5fi*0w#3p+czeLxE=Ti{%QE9 z#}x!fCOakV0L%OA*zc`IRjXs{QHs?Vt_`7>|s z1~l8hPH4v5KEYeV8b=QryvfYe*isT0q;W8~0U$X$+l=qr+1OJlRlDBu{ZsR9@cEk& z&Nmq`_!MNMH`q8_-w8H*I-gpIdvXt{i!!geZ}YNG2lebLzdxSgXn1F{qTg?|Q@>-H zp``k(##4hT>C2(TAKF07ZuigHCJY5Mm zlTUKhIXT z=P@l?XBWfg$2g3{TkCBNRw{LN#JI_!!~d&932+9IYH7 z3R7}#GtLf->{!H@zn2@e$&z@+Ly@r0y z^|g)l)T?&}8gA_E@MN{HlSA|JwmAuX#%MSZ0Y1@*Yk6TAx?dt9Zo;uKw?V`YPfij@ zKyQN9X7#*j#M?9>$quupMMXvWg$C0{m^fZv|CXf|g9u7YOe}rlbKYB-=2Dfbj^W32 zog)>-py!U#)zQ%tN^%&T+7!F%lbZZrP~Poc$mHxY4U|KdH zz|A`6G2$Jb(V22bK7;%#V?2W}+o7L`97|j@ip;BMHL^SWO#hy0ut%-30*m8-tZtG& zQNF-`(bdR2En@QNJ5joB-Ucg`C~M7B-g73jxK-W_VG~7zFZPJ9#!3Rv_&n?|KFr$u zys0sp%~K?!J5aogV)%+R!IM?e1we zkmsUN>TxHSO9p*^(@X)JznH%H zh*KWgqucjDpJUE&eA$Els;wiQb3xp+Nknj<2asv)z>TiyPVYH z`xucxYX%{ffCSTu&a6I@v)r^c?~O+7S1uizl2*;XFQy*HFK}=7N)C{gUO(hKry2Ju z^rT(x)9unhV|7$#@|{&4zsT%L8!u+&^1+eKuxTHy*Xr#GpG9nPPD8qV*^@;M+KhqR z2&bcvbJUaJEEgIT8v9enj*93jYv;i$thP>cZdkMtMTs$#Vgs}!jQPhpk zK@JAT9yEFiy0fw_6@7mUoP9+1aHC=9;qJ9q*xE^4V?K3M{(C3JwxRmNB>>(;jqrU zcNtl)D=7CGB|quKLOg$!@1GL!0vzHV6E|*H!t_7q7d~8X2qdx_%jalAi`)h}*4t z3L^U&F$m!}=n>U1czA(qkx z=D5`dV9SgXVznM-RkCC)&8}!+a!xYjKg}NyJsT2H-qUdwA1SKp7|;}dSK#ICcds9o zwOkC9GD&rvyG%f4KoxN4;Frfzy*Mi)oJ4P90FO{jI|15q=;k`oceeX0a zr{TS{?&!A=jXYv9(LDXx5j>cg93y9iHGep8lX;lOHZtbE9rV?*s#GRMAayWmgu_v@ z$ae*4?OqC4e-rf^i(B~MjaJDNq3u)5|Gw!TuILJ^KLG_&G-{8uH?((goX){+P(vUq zK_6<(Wl4tMAlucnxuk~h1uDkpm#Fx?cY0s*lVZ8Kk%iyNB-gkj_0G48n6!LpV-0fB zFT-wl;?bHJJ?1gftQb;EPV7_eK^S2PpWAwe)+r8?pPn;tW{A|l!NU~EvvoYe z4xIT_p;{El#D%GKyQ<9bjrprR)@0&>*M-GeLRSS+bh$m zi%F$X9cK96-&~YgV%l}HmFXLF2ZpWNi=N-WEZS@2=<^P4VqEo`wDQL`aaFU~wX=8} zt93Bfm->suji>*~ZpOEpZ zIM3aG>%^1dA!|hK13`C4uBD^3A!R8JT>oeXIOF8@oYXOdle4p>qoZb^$jKWkD=Hg~ zQ^sd>O_Cp)m|XhHXaK$wRDR-!{o?6RP^LWc0OGNmo2XoZZp&R8=vDxpG;_Wsu45e2Q zb$J%cxU-vQxJD6tS8?k!A3w`cVVy;wE3nS#RwOe<{`NV040=_?_yo2H$+>&icn@A8 z#V$F#7)j~;cAe#hXK~y0cY1pBG=VEdopJV&m&G*Ark0)jM9d8aRY=y6Z={4+Gv&TV z*Qn6wF{BJVnb1tSV>Fp#YVx)M?_PhbZQw(g*^ow5d$vK!Be^S7c~ZoJCQ{Peu+Hhh z&#R@9CrnB?ecGp0@dxBz``*)(KDr{pdnLwLz_EF`HnYme>}%rI9Nv0VqJscauZ{;d z0!5IEj*;16`Sa0WE=xO*`|>g>>D<_pN6-axojV>P&&v2gKOe~I@eG}9tk0#RqbhDC z>^vOp1p?jKzDuV#;&^Jf&dy4;uAGxbU63CykNz&&kT^?kGZo@w3a&Lih)$ICvUnc- zNh?&U@MU!|-rmq*EBxajMY!Z(K=%ZHc&R;)DV_Tr85w-}`s4a*w0A)$q3G^(PuTMx zjgrvYl|*Dy$4N!$ojV)$bwY`Sc5x>x^V08|Jud#H=qO#+K*JUOUY*RW!Yq(eK2G+U zczpWzWi~mhkjPtdiqyK{RL)Crs5qXH>MsQAs%PpA3Wa{Mld1*}%SdAOE4G?O1{X?8 zT=KN>tsF7mNMz#mMt;WNlGUn(cm`Q+R)AZr57=Oe=93jTx1aoPEu$zDVaZ&aJq6Ba zz`=9$!wNaC`AD;iF`vshEiMi@ijESR2XW%nyRi46Dq7$dH6`E*C?k}v+Qmaz0OL+k zRtZK(kVyOQ?FN8NO+9(SZ`1R~=)*??2Pt%je!a6JZ|}6)$Rk%?OaxM$<-r)SjTPM7 zet6XtruOhEYk>uqg&njZ?jfr6WhVW#$%5z3NbFQ-M2=Tw)-XKx2}NyE6n}aLQm>UW z$!(TlHx~@ATmguzc={@Kx#nul&b)Bo%khTihWblgQ%DBjx+t9Sm-N4ULF&RA%05DhPKH5gIijvK z|EA-}W4!#?h=bK!_hagq%#F&|i zH6Ab~Xyj;ZLh>)TEC?-p{rv1mQQjz50k#Ljs8+m2h9)srI$DPA05#-{fkx~FVe#l` zQJf5z+{GzF;}u9?g06Mr@A;>Ig|UgY(tM})7W56s-Vw=HmUr4%<1urZmaLs^c>bJ( z78C7a+hS%mYyS1r_un+bEgDjtlP=EUmv6=M#10L;As~$?V($ywd6=AsuBxAe?laQ) zL6fic!%kW;rsAX40s$EngZKGQ5BK7ED=5LgUSo`LhSsu` z!Dem&w)i4uCjHb+$mPFyJ=P9BZ9ef)>%SSK0%O=a{PqMtLuM$5Kx*ei0kP;Z1? zTeakVSqZvLx+kOgG!x8C|8Kq?Ber)LiuYExH2QIe0GW!20YWv*{f?TbynTXJKRWTx zY4tc(+^W`AR($^bEBj4B)>mUhubL63)bgG+=o#?S9Vmn57SOSVM@PBo185>AhEwlP zqFXACc0$LC5dn;(w@|M_4sQwM2_SaSWSQRrBEj-5l@xQ~ixjj0pa?zyDD1QN_)}mS z2JL~gj0~%pck)*K3+eF~?5_!)5{g*uxWbfllkV=WXds~q%ZMm8y}#@E)`0$9g_~#s zD(WKlw<{OJd*w#ll#-njo{Y_-u2}P*V(x4`pmAAh(yS%Dl~=(j)BmwGKki%Cqi+edCPKz{lN3jh1Fxr#`-@_vwpS9T>FClCHMa;msmPBl-z;&JB;^yGTP^x{AZ9H@;D^`5`f=PG1$2(51%M|Txeyp$C#JJnuPkfkFaa;TwBaG^E3t`cJ$^9Siu_*8 zqRgU6djJ8ck;V=DW#LQfV5lk52KIgi;2p`<0*F(hNYbn@cWSxzJH4P#4}&`MUs4a= zCzO{Y#D?=5VJR44Yr94BB7c+7$(-nuylp~tdi*d?ULeHsgRS6bC_fJ|En1w}>ZY=K zU|Mdlp+T{K#_iGelSOev_eIIlxyeIR9%oqtF&bP^-dfJXBh(D^-hcA*y{>Llp{Ha< z*q7ne%zh<$6$_!W5zsBw%sE*;I0qj%U^0o*#-=ElM_sGXKz#nzvpFz}{jswXFyJIv zqn|H=@L&IEE&-#gVQ&4A{l<$32}2bE=FIfC_oM{ug%KL`DYl(gPl#9enERlfMeO7G zFaRA2E?}y>tg5Ws1g?S)0Kfa}vn(vN4%-Z@A|O!#PXU_1lwZH@DBBJi107dCKd6xF zyJ5h0_f$gEAF+uiM{sH!9eX>Z%0F%CGTS5O+w1lQ_c!EU*37L9c|s#>+LrzRXVsiz zF`lvAG1oxgy#bOk+?8p-yYuUY`nNMAYdQaCqKD^S31|vuux%`qZP@!gw27>_&z~`K zF`fJ>bi3wO`V=2w-)Cv@y7KixniG?aLIJKVP?qcyXO2n`Gn97}8w8*-zC{{zPpJRCSDkk2CLc>7006TcvhJ>_T_< ztrH($VV+j`aQ{jttM!z0B@^GJkLINtYXO;W^shu)C;H{@*x*S>4(8ZIKj2*<^i}|e zOyYdVA8_xcQAvk5{^-lwULtRzBIPE@zyta)w2;C@_rH2oeG3WUFRa>)zO zjWZbiFiEn%ot4fc?(zHFhXSuNG|@H%cc|GO_t*(KQMJ3uZtNZv z#VL@ex}KMkGndsp84r{%>p?Sn#1etGAAa>E+zJIW~g zt=ImS_~sC%l$0}w+%*vlTJVZ_(r1~$4&eDbFflo}UrdN1AVq`zAjb{gH9EwTCr=uI zG$VNM&#zyhK*FL4($UcYwZIc-@ERA|AwKkRdwuJTmy|>@p_7))dOiNsKgVtfqhg*D zE-+4A{A@Av&j!xeywMdS!Jz)Pl290U|6itsZnU~t`^F8+{w>uPx0sI*EAH;5()D?n zb*DM!7O(IQMaU=Jz;z)_G)L?8=ov%KKlh);z6B)MLaAg;}eZ#(EU)j5SlNQajArxJ`9y`2uWWXz=ml z95GjpS-wxW&E5{kjchi24ph7X(3!L zPS?p#&KbUue?OQ+otot>ej&~r>m+#j^qmv5l|L>W*|o7fXDv;4K%@B;Y)pVK{Jp&9 z-gM1AkX(gDTxq{pRvt3^i>eSQYHWrV281J;>1>Raz`aqPGhC%KnU!cdm4w~dZ+6!* zQXQPsA?$m)yT{SH%zMrvIq;uD%GEL2&$-@0%OTqO8qUa)?!l<(9Tghp3yYr)rEu|P zL|1-zy?50)RzY#l-YfN+9PUw}_^o!pxl8)y z^P0?90DujIHEq7Ru{W>EUw(HG!CEK*_uO)_uyhOW`3b9u+Qi_1maC7GUkNam7cw$C z1q|_ir96v~E+C{4Yxq4A-M*5@!sRst%iavaeO4Zu;NB$m2V2V9L3<);Xdx$A!SYJ@!FgCfw;kXa!RfbW3k z?NPu%NfQm4AOg7cdrKSfx=kJQK&WXprA<#Q8>%&>NJ}x>dfA};@-8#-^C^#qu_a(%xm zi+s?v%hD_>Q>H!*zkbSVxF(4g8Q?P-HzrH2*ktrh|9suD;67fxUEVf3jL1sC4> zKEPcL5$gT+;y#HwrTrh)SE19afI8~KX_`Qy3jSRGEW%SKi0looNJi?>Wc9e`=*KD* zi0KOb7*leUW`DfwbF}%j{?9fD0$vY8zr3*g(C_mOC>lV#1-i4~cIJ#jQmn6;hL&b2 z?IGaif*Nw}yM1IIV102s9agrv6Yg}u2xwdKICGUq1B=ivBdH=jK#}gi0Et}Ie5P3> zI}nSDQhiR{2^^GK(JCH#{w+^G|E9p3?@^)9Cxg_fivdkIpcT0itrIC$vx*`|M1(CA^R)5HUY&IlKSMe_R}i?il>};m?p_^4#VFaivzKl zvsZ)70vU~hb1U15#?7IsmRT2hS*|O~1nIxB@`{G6rg476H}l`1Pmm6OuXRGRU;5L$ zhsG2^V1rNhTaWn^{B6;<#pd&z}18SKly9@kfBNmE{v9X&RxG1$<^cb?)U7pj+8;PXVt9Q4RG6d?jJ` z6kH}T&T#C%fHEGqHo(^D?(2))0^VZIWB~c0FX%_Qfn6kUVS(kwSr`7H_dc#JF8{)a zv4XoZ78sDuR5Ud7KpO$5i;y(?uKi-ZCM;}U9D)D~)78L@vdp_>BRrHNE5IW|$RqyM z&p#pl5w#g&z7(+J5s`JYONf#=ctOC^odDxW?Om~SIjC-(VzzLWH3gAofoJio89*W zl73L*t*^?kBToE9R)HqHoJtQ!>SdPNsej^_eoD+|_)E6g1eZ8nUvXG9>x!$|dANkP zvYUujR~*$$aVM2~OUhbAzuvvTV$wQY>39C8;Q8FGt0ujlZPfBv-^lr%v^Z}bJ@BIP zythN;i$M=v^V|O_4fv$0<8;3z3p6yvPm5*_J#+JWr*QdcD?q1$HZgvD_FEFKyN=e_ z_ZG*zoJVAteKhNs|IBg8&(C?g4nYf%m71v52s;^xWJ$NII~5_K-fi(fE8 zw_jho`2}PJ-=G0qe=W{M-zd)2S7STJOZ}Yj!lJ~SgJh>S^S>+Wy-Ata%wqJ|)zb`u z#U;8|%=a^_g9tAw9y!hD!O@nR^n}@S>Qo^(bpc)ijUa z|95~_t-&Jv@ayz;gF=IgL(pUCAl!b}IPSvIzq^m?_-{bAhYBXDhjuQj%? z{tfEyPu11GI^OvkJ6>LXk(}~-U3Os4W~Sf=loR6P7Y*5m>aW?<^5T+WW!xAo=5TgzLm%k#KRAR|<73o_eG}5;y@pGjjr@I*o*OQaeSVD>N%1*rzZHDxoqSv$o z10+r+5S1vr#hJJ*rKf&<$n(p!+Ix1C&&M4t+odxyyb~{c={G3!dREvGeNuNYb{47m zwZY=~o0#vs%Cx!y-8cjHpF+7|Sc;#_>{zY0TU3Opab4mx@+8go&empb^O5z=Z?!R+l*1FeLPv;u{QyGca?Ehyp^E;FP;b*ecP#9yXO$r$Pr}rvH((= zJ#OYAMI&&-T!38a4|cGz(y^7-R?-sA(9(FzUih})`Qe+h{_pfjT0Eue=e>M;DVm!T zl5$LSu2rdLkuvDPHRB zlP##~#f{PhA+I|M`gRivCEERMZdzg!uxc1y9?-10dH>@A0K=}7;25|&yd#iF4D7`nqcC!zPyK%%OTVgX4!g4r+LyCKb@0e0 zNf9EEfXf$jLpCFn)WvgzsBUD*>OMMH|LGHm$hvoT73SvV7!jeb_c{BAgW$Px$ouT8 zF@S)r1RyGKy>Kg3&lR1+Me2HoZ+1sQV=KbDl=v+tr0XN|UkFohn z=XnMtw4x-R(I>Ti*2%_&NiDtnbXtCC-T$Yb!BE--*5c-u?nHth+fmHzEQ4Uusa)Yc z+|{cg8`>Jyljw0 zlADrbIG{(-F)^az;?LnlEE==E+Wu(1qcL9V88KT6ZOQu|zk@Xb*Ya*zvAO~jQi8J` z-bl>-E3rPuG`9XKr%L6Iq`AI^`opd)3>x>@9OF=#k3wcmfe{+-Mx5*@z*)0LPx+4% z!9qOK41=GDaSs4CpjX|fCo|smo)B^qQ%u~b?^bo_0yY4ic%{Q|l!Fy%yM-x^=UL`a z#H}V|mv!;K(Kx-&hSaXY|AT0As-oU(YloJ8n{q_G^T4Pmqtb@FZq7xcQg;xU{d0oW zi5yd&HGIV8ooT7tMQb5Nvl-tSQ(nYtSpv1og#h;oAGcefZeqo>#x$2nVKc}!P5XRa z(oRL!8yPNVE8Hz+d!E9vc!jD3J_l!XD{n`0hTnf-qE?LO>Yo2DNGu~47mz?&Xm`_s zlpT^41z^fHf^iftmv`MmGgDLLyn&wp4*}!x2>b)o_B7|J>&~DnkgY`kF4Z3@aP{)? zss?UDXD7q#AzRGPBg-SECVG=knL81WKs|lbo91TDJE6^q20tIjON51mn^~2-m&Rxi zWmQ#bjr=Yk1BpGLime3D6rz~%LGqO9oXY2wf0oSK%y*J5`2Spjz5|M9W4G58bjXcYM_6VoJaoxM#jrf za9(#J{4*#E$_&btd~0_WdGx<*2M(y&{l1)bG?(BOs7C4X^>@GF+~l*JaqWs`MtrM* z+>Ug9`W+=_)2XtHp5-@Q;~HMAZhU$(SAQeBNaLZujsjlt#4KsxrDXYCmQKvD>xT3f zIgc+fr;HtmNRIThv`+-2@$vFlswL}h0xolv&9#1?9s<$rM!;4Ax`$WgZVQG4EWQDt zExVj)j{P~=8#iXT=pcYvBatKQ z?Ccg^UW4$@uOK;Y&J`wLI6lVnTnw9)QB=fn`v0N1tZt?M>-_2`b-E4(fjbZ)=&)*t9f%GzNT31(I+n4c zhr9dx{^a5nL5SRgmJMbxmsRr!$lsuk!L!>3xZH3=fn7#rv18%M)d_un{AO!Mfr4=@9|W}vs5a9^+kiRn z;?&`w`yZw>a&ALlEXNbI)}TBAhKE27NP2&-`xYeh_Xnz3mU`UVahELp{Lq_ODnXj~ zp)YAh&=TVh2SD@nTDmP0nD)6t>o`BFW#o-l>_+kT$A1FMM^*P9^^YG2V@J60*D}|k zWx%8UV6?{IV+v&bZ!``WJBGoaOo6a7S0y#g7Wb(Z`MY=;hp(&fFsQe1TXk=vWvR8$(lg`%6P7Kud5z`zMmd~hL>Q&R4BYps~WSq5+FO_%Lf@!mRVBPq%C;jqZ8l(OM5KTC6T z3wbXy_7k;da&POw=Fwa7iRKcMdri70UMX-WBEwNTA=nUO;*?`(G0c>yF}!;rx&xZ{ z7Y@%nF>PUe&eQi7>9s50eCjG*e|#l)GTX0`#a*GTbS!f&N`8LVwldP{QcOsFb+_h- zUFj$46oGdf+d2BDbVWhqPDPAW*FQDi2G>wR;e#yB%)cu-UtVvlPO3Y9;j`4)l_~Sx zuD#`Jg%waL%Z77z_wvx@7o)^O# zgBJiBcOKfOKxAHa-JNqy-JfQ|gQ)ggIIFPzM+xzzKBTv|x25bg4JZ*K7jXzsuFtQn z-Iq_oDyJm$34geVrzmHBY8h2^la4O9tdexbJ{C=lD7j1IM zVI~knI_@>yoPnE`c1dHJOthcV<-8L_+wMHfaYBIB<;&<4T=q2qGyv*>CtbxRqvMXV zI+w$KXln}zP|Q?a1lI)6c!8v-dH*NN>mH)ybnBvc-^3DNv(7a$Dc+|j^bW~PqQnj_ zu7E=C`xEKw6gQigiygSYC1}z|j0jA3EaCm@D)G+slsR--)jum&lDf7b?}tah#&#-> z2_YyXWcEu}-5M`wEhn4{Jx^dF-~TJ;NWyMkCgAy9?*VkJsK|8AzH*#W*@sB}K&MXBJH3{`wcL2HFt{6R=u2vX4Tb<~7aVoA0h9rBs#7 zfZh|3XxqtYX-PniA#$6UnW=6mN%34Cg8UIU&B=foL6jQt039MD0-z-+Ny#A7$kMUe z5&F)7PH@KKv3ROC30!Z;NLB7CbMtozv#G~@1&0Qpt;dO~(ms+97gyR(&C@odEA00D`awv6Kw#RQ@S zm33D>XG};YwP#|g&E(0d8D|-0#L#&;b-&r33P1Lc>#=pVovdm=r>Gq{Ik=FeL`l$R z*$N!>-FnV6y2-LPg{1=1Rx?sGyFw?Z@7Z-Y&|pP8@$q@Y@G=;*7SOa&b=1CfA{m;N z{RSONWd>e`~Z}_RA*3?kf7oW<4OT3gzxS{5FJ-PZUi7kD*P27Ql$3SNj?F&4d;amZooZ>#=; zNfK>CxL9d$<;=wRcu&TeIwDDb0$hzZ3TDo=moT&O=u#ZXZm=l8Xkpw*a{kAUn=mE{ z4TIV%-Z-W4ja&xC!USk!1@JCO>WfQD?;#7>*r)`n2O|u`8V$*L{pfOA5YE7-3xpdA zBaDE~5|q-VlBIZIbGrsJoEb9ddVjlqv^*afG&SwNFA>>jpT(^}g5EtbVKyiQGkNPH zVPRDcsQW>*g}GGFomiQ>wz6X#69PT!245F)$@rkyT(*u^VEh)3qF0XYg#W7c`r?TA z@NIPkl={`xc9*Li9{LTlDgNj%Jpm#w<%@kC2gUL3={^vJRgkEi87NBN{}Yr@BNP;M z!x`|vI10AB+y?QZBc9LN`N7BU)p+7bggj(yisTwx3fxpJ3>-d-^L262DK04Z1IqSA zits*zBSo!rHiePP#BgUc^zWID7fryD8%RipFGmZCiuyY`!uH<}6ym=?W@8ii4k)Vd zDEIHV-}ox`6m+s-RNcOQ98MEAUoLYPVMfAYtKiqBM^p%^ zx7*{vZ8Lu$b)dK@|B*v&u33c`C(k@g+Y?&gvhGfPD=pCT52ow9R*`pYL;gj{#WlnA z+=lRB*;>gSO|mIZ#>QgA;(6SjyY6qdI?CzdM6*r$xVviy+2=jnAfDG6!FpvYj)S4BGBf5KgQ$hRkRq)!iSp6%p%^?uJ zLO6GRsqLFNoZD0MBd^Z1dCrFI`U&Y3SkV}5-U>Ql`8rtO&SPc{6EUatd%A0M8E(Sg zT&~q#dYg=GJlfuZuUEYJ0sDiCr2aPW&ynx~*-Sc>GtauwQm0bB_S}=gEmL=3M3JM7ORsne zW1an>hTW}&&EFr35eUcw1-qt>vIvPFW-}z#el#6irTf>*W&6uc;PvyfUUk~J4@tW< z_$OHNLW9^lJiPdaMn=nEMX|3^`BJB_;m!{tZApPa>tu}XNYTiU1F>nXYLawMHr)%C zU`3WK#_q}j46l}TSFF9OAqqxOnYJIF+f1>hzd7_+Vs@l$#^+)6lV{z5DL5FAaCKF> zqE2@rz|$3FfX($k9*-OL6jz-0gIg)aY(1WIdy;we$a;G~{aE`~+Ee}W<}`8&{bWLY0R;{e z-;I;NEIM+4hE(%sm~d$bYr6M(!MteC{>cvX`nD;VdouXSC_^*kOU%xON#N`oL#RmF zx0`~wW9bgw6icz;OMi*0B)XN2Wcw>9%8<2-W`L37GEv{LbW_ai{kaaaRo{sO14F}o zR{_DZ9hJ^oYXiD8i-+Q`bxpzTrirC62JBJ@-xk)tlnT~9`5DY{JY;x$e4UvY9y2&J zF*C#Hll$07KE7p17|;0?MG@Yk?v<3Dlr})HO3PdLr)_L2aAL>?=OZGHw*PK+D<8d4 zztccc7;>h&_~OLW)ANqRj@Y_(G#`sk0$X1_$*h;^N-kaU9Pg?d5>*_3iE8N5G0Bh* zSQjGc47q)E_p3?CD@VQXY*--X`1!7->UVky+E>jgs7}~2Es*b0C5fvk;~Tryj)O9& z!1l3JzCi=VzgtP#1o4sal7#WMBP1jiOee1`LbMYzrlz(p!af`Q+#4L$ZM6goe6K zVxlZWKAZ}p1zT1Sr5<_SgaG1z6GuRWGg>W+S+47sGWR>V^y7kEpTW9O4VXLlxqvZD02`@(HQI|ou*BBrDV&y&2v{Z>nVGc#G$49Ds@wVx#90am zP#`l(7A&i&$%lpF%n{S>L#!Cu=O=7$;9XY%6u?)Kuhqd3ubTDlT36~?J9y|}Ct(AEv`}A`1{(WLVh85Fpy+o!$A!IkXzK?f|IF+uZq^hNQX7vTFQmn3@hflsakhy!^ zp?UEmvhe%kpD>{OJ7Qn_3>+=>PkUZ{<1QiC7XN;W% zJE@zlV?S9ltUqeKwEinQDBYvHA&Dk$AO%lm0FJjXB7%dGobdhf>(#jjxMV((D^T8MQY z`8jx2)9@6Z6e?2cyCu&@weh^X1&=Tg%Y>bb0plbTI8?xTUB$a%d!X1RKyRc&@?;q8 z*e!vY-Px_AzsCY{XK%Az5#FLS+&@%<9^Do26wz1;8>rk@AT8dk7&1GEAZD*#fE;Fv z=!*KdN|Bd9YXfyS3r2ZHbQ`1K^wIaVd1 zrb21}lxWo+#ZKZt)tRKl`!2z%P{P^Ol}f|KetckbG_}@!mSG8WqX^!zgd@+Nkrs+c z5<=!W@}o6Mb_rVI&XpZ*2fyL)z0D5=GtlNDDJi0Z!jm;ir@R*%%=ec08$Zf5@&`+5bgYeXHSjm zn4v?!ia_==L#c}R1558dYHL1^VHkal_U6Odr(uxzx!163-DO8qKb=3>0Q+Nm#6I&~ z-y=P-lo{9#u?|;Z7%_5ZhdilPHZr5DxO2b8gWC*#a?JUF9<&5KYS{V%H5Itp5P<`_Gr9{S;6;O#>%b%TViehJ|^pt@%8wpu6R5P zd87DYT^rDpuAB^m3o+zc8_O+ibi0WxnALejjc3M`3DqcSk`?M=Lc~`i#;D%&{g)4j z7}<}Ciio!%9vPzNG)?^Qe_U{Dt!YY2xbF@1Ms4*jTNZ=p%IJ}38Quw8=Xi_}A%raN zM#dps-HVbHQi;`y?GDbhmY(F3VS+S!25AzSFfX zJcM=3%=o<})y|24Fe4-5IyNu(qc#I>TD7;^8qo;(sj0)Y|2i}Itg(*evufeQ-!dXZ zWD;oJnhJdk#OlK8DjgAY!skAf)z0b*g$?p%)$j_~Amu%}{_p!fei$YVHVmMAFBu#q zoSwa8w-K=1nu&2cl92g^<=R_$SEgF@@R;v=YVRpe0hbF@UaZ`(s_UoKG~c!DmMj{< z^i@vB^q+%D<#i^wd^H>{esQ6_3!r*3iuMMXt1i$gg*Wm4rb4`TO_h4oq;o|B9S2om36 zlIp&@j9^#)vQHY|VyQj3Iyw3YID%TG^GCa~d>Ss;Yub8v_rk z(pAKq|Fn;%=RXhOwf2-Eewa;+-ArZcG}3yTeg6t5;p8`J{rvVjs|wV`vJSDLw=XeX z#Pa|PAsW{ghLG@+G>z??r~++sZBCkMs(Nm^19f^SK5x>WI4Tj|J`Fcf;ddbq7r%qp zU|>LQ#5m(~og^Jy>N^%cI&@WFUY*($%Zw$nrVwSRb{k=nsJ(#MrA*5W;#?1UZMmIu z0f-Sx8)E_9*f~lhIY5;&V2$|&evy`EjBwzfo*k;5>bUig|1_)zjU`VlHfZ&9&6aL0mf^}jlEiV$hKkGZtERJJ z6y7fk6_hTd+1%l zHGo3=!6-z?eX)sdhXoAH68*+3v@2Obms&-r27}?nL+&e-Z69Ov`gUBvc8}y(6V0kj zHQu{wdfc9@P=l+0%vP&z$x5?mT6%Vgw z3}V_h^2P}0kJmDmCR`=DT#zFMJ_Ab>z!H_qDf+SH9c^w%+!cAk#w>>G+Nl2#B{^X=T)MwTyv{V$6MO;MFwjl=7S^B2bJ#;1 zA9wL~8tt+XbF$-t82K{IV#UYVS^Hsw?>`5Fu6{Yd?%q7d5DEVO3+o-&o}S5aGLx}u zXC;RpZYQ;7Ijv&%mg$bn^+wkf@!CoyLmsn~CWqUl2%vsQ zf7ns~)9tCSh@ZRLYeMoeoj+eOKv0T#<9&8C(qsO?%b$7aRca2TPq>-4(+A`kxHxhi zmK9LT(!6eHj}*7t5oHZoWSqMKr)b4>Ta2np`Jx|hG_P-^ zc+}Tr@>P*eA@+?Bcf;pby__vUKh2O=MP)arU?oSQwGwd`bY0b$f1wa=&L$N>wM zdkx=b<_60q|2WyZM|SV}QWc3l-M*8jy17UTMbkgWQLCf?>Oy%%F>1+rs=+}@YHMkF zdM5Ab$VSkbmnoxkhT5J#`x$x^clAS5Z@ZgQS2Yn-rmH zb|1ED7wrk)6BhOw7bg95bn0I9W+M+J&;y0pRPd~}&Y51qDiBLHe>`XU)fUC(3oh_a zWuw4eC~ok4hs{ZZ^w|g+1VBAs0j>w4`J+O5#MaJkc7Vr(kIrcO;m>6UI+mJN3ua&Z zeJRpWW7BpP8xJ-=iIyU<-yge)u(BWg0Txv|f#wJOURH*Mg6|Jp(yhkS4w0tC3x2fO z*v+Y<;3mvKdtfVGS0UxJ66mm~E(I~ta=#kcZsDS)tg*O9nR(-Pbn52Q&X%W~e31Oz z+-pz1ptx*ndLqQ=R`xkN49VGVbXk5$doy9yeq*{i6Q6){Ge;kB!Z&nVM)*AEoS4jW zZpO!?ka`}FK1dZDxsc_6S9nz8klAj{mgdID;Ygs{86M(f|0&J!r`B{Yq`*6YJiDb> zo$0=lS-W7~Es|s}*z>ku&KYAWbP(Y(R{gH$W{JVaz8VigxqdiUs-;Z*yuu<83AB1HyNCm#y-1=fL6SS+iVu&ix^+`FX}vhpIPq#lGc^!>S@a|f36c2 zb{S)?+4=&FHFq7W@UR!YAOpnwAm@at5_C6UgXFIe1A0)*yR`G)nD@8A3EZTq9e>@P zyX^{J^Es_Vjmw=jVnDX~bT21`=B-)rDTKa8NlKIx$8!riaOS)Pm8ONrFS$pFEtX|( z8jAfU(UPs^``5W0I?zT<$e_|=_$1hm%doECGaBIP-B z503+Jz`o2#nVY$H)*a}p@MpBrS?tWr->uowk!;u*5eAHG1nfK*%v)vz&Qtw}`weCU z=qcvk4@WfK_~cZDXAFX1$jzsy3t$C;?(lPey`bUbvkxST(L;8HSN}ep76-|zhEs0s zZAJ9#yo$VVM+`4-gCldOGB;G)0vs`pu&WWkP)6KDQxO9RS7#3aGwNl7e}~uumi^}= zeanNMzwSsLko+p0PwksJmxBi~loKghcBRsP_-S^wwq&hJSX-?`VG(k3HQ|Pd@Sc$Gs z{AFYBe1*FJ*VF6)2b;1H_2-ao1OE>UdEvt#;)B)Qtr+%SQCbjk);*avH2p_uCt-E1 zjm`BdU#5SslceTfrU$^}X`lTy1X{rmjz+KK0i&@a^wbz){uMfjIKz0@L45>nM z^SsLq<+{4{{e)e<1G6O0{xi^TGtHN$r72^k?hDa&$1INY<9S2DWK-W)ByDe|+`LPu zz0=@5f0rXsDR~u!pyv2)7~f$bA`OBYC!@st=VI<>2M{M>+C9rgz9FRFXn-cwLpB{+ z0RxEFU9;8t`3RkfnT9z&+aEnXW(T_du6B>>xFE586W};!JfxNM!ygA18BfOc*ug9- zz#(&?8r+9omwmLLynMR;`=3cTx4`@kGD6@d@pqL1FqUT_a>v+lkHvg@9)17!RLOxR zHDn~I;7Fpxs-}$iy!ruY;RVbU6}@(OU0grrK;y4-y=Tu%NoVjTGuGD+{p){(b5W4^ z(P#OB{mM)7juG+u!a}tdab2^wbh^o+5%HA&G%zh4gWIMb2p-PFNvQ|COPJE*zYbjk zs1dS@%^E;A9JWfSS4Zcik91HaDff@u{JR6$yT&~DYnEtn%`cS=LRYxFJ5D?toj9hG7~E`fkkesLnvFUT=4)4-;sp|{)? zx3y^V_p)wV$b&7)*FEKl58JoxcW-Emu<{`2Ufu>vPH{xEGB-?YfW;ycJFIDBSUkkX zHZn1hk==p^^J%D0U*7&%LR9+SB9Tf8DwUJ&>IUx1n3H6_sZevNpd=KtjU=+il{fo9 zAFlpvO#8bE-hEQ+M`-bdhWa&dKq3IcR>0l3rphzn+}VKx9U;d;|C4Pp4*?ktquG;e+6T^KxGJL4Z18&7HRK7T5E4Dp8o1GO=|uzp1-iO@UT zpY#ZfI^g%%KhZ!8%PEZz-x{@*O`y*vpZ&WhrmoBWn#W8ll-MVlo0%EFM}Q#=!A)KG8vlMyn#R%bVRue3pJImmcF_20S1Al3d=7|#zrv)AQ*pwSjhMQf zS7}O0Jsiv6@r{5AuZf;N-qA!%$!LF9SoO}G(aSj9`{YJ$8P(qg$M#f@gvYz0HA|WQ4pH9BE+Q-t_A;3IF)OJYC>cIg8<;$l~6!rFLx$MKwl+%n% zg?;=T8P6ecqdS?@k=u=Ux9q{^!Da?ARe*4EPq_>0JEHXu1*DBOw; zn00)IZkjaC3wUAyVno&84*7W2n@0h_pG=_R4J8>QeNYF%TevXQGG%XiPagIW!Wg6Y z5eH?UB#LID2Y)aGj%c2+c9=exN1cP;b)xvrsq-F0tMc?Fe?mzQU5fDLqX`q!dgvA; zU~I{QYC07Hd^>?H;SLJvQ(~1da>_z(FcNJM~!L!7H7;taJeKGLiN49o7=}!BqL#x(3wwKmLcM)N# zGoQX1@|+50xN_3-d-b-ws?om{CmytF3j;JDj3_Lg?Pj>}19v{13#tQHd@1tiMiWSZ z7hVvQc1;Ef!&|2Q)8m@AFUdsA5p@m<*8I3XAj`AO;#JNI(&qMH6=J znS~H+Ow6hEj8lI$Kb=F&hlQ(_cUoKFS>{;9LL33G=rq-Qq10}I(2S27_7MJ-DJwI? z4v&0lV6E`L$Wu|KR(XyHrp6%e!IRPJgp#Ulhj@a9@Z|fczahAd_^PSbPwC#_UV>*x z>=!V6@rNi3OCM^-pjv@1Ity-_zdUG2p-;`+Ry3Z4Or0xIp}Eis%2} zl+|#U|IJNh;}Z3&ojA+-JS$oQugwdJ%|SX@_0YsY7Vr{kzPJ-i?RL_AWE5=P@ewmw zd;%%PbMK`XtC6-iFVCN&D|XR;Z=t|S=nJzp1&L-$69nq~<@PKna6$hBuLH>xPV^Qa z0}?Z!^9T?igpMJzj_!OXi#URC#m39AZ6GcNqs^0=7~_}^9Jl~#hd+HCxxCF`vaa*t zUNuzjKdE1B1YEC^#T2iM9$5yeL21NQJQWTd)*==JK$fKH2fk?ovAU-@xqrhPpBTt<&KAaK%37=Z7AL=MiX;>}+lh1p7oSZS7|r zGqga(As*;kj(jmctM$)DI*Mjl6wu`LW#S-=`qk@%Tc_Qi6$p)a9upJKTiS|$l!?5} zFKN3cc(V`a2o#b`Rz^9?j<;1>s_xcH`%wXLl{io+L!widuEVOI0+F|FgA-_0;( zsH#K96E}}`vOnebFMUl*8tGGGNM5oqqe<#8aRH~?y%9k?+e@}}bO z?RBbz2VcVcd1yj->8vGZmlE+gL{jR)&$Zp)EOVW0+z#NGz>iU9+v1t8p*+w1vL@kJ zT$D%O@9q6b@o+hqlSN>dCVQGaMF0U@NXR4qtk+WT0odbmMH?Y7-UHB8himqqWo6fP z<^4jQjiWXjxnsKeTFlo+Ea`6!KvS2Ffr&fWXz& zaM|L$?A~yN#gGz8O3J=ME44Ra6RW~Ny{Wy?Ri^qA^aY}4U$!G%XAYh3OF5Mnzy%Q;+b%aT=h zv*jpvJ87qJ@k&;lhc=?6ub(Q#$iv<^wCKzpsc^q&U<%-h9J4e4<`7u|a0OyM|4Ba^ zE;rw5;A(8!c?6Jux1}I!$(vR4h|~P}XL|mg+zIU5ClNqucv2w7#tGCKr|e-G%QUQT z1c9PBJT@q-0ZTIjfdbwHvOy?Gpg($kxOZ}5_16hFWOa3P*4o@Xd&Ddr>pTt84&`_K z3o5_7yY4JFVY0k01C78B4 z#1JoG^_RMDe!>kuh~k6hKqjW97$)2r;Wb-Vc5Q|}PP}UNPk0E z^A2I-Uw_{mcn#q*%~+2#9cU$)pGousCLS;C^;z`vPC}(XM)%?{?zNDFnb7WY6fY zBje-aM&YpgiE+o}!lViz(!J=sy=KOPCNpS;L6A&p_B} zao~GQprQ^2j*g*qq3n;I3$1K~I9CTaO&e#{)zj5qHyN{#4ys?h{pTPeL5{XLAlpGm zfC6NM&j*0}K?sln5TQga%j0k1*ZVzq@9Z=uviSM?yIe7vMA#T=TH->>@8YHDECqU~ z!c=C^ZT1xxhnCD=4h?nfVxye&dZBNgG{n^Z49=<~3T>HnQGk z4mIR~G^aYV;dA0b>pfYZQ^95gbY3Jb%fl&-qHW6kL?E?{Y%_f{PzWg?`u$qc=SG=7 zpvf(^_u_)lBAl{pMM0?Bnosq zT|@NXbAwAX#8T`f_sLkZB?rG67CZbH za<%r9@YRy}TvYR=-|I{NArxjD@<+GF$~3u}-FRwTGgsg9*SdPTH&9;tdIbUqL=R%e zU$tjlI_F{9T!Zp7qzJH@=jOUI+`&)+3MOS`WmjVq=ODh)fZXURM1^)^KAxLXuHv6# zv{4(Kca|P|n(C%)|6Pwa+bn2~Wqhbx)OO&b3Q`XA7CxNFFt4#MJr^I@X;Se9kB1-3L%tTl zTrZrCBA`g@U8$IuSsY`;zXDx>06ZhzEIFfFRzqlo;5cbaE=Q1cDhR65928xtS#To( zk>|hw3^uAWYarsyj~c@~p`)O3mMom1r@L#ou_ISO7iJMy)5OZIPi{6A<}r6>VWLZv zh~LG_%#b1Rr}hrLYv*<+t(^0=i|ibcJys9K2*D1A(5)zg{z3uiupA2-k=kAwngEc7 z7!GVDXSs^ELqM?_5VV4wRr%|L;!zIv=ACYNW27WCWU*s_Ak)fE~V}0UfXrZc|LRWT&mY*84SvQ z&vFftai`w6@H_q&_;UvUqY=L<2KLi}MsB95<^qAP;u6}D0Q=1am=KV>-hvAggk^VJ zag9Azyme=1BFtpk4_8~SJ@xi3u?fHbF{xBOfD2IR3fAKia;R{xkSMY6mra71EsRV( zIvM)eCtC70Y=t*O>LghB-wcHfUNqIX6wbP-l86dG?Hy1IACA$wZu{9t*oo<5S3GDf zTq|MpGrU5L^0;AbjgL1U|2-^3U=Lnf;QQ?d z$PoItSA?ox^>q}rn#+KvQrOn}`5}1mu$sMEWB%_<(;x85>*7%2@W|AjFkVq+@>A;> zTuIWhnxp{MO8JrHp#Z2-&z^Ku6FFq?k|h4zt6*c;g?Xt zWUI?~NG3o|iW@D5++!oB5nEn|k}jOAPS7d_$i|pYguU?wgd~7VfaO3t1$jKe!;fyu zG)8Nv9AWj=PQ3(by@wC4^j1gU-Ahr$SqEH;3~Wkj#mx%ZT{FQ2Y8Eul6K>FzAz9ZU z>4UKnkTRz}frJ#8iFU(?tu~ef!F*SZ*SqhX%AK8MchWj+#(Jv`FJk6VSdXLY$VTmp z=_yq`cR}s2Jf@W1=`%Q_O7z}3aWIA5x{>L1$cdR<*S#ZYcywvfjR{GlT=nmDM zin%KV<%+!dP9YHA0Q`eQ5d;!5z!XV;@aZ&{sRC;wk$7c*tG0r%4-lBw1%n-uVDkXv zw04Q0eH*Xcj6M`rn3{dk;%*?;*k0qeGu~3BC0^uHwLibTvP3dc>%#T0T4zx=)2l*L znA`c%%PP-hj~x`ILGm)i*+*H<<NmCX{=HpTpUW&?y(04UI4CDw8;tqTu< zvB2_=B-DaI%1eqs2|@j8q`WRjaPX=Nt?b)W|DaA*L+b}hgD=`F1zT`hon23Q=(9vd zLEnP-04)jZ5yZzrcvK6?t?yy`%cf6Xe*wb7?Xy?oM>i`aiq#&9hg`6rM(yWUNv+?c zH1Kqy94T?clk~z`9V}#fzpTP87_78aP~etc&9c?OsE1}JgXOplYKu~(K7{RM%?yfVyc89IbnqkMww+M&bC-{7X zLcNpqrMwfDlgk+O&#UY=(Ae#r7nRcHl8G2jLER%%EZR$T`bMK2+agX4Wi zk9fTAZOh5l2X9E~9^0+G)saX{gW5Y><0Sde`~NA!x}j+aDg~>cVKlT6yndUB)Y8&& z2U7Q(yu9bo(7AH8aTU9P%=$`Nsjm-M*dn^=i$=bwLZ6ghxIxR((G_gvw4j@|J?9T>nCPAfhIRn6^37329^}j($k{@f)Vm@i18xm zCR;Z0AHXs~Q4x~4Cua76$(4WJ^OA$x(yxoJAt^Kco(Ad Ymj1c5(1$asK&e!bRJ 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

    M@46n@D_MZK*gpO8HD;GOpUHV7A z`8MXf8(G=juqARgWp+5i>~{vK z?J-_wre0}qLvvIrHJ+$4rkwie(RUsS*Z7dPZ6wiyqe}f@%E|%ump=u)eRKHC&e{~@ z{KsY^>_ABYF~xam?KnG*^kC4SgKtX1*;hOrj=%^49_XVx{;|k+-!~vPn>17!bQ)FW zU1rON_8qu5pHSF-`KrbY@|aHxJD-fC$yJx^DRr0CppDs=)y*ST5W(7_}>zg&gYkZ1Dcxn!T + + + + + + + + diff --git a/x-pack/plugins/security_solution/public/overview/pages/landing.tsx b/x-pack/plugins/security_solution/public/overview/pages/landing.tsx new file mode 100644 index 00000000000000..0554f1f51c28a8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/pages/landing.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { SpyRoute } from '../../common/utils/route/spy_routes'; +import { SecurityPageName } from '../../../common/constants'; +import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; +import { LandingCards } from '../components/landing_cards'; + +export const LandingPage = memo(() => { + return ( + <> + + + + + + ); +}); + +LandingPage.displayName = 'LandingPage'; diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx index da36e19d20a55c..e5be86a1c9f91e 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx @@ -27,8 +27,30 @@ import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experime import { initialUserPrivilegesState } from '../../common/components/user_privileges/user_privileges_context'; import { EndpointPrivileges } from '../../../common/endpoint/types'; import { useHostRiskScore } from '../../risk_score/containers'; +import { APP_UI_ID, SecurityPageName } from '../../../common/constants'; +import { getAppLandingUrl } from '../../common/components/link_to/redirect_to_overview'; +import { mockCasesContract } from '../../../../cases/public/mocks'; -jest.mock('../../common/lib/kibana'); +const mockNavigateToApp = jest.fn(); +jest.mock('../../common/lib/kibana', () => { + const original = jest.requireActual('../../common/lib/kibana'); + + return { + ...original, + useKibana: () => ({ + services: { + ...original.useKibana().services, + application: { + ...original.useKibana().services.application, + navigateToApp: mockNavigateToApp, + }, + cases: { + ...mockCasesContract(), + }, + }, + }), + }; +}); jest.mock('../../common/containers/source'); jest.mock('../../common/containers/sourcerer'); jest.mock('../../common/containers/use_global_time', () => ({ @@ -129,6 +151,9 @@ describe('Overview', () => { }); describe('rendering', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); test('it DOES NOT render the Getting started text when an index is available', () => { mockUseSourcererDataView.mockReturnValue({ selectedPatterns: [], @@ -146,7 +171,7 @@ describe('Overview', () => { ); - expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(false); + expect(mockNavigateToApp).not.toHaveBeenCalled(); wrapper.unmount(); }); @@ -279,14 +304,18 @@ describe('Overview', () => { }); it('renders the Setup Instructions text', () => { - const wrapper = mount( + mount( ); - expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true); + + expect(mockNavigateToApp).toHaveBeenCalledWith(APP_UI_ID, { + deepLinkId: SecurityPageName.landing, + path: getAppLandingUrl(), + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/routes.tsx b/x-pack/plugins/security_solution/public/overview/routes.tsx index c4fc3a6678c518..b4aa19e1e9bc1d 100644 --- a/x-pack/plugins/security_solution/public/overview/routes.tsx +++ b/x-pack/plugins/security_solution/public/overview/routes.tsx @@ -7,9 +7,15 @@ import React from 'react'; import { TrackApplicationView } from '../../../../../src/plugins/usage_collection/public'; -import { OVERVIEW_PATH, DETECTION_RESPONSE_PATH, SecurityPageName } from '../../common/constants'; +import { + LANDING_PATH, + OVERVIEW_PATH, + DETECTION_RESPONSE_PATH, + SecurityPageName, +} from '../../common/constants'; import { SecuritySubPluginRoutes } from '../app/types'; +import { LandingPage } from './pages/landing'; import { StatefulOverview } from './pages/overview'; import { DetectionResponse } from './pages/detection_response'; @@ -24,6 +30,11 @@ const DetectionResponseRoutes = () => ( ); +const LandingRoutes = () => ( + + + +); export const routes: SecuritySubPluginRoutes = [ { @@ -34,4 +45,8 @@ export const routes: SecuritySubPluginRoutes = [ path: DETECTION_RESPONSE_PATH, render: DetectionResponseRoutes, }, + { + path: LANDING_PATH, + render: LandingRoutes, + }, ]; diff --git a/x-pack/plugins/security_solution/public/users/pages/users_tabs.test.tsx b/x-pack/plugins/security_solution/public/users/pages/users_tabs.test.tsx index 41cb19e48e94d4..e3807f359a0fff 100644 --- a/x-pack/plugins/security_solution/public/users/pages/users_tabs.test.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/users_tabs.test.tsx @@ -15,6 +15,8 @@ import { SecuritySolutionTabNavigation } from '../../common/components/navigatio import { Users } from './users'; import { useSourcererDataView } from '../../common/containers/sourcerer'; import { mockCasesContext } from '../../../../cases/public/mocks/mock_cases_context'; +import { APP_UI_ID, SecurityPageName } from '../../../common/constants'; +import { getAppLandingUrl } from '../../common/components/link_to/redirect_to_overview'; jest.mock('../../common/containers/sourcerer'); jest.mock('../../common/components/search_bar', () => ({ @@ -26,6 +28,7 @@ jest.mock('../../common/components/query_bar', () => ({ jest.mock('../../common/components/visualization_actions', () => ({ VisualizationActions: jest.fn(() =>
    ), })); +const mockNavigateToApp = jest.fn(); jest.mock('../../common/lib/kibana', () => { const original = jest.requireActual('../../common/lib/kibana'); @@ -34,6 +37,10 @@ jest.mock('../../common/lib/kibana', () => { useKibana: () => ({ services: { ...original.useKibana().services, + application: { + ...original.useKibana().services.application, + navigateToApp: mockNavigateToApp, + }, cases: { ui: { getCasesContext: jest.fn().mockReturnValue(mockCasesContext), @@ -71,14 +78,17 @@ describe('Users - rendering', () => { indicesExist: false, }); - const wrapper = mount( + mount( ); - expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true); + expect(mockNavigateToApp).toHaveBeenCalledWith(APP_UI_ID, { + deepLinkId: SecurityPageName.landing, + path: getAppLandingUrl(), + }); }); test('it should render tab navigation', async () => { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f1ab772dbb2439..2395df6d2d9014 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -25211,8 +25211,6 @@ "xpack.securitySolution.pages.common.solutionName": "セキュリティ", "xpack.securitySolution.pages.common.updateAlertStatusFailed": "{ conflicts } {conflicts, plural, other {アラート}}を更新できませんでした。", "xpack.securitySolution.pages.common.updateAlertStatusFailedDetailed": "{ updated } {updated, plural, other {アラート}}が正常に更新されましたが、{ conflicts }は更新できませんでした。\n { conflicts, plural, other {}}すでに修正されています。", - "xpack.securitySolution.pages.emptyPage.beatsCard.description": "Elasticエージェントを使用して、セキュリティイベントを収集し、エンドポイントを脅威から保護してください。", - "xpack.securitySolution.pages.emptyPage.beatsCard.title": "セキュリティ統合を追加", "xpack.securitySolution.pages.fourohfour.pageNotFoundDescription": "ページが見つかりません", "xpack.securitySolution.paginatedTable.rowsButtonLabel": "ページごとの行数", "xpack.securitySolution.paginatedTable.showingSubtitle": "表示中", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 51c4915baab297..6d4465ae164878 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -25241,8 +25241,6 @@ "xpack.securitySolution.pages.common.solutionName": "安全", "xpack.securitySolution.pages.common.updateAlertStatusFailed": "无法更新{ conflicts } 个{conflicts, plural, other {告警}}。", "xpack.securitySolution.pages.common.updateAlertStatusFailedDetailed": "{ updated } 个{updated, plural, other {告警}}已成功更新,但是 { conflicts } 个无法更新,\n 因为{ conflicts, plural, other {其}}已被修改。", - "xpack.securitySolution.pages.emptyPage.beatsCard.description": "使用 Elastic 代理来收集安全事件并防止您的终端受到威胁。", - "xpack.securitySolution.pages.emptyPage.beatsCard.title": "添加安全集成", "xpack.securitySolution.pages.fourohfour.pageNotFoundDescription": "未找到页面", "xpack.securitySolution.paginatedTable.rowsButtonLabel": "每页行数", "xpack.securitySolution.paginatedTable.showingSubtitle": "正在显示", From 416580cfa44be0564136d8e1413f7959a1a4946a Mon Sep 17 00:00:00 2001 From: Shahzad Date: Wed, 23 Mar 2022 21:34:00 +0100 Subject: [PATCH 59/64] [Monitor management] Added inline errors (#124838) --- x-pack/plugins/observability/public/plugin.ts | 2 + .../get_app_data_view.ts | 25 +++ .../observability_data_views.ts | 3 + .../common/runtime_types/monitor/state.ts | 4 +- .../monitor_management/monitor_types.ts | 4 +- .../uptime/common/runtime_types/ping/ping.ts | 11 ++ .../common/runtime_types/ping/synthetics.ts | 3 + .../journeys/monitor_management.journey.ts | 2 +- .../e2e/page_objects/monitor_management.tsx | 2 +- x-pack/plugins/uptime/kibana.json | 1 + x-pack/plugins/uptime/public/apps/plugin.ts | 2 + .../common/header/action_menu_content.tsx | 2 +- .../action_bar/action_bar.tsx | 2 +- .../hooks/use_inline_errors.test.tsx | 83 ++++++++++ .../hooks/use_inline_errors.ts | 110 +++++++++++++ .../hooks/use_inline_errors_count.test.tsx | 79 ++++++++++ .../hooks/use_inline_errors_count.ts | 48 ++++++ .../hooks/use_invalid_monitors.tsx | 47 ++++++ .../monitor_list/actions.test.tsx | 2 +- .../monitor_list/actions.tsx | 25 ++- .../monitor_list/all_monitors.tsx | 34 ++++ .../monitor_list/delete_monitor.test.tsx | 4 +- .../monitor_list/inline_error.test.tsx | 62 ++++++++ .../monitor_list/inline_error.tsx | 51 ++++++ .../monitor_list/invalid_monitors.tsx | 53 +++++++ .../monitor_list/list_tabs.test.tsx | 35 +++++ .../monitor_list/list_tabs.tsx | 122 +++++++++++++++ .../monitor_list/monitor_list.test.tsx | 1 + .../monitor_list/monitor_list.tsx | 13 +- .../monitor_list/stderr_logs_popover.tsx | 55 +++++++ .../browser/browser_test_results.tsx | 4 +- .../test_now_mode/test_result_header.tsx | 12 +- .../columns/monitor_status_column.tsx | 27 ++-- .../columns/status_badge.test.tsx | 47 ++++++ .../monitor_list/columns/status_badge.tsx | 70 +++++++++ .../overview/monitor_list/monitor_list.tsx | 5 +- .../synthetics/check_steps/stderr_logs.tsx | 148 ++++++++++++++++++ .../check_steps/use_std_error_logs.ts | 65 ++++++++ .../contexts/uptime_refresh_context.tsx | 4 +- .../monitor_management/monitor_management.tsx | 58 +++++-- .../use_monitor_management_breadcrumbs.tsx | 3 +- x-pack/plugins/uptime/public/routes.tsx | 2 +- .../search/refine_potential_matches.ts | 2 + .../hydrate_saved_object.ts | 17 +- 44 files changed, 1302 insertions(+), 49 deletions(-) create mode 100644 x-pack/plugins/observability/public/utils/observability_data_views/get_app_data_view.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors_count.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors_count.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/hooks/use_invalid_monitors.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/monitor_list/all_monitors.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/monitor_list/inline_error.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/monitor_list/inline_error.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/monitor_list/invalid_monitors.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/monitor_list/list_tabs.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/monitor_list/list_tabs.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/monitor_list/stderr_logs_popover.tsx create mode 100644 x-pack/plugins/uptime/public/components/overview/monitor_list/columns/status_badge.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/overview/monitor_list/columns/status_badge.tsx create mode 100644 x-pack/plugins/uptime/public/components/synthetics/check_steps/stderr_logs.tsx create mode 100644 x-pack/plugins/uptime/public/components/synthetics/check_steps/use_std_error_logs.ts diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 3d2505ed80513b..9d483b63ac0a97 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -47,6 +47,7 @@ import { updateGlobalNavigation } from './update_global_navigation'; import { getExploratoryViewEmbeddable } from './components/shared/exploratory_view/embeddable'; import { createExploratoryViewUrl } from './components/shared/exploratory_view/configurations/utils'; import { createUseRulesLink } from './hooks/create_use_rules_link'; +import getAppDataView from './utils/observability_data_views/get_app_data_view'; export type ObservabilityPublicSetup = ReturnType; @@ -280,6 +281,7 @@ export class Plugin PageTemplate, }, createExploratoryViewUrl, + getAppDataView: getAppDataView(pluginsStart.dataViews), ExploratoryViewEmbeddable: getExploratoryViewEmbeddable(coreStart, pluginsStart), useRulesLink: createUseRulesLink(config.unsafe.rules.enabled), }; diff --git a/x-pack/plugins/observability/public/utils/observability_data_views/get_app_data_view.ts b/x-pack/plugins/observability/public/utils/observability_data_views/get_app_data_view.ts new file mode 100644 index 00000000000000..4b4b03412c0c71 --- /dev/null +++ b/x-pack/plugins/observability/public/utils/observability_data_views/get_app_data_view.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AppDataType } from '../../components/shared/exploratory_view/types'; +import type { DataViewsPublicPluginStart } from '../../../../../../src/plugins/data_views/public'; + +const getAppDataView = (data: DataViewsPublicPluginStart) => { + return async (appId: AppDataType, indexPattern?: string) => { + try { + const { ObservabilityDataViews } = await import('./observability_data_views'); + + const obsvIndexP = new ObservabilityDataViews(data); + return await obsvIndexP.getDataView(appId, indexPattern); + } catch (e) { + return null; + } + }; +}; + +// eslint-disable-next-line import/no-default-export +export default getAppDataView; diff --git a/x-pack/plugins/observability/public/utils/observability_data_views/observability_data_views.ts b/x-pack/plugins/observability/public/utils/observability_data_views/observability_data_views.ts index 8a74482bb14ca8..86ce6cd587213a 100644 --- a/x-pack/plugins/observability/public/utils/observability_data_views/observability_data_views.ts +++ b/x-pack/plugins/observability/public/utils/observability_data_views/observability_data_views.ts @@ -176,3 +176,6 @@ export class ObservabilityDataViews { } } } + +// eslint-disable-next-line import/no-default-export +export default ObservabilityDataViews; diff --git a/x-pack/plugins/uptime/common/runtime_types/monitor/state.ts b/x-pack/plugins/uptime/common/runtime_types/monitor/state.ts index d43fd5ad001f23..74a3bba6ae0277 100644 --- a/x-pack/plugins/uptime/common/runtime_types/monitor/state.ts +++ b/x-pack/plugins/uptime/common/runtime_types/monitor/state.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { PingType } from '../ping/ping'; +import { PingErrorType, PingType } from '../ping/ping'; export const StateType = t.intersection([ t.type({ @@ -27,6 +27,7 @@ export const StateType = t.intersection([ monitor: t.intersection([ t.partial({ name: t.string, + checkGroup: t.string, duration: t.type({ us: t.number }), }), t.type({ @@ -47,6 +48,7 @@ export const StateType = t.intersection([ service: t.partial({ name: t.string, }), + error: PingErrorType, }), ]); diff --git a/x-pack/plugins/uptime/common/runtime_types/monitor_management/monitor_types.ts b/x-pack/plugins/uptime/common/runtime_types/monitor_management/monitor_types.ts index c63f5eb838d60c..e0205b9362e232 100644 --- a/x-pack/plugins/uptime/common/runtime_types/monitor_management/monitor_types.ts +++ b/x-pack/plugins/uptime/common/runtime_types/monitor_management/monitor_types.ts @@ -222,7 +222,9 @@ export const SyntheticsMonitorWithIdCodec = t.intersection([ export type SyntheticsMonitorWithId = t.TypeOf; export const MonitorManagementListResultCodec = t.type({ - monitors: t.array(t.interface({ id: t.string, attributes: SyntheticsMonitorCodec })), + monitors: t.array( + t.interface({ id: t.string, attributes: SyntheticsMonitorCodec, updated_at: t.string }) + ), page: t.number, perPage: t.number, total: t.union([t.number, t.null]), diff --git a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts index e78f026277d3a8..6208e42868d9e8 100644 --- a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts +++ b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts @@ -180,8 +180,14 @@ export const PingType = t.intersection([ }), }), observer: t.partial({ + hostname: t.string, + ip: t.array(t.string), + mac: t.array(t.string), geo: t.partial({ name: t.string, + continent_name: t.string, + city_name: t.string, + country_iso_code: t.string, location: t.union([ t.string, t.partial({ lat: t.number, lon: t.number }), @@ -221,6 +227,11 @@ export const PingType = t.intersection([ name: t.string, }), config_id: t.string, + data_stream: t.interface({ + namespace: t.string, + type: t.string, + dataset: t.string, + }), }), ]); diff --git a/x-pack/plugins/uptime/common/runtime_types/ping/synthetics.ts b/x-pack/plugins/uptime/common/runtime_types/ping/synthetics.ts index a143063d221b97..c95f9c281dc923 100644 --- a/x-pack/plugins/uptime/common/runtime_types/ping/synthetics.ts +++ b/x-pack/plugins/uptime/common/runtime_types/ping/synthetics.ts @@ -79,6 +79,9 @@ export const JourneyStepType = t.intersection([ }), }), synthetics: SyntheticsDataType, + error: t.type({ + message: t.string, + }), }), t.type({ _id: t.string, diff --git a/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts b/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts index 1d6270c00df658..309cc5eb0ec6d9 100644 --- a/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts +++ b/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts @@ -202,7 +202,7 @@ journey('Monitor Management breadcrumbs', async ({ page, params }: { page: Page; step('edit http monitor and check breadcrumb', async () => { await uptime.editMonitor(); // breadcrumb is available before edit page is loaded, make sure its edit view - await page.waitForSelector(byTestId('monitorManagementMonitorName')); + await page.waitForSelector(byTestId('monitorManagementMonitorName'), { timeout: 60 * 1000 }); const breadcrumbs = await page.$$('[data-test-subj=breadcrumb]'); expect(await breadcrumbs[1].textContent()).toEqual('Monitor management'); const lastBreadcrumb = await (await uptime.findByTestSubj('"breadcrumb last"')).textContent(); diff --git a/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx b/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx index a19f14fa1a6d14..b56cd8a361684d 100644 --- a/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx +++ b/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx @@ -23,7 +23,7 @@ export function monitorManagementPageProvider({ const remotePassword = process.env.SYNTHETICS_REMOTE_KIBANA_PASSWORD; const isRemote = Boolean(process.env.SYNTHETICS_REMOTE_ENABLED); const basePath = isRemote ? remoteKibanaUrl : kibanaUrl; - const monitorManagement = `${basePath}/app/uptime/manage-monitors`; + const monitorManagement = `${basePath}/app/uptime/manage-monitors/all`; const addMonitor = `${basePath}/app/uptime/add-monitor`; const overview = `${basePath}/app/uptime`; return { diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json index 28a49067b6698a..0ae53fe56b1a46 100644 --- a/x-pack/plugins/uptime/kibana.json +++ b/x-pack/plugins/uptime/kibana.json @@ -7,6 +7,7 @@ "alerting", "cases", "embeddable", + "discover", "encryptedSavedObjects", "features", "inspector", diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index a5e2a85953d65c..bf7c5336a8b0f2 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -16,6 +16,7 @@ import { from } from 'rxjs'; import { map } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { SharePluginSetup, SharePluginStart } from '../../../../../src/plugins/share/public'; +import { DiscoverStart } from '../../../../../src/plugins/discover/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../../src/core/public'; import { @@ -61,6 +62,7 @@ export interface ClientPluginsSetup { export interface ClientPluginsStart { fleet?: FleetStart; data: DataPublicPluginStart; + discover: DiscoverStart; inspector: InspectorPluginStart; embeddable: EmbeddableStart; observability: ObservabilityPublicStart; diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx index 985b1ae9146f24..0c059580b54614 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx @@ -85,7 +85,7 @@ export function ActionMenuContent({ config }: { config: UptimeConfig }): React.R color="text" data-test-subj="management-page-link" href={history.createHref({ - pathname: MONITOR_MANAGEMENT_ROUTE, + pathname: MONITOR_MANAGEMENT_ROUTE + '/all', })} > + ) : ( diff --git a/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors.test.tsx new file mode 100644 index 00000000000000..369aa1461c4257 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors.test.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { MockRedux } from '../../../lib/helper/rtl_helpers'; +import { useInlineErrors } from './use_inline_errors'; +import * as obsvPlugin from '../../../../../observability/public/hooks/use_es_search'; + +function mockNow(date: string | number | Date) { + const fakeNow = new Date(date).getTime(); + return jest.spyOn(Date, 'now').mockReturnValue(fakeNow); +} + +describe('useInlineErrors', function () { + it('it returns result as expected', async function () { + mockNow('2022-01-02T00:00:00.000Z'); + const spy = jest.spyOn(obsvPlugin, 'useEsSearch'); + + const { result } = renderHook(() => useInlineErrors({ onlyInvalidMonitors: true }), { + wrapper: MockRedux, + }); + + expect(result.current).toEqual({ + loading: true, + }); + + expect(spy).toHaveBeenNthCalledWith( + 3, + { + body: { + collapse: { field: 'config_id' }, + query: { + bool: { + filter: [ + { exists: { field: 'summary' } }, + { exists: { field: 'error' } }, + { + bool: { + minimum_should_match: 1, + should: [ + { match_phrase: { 'error.message': 'journey did not finish executing' } }, + { match_phrase: { 'error.message': 'ReferenceError:' } }, + ], + }, + }, + { + range: { + 'monitor.timespan': { + gte: '2022-01-01T23:55:00.000Z', + lte: '2022-01-02T00:00:00.000Z', + }, + }, + }, + { bool: { must_not: { exists: { field: 'run_once' } } } }, + ], + }, + }, + size: 1000, + sort: [{ '@timestamp': 'desc' }], + }, + index: 'heartbeat-8*,heartbeat-7*,synthetics-*', + }, + [ + 'heartbeat-8*,heartbeat-7*,synthetics-*', + { + error: { monitorList: null, serviceLocations: null }, + list: { monitors: [], page: 1, perPage: 10, total: null }, + loading: { monitorList: false, serviceLocations: false }, + locations: [], + }, + 1641081600000, + true, + '@timestamp', + 'desc', + ], + { name: 'getInvalidMonitors' } + ); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors.ts b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors.ts new file mode 100644 index 00000000000000..3753d95b8e8583 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useSelector } from 'react-redux'; +import moment from 'moment'; +import { useMemo } from 'react'; +import { monitorManagementListSelector, selectDynamicSettings } from '../../../state/selectors'; +import { useEsSearch } from '../../../../../observability/public'; +import { Ping } from '../../../../common/runtime_types'; +import { EXCLUDE_RUN_ONCE_FILTER } from '../../../../common/constants/client_defaults'; +import { useUptimeRefreshContext } from '../../../contexts/uptime_refresh_context'; +import { useInlineErrorsCount } from './use_inline_errors_count'; + +const sortFieldMap: Record = { + name: 'monitor.name', + urls: 'url.full', + '@timestamp': '@timestamp', +}; + +export const getInlineErrorFilters = () => [ + { + exists: { + field: 'summary', + }, + }, + { + exists: { + field: 'error', + }, + }, + { + bool: { + minimum_should_match: 1, + should: [ + { + match_phrase: { + 'error.message': 'journey did not finish executing', + }, + }, + { + match_phrase: { + 'error.message': 'ReferenceError:', + }, + }, + ], + }, + }, + { + range: { + 'monitor.timespan': { + lte: moment().toISOString(), + gte: moment().subtract(5, 'minutes').toISOString(), + }, + }, + }, + EXCLUDE_RUN_ONCE_FILTER, +]; + +export function useInlineErrors({ + onlyInvalidMonitors, + sortField = '@timestamp', + sortOrder = 'desc', +}: { + onlyInvalidMonitors?: boolean; + sortField?: string; + sortOrder?: 'asc' | 'desc'; +}) { + const monitorList = useSelector(monitorManagementListSelector); + + const { settings } = useSelector(selectDynamicSettings); + + const { lastRefresh } = useUptimeRefreshContext(); + + const configIds = monitorList.list.monitors.map((monitor) => monitor.id); + + const doFetch = configIds.length > 0 || onlyInvalidMonitors; + + const { data, loading } = useEsSearch( + { + index: doFetch ? settings?.heartbeatIndices : '', + body: { + size: 1000, + query: { + bool: { + filter: getInlineErrorFilters(), + }, + }, + collapse: { field: 'config_id' }, + sort: [{ [sortFieldMap[sortField]]: sortOrder }], + }, + }, + [settings?.heartbeatIndices, monitorList, lastRefresh, doFetch, sortField, sortOrder], + { name: 'getInvalidMonitors' } + ); + + const { count, loading: countLoading } = useInlineErrorsCount(); + + return useMemo(() => { + const errorSummaries = data?.hits.hits.map(({ _source: source }) => ({ + ...(source as Ping), + timestamp: (source as any)['@timestamp'], + })); + + return { loading: loading || countLoading, errorSummaries, count }; + }, [count, countLoading, data, loading]); +} diff --git a/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors_count.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors_count.test.tsx new file mode 100644 index 00000000000000..c4c864e7720cdb --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors_count.test.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { MockRedux } from '../../../lib/helper/rtl_helpers'; +import { useInlineErrorsCount } from './use_inline_errors_count'; +import * as obsvPlugin from '../../../../../observability/public/hooks/use_es_search'; + +function mockNow(date: string | number | Date) { + const fakeNow = new Date(date).getTime(); + return jest.spyOn(Date, 'now').mockReturnValue(fakeNow); +} + +describe('useInlineErrorsCount', function () { + it('it returns result as expected', async function () { + mockNow('2022-01-02T00:00:00.000Z'); + const spy = jest.spyOn(obsvPlugin, 'useEsSearch'); + + const { result } = renderHook(() => useInlineErrorsCount(), { + wrapper: MockRedux, + }); + + expect(result.current).toEqual({ + loading: true, + }); + + expect(spy).toHaveBeenNthCalledWith( + 2, + { + body: { + aggs: { total: { cardinality: { field: 'config_id' } } }, + query: { + bool: { + filter: [ + { exists: { field: 'summary' } }, + { exists: { field: 'error' } }, + { + bool: { + minimum_should_match: 1, + should: [ + { match_phrase: { 'error.message': 'journey did not finish executing' } }, + { match_phrase: { 'error.message': 'ReferenceError:' } }, + ], + }, + }, + { + range: { + 'monitor.timespan': { + gte: '2022-01-01T23:55:00.000Z', + lte: '2022-01-02T00:00:00.000Z', + }, + }, + }, + { bool: { must_not: { exists: { field: 'run_once' } } } }, + ], + }, + }, + size: 0, + }, + index: 'heartbeat-8*,heartbeat-7*,synthetics-*', + }, + [ + 'heartbeat-8*,heartbeat-7*,synthetics-*', + { + error: { monitorList: null, serviceLocations: null }, + list: { monitors: [], page: 1, perPage: 10, total: null }, + loading: { monitorList: false, serviceLocations: false }, + locations: [], + }, + 1641081600000, + ], + { name: 'getInvalidMonitorsCount' } + ); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors_count.ts b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors_count.ts new file mode 100644 index 00000000000000..adda7c433b29c9 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_inline_errors_count.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useSelector } from 'react-redux'; +import { useMemo } from 'react'; +import { monitorManagementListSelector, selectDynamicSettings } from '../../../state/selectors'; +import { useEsSearch } from '../../../../../observability/public'; +import { useUptimeRefreshContext } from '../../../contexts/uptime_refresh_context'; +import { getInlineErrorFilters } from './use_inline_errors'; + +export function useInlineErrorsCount() { + const monitorList = useSelector(monitorManagementListSelector); + + const { settings } = useSelector(selectDynamicSettings); + + const { lastRefresh } = useUptimeRefreshContext(); + + const { data, loading } = useEsSearch( + { + index: settings?.heartbeatIndices, + body: { + size: 0, + query: { + bool: { + filter: getInlineErrorFilters(), + }, + }, + aggs: { + total: { + cardinality: { field: 'config_id' }, + }, + }, + }, + }, + [settings?.heartbeatIndices, monitorList, lastRefresh], + { name: 'getInvalidMonitorsCount' } + ); + + return useMemo(() => { + const errorSummariesCount = data?.aggregations?.total.value; + + return { loading, count: errorSummariesCount }; + }, [data, loading]); +} diff --git a/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_invalid_monitors.tsx b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_invalid_monitors.tsx new file mode 100644 index 00000000000000..98e882e543a875 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_invalid_monitors.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { useFetcher } from '../../../../../observability/public'; +import { Ping, SyntheticsMonitor } from '../../../../common/runtime_types'; +import { syntheticsMonitorType } from '../../../../common/types/saved_objects'; + +export const useInvalidMonitors = (errorSummaries?: Ping[]) => { + const { savedObjects } = useKibana().services; + + const ids = (errorSummaries ?? []).map((summary) => ({ + id: summary.config_id!, + type: syntheticsMonitorType, + })); + + return useFetcher(async () => { + if (ids.length > 0) { + const response = await savedObjects?.client.bulkResolve(ids); + if (response) { + const { resolved_objects: resolvedObjects } = response; + return resolvedObjects + .filter((sv) => { + if (sv.saved_object.updatedAt) { + const errorSummary = errorSummaries?.find( + (summary) => summary.config_id === sv.saved_object.id + ); + if (errorSummary) { + return moment(sv.saved_object.updatedAt).isBefore(moment(errorSummary.timestamp)); + } + } + + return !Boolean(sv.saved_object.error); + }) + .map(({ saved_object: savedObject }) => ({ + ...savedObject, + updated_at: savedObject.updatedAt!, + })); + } + } + }, [errorSummaries]); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.test.tsx index ec58ac7ee5010c..f60d54e9cb4f62 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.test.tsx @@ -15,7 +15,7 @@ describe('', () => { const onUpdate = jest.fn(); it('navigates to edit monitor flow on edit pencil', () => { - render(); + render(); expect(screen.getByLabelText('Edit monitor')).toHaveAttribute( 'href', diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.tsx index 9d84263f3701e1..47a0b8547ea8cb 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.tsx @@ -8,19 +8,37 @@ import React, { useContext } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonIcon, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import moment from 'moment'; import { UptimeSettingsContext } from '../../../contexts'; import { DeleteMonitor } from './delete_monitor'; +import { InlineError } from './inline_error'; +import { MonitorManagementListResult, Ping } from '../../../../common/runtime_types'; interface Props { id: string; name: string; isDisabled?: boolean; onUpdate: () => void; + errorSummaries?: Ping[]; + monitors: MonitorManagementListResult['monitors']; } -export const Actions = ({ id, name, onUpdate, isDisabled }: Props) => { +export const Actions = ({ id, name, onUpdate, isDisabled, errorSummaries, monitors }: Props) => { const { basePath } = useContext(UptimeSettingsContext); + let errorSummary = errorSummaries?.find((summary) => summary.config_id === id); + + const monitor = monitors.find((monitorT) => monitorT.id === id); + + if (errorSummary && monitor) { + const summaryIsBeforeUpdate = moment(monitor.updated_at).isBefore( + moment(errorSummary.timestamp) + ); + if (!summaryIsBeforeUpdate) { + errorSummary = undefined; + } + } + return ( @@ -35,6 +53,11 @@ export const Actions = ({ id, name, onUpdate, isDisabled }: Props) => { + {errorSummary && ( + + + + )} ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/all_monitors.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/all_monitors.tsx new file mode 100644 index 00000000000000..550d3b487a4ae5 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/all_monitors.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useSelector } from 'react-redux'; +import { MonitorManagementList, MonitorManagementListPageState } from './monitor_list'; +import { monitorManagementListSelector } from '../../../state/selectors'; +import { MonitorManagementList as MonitorManagementListState } from '../../../state/reducers/monitor_management'; +import { Ping } from '../../../../common/runtime_types'; + +interface Props { + pageState: MonitorManagementListPageState; + monitorList: MonitorManagementListState; + onPageStateChange: (state: MonitorManagementListPageState) => void; + onUpdate: () => void; + errorSummaries: Ping[]; +} +export const AllMonitors = ({ pageState, onPageStateChange, onUpdate, errorSummaries }: Props) => { + const monitorList = useSelector(monitorManagementListSelector); + + return ( + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/delete_monitor.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/delete_monitor.test.tsx index 2e69196c86cff4..f8a81a6efce0b6 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/delete_monitor.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/delete_monitor.test.tsx @@ -40,7 +40,7 @@ describe('', () => { it('calls set refresh when deletion is successful', () => { const id = 'test-id'; const name = 'sample monitor'; - render(); + render(); userEvent.click(screen.getByLabelText('Delete monitor')); @@ -54,7 +54,7 @@ describe('', () => { status: FETCH_STATUS.LOADING, refetch: () => {}, }); - render(); + render(); expect(screen.getByLabelText('Deleting monitor...')).toBeInTheDocument(); }); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/inline_error.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/inline_error.test.tsx new file mode 100644 index 00000000000000..1cf05d7697e60c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/inline_error.test.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen } from '@testing-library/react'; +import { render } from '../../../lib/helper/rtl_helpers'; +import { InlineError } from './inline_error'; + +describe('', () => { + it('calls delete monitor on monitor deletion', () => { + render(); + + expect( + screen.getByLabelText( + 'journey did not finish executing, 0 steps ran. Click for more details.' + ) + ).toBeInTheDocument(); + }); +}); + +const errorSummary = { + docId: 'testDoc', + summary: { up: 0, down: 1 }, + agent: { + name: 'cron-686f0ce427dfd7e3-27465864-sqs5l', + id: '778fe9c6-bbd1-47d4-a0be-73f8ba9cec61', + type: 'heartbeat', + ephemeral_id: 'bc1a961f-1fbc-4253-aee0-633a8f6fc56e', + version: '8.1.0', + }, + synthetics: { type: 'heartbeat/summary' }, + monitor: { + name: 'Browser monitor', + check_group: 'f5480358-a9da-11ec-bced-6274e5883bd7', + id: '3a5553a0-a949-11ec-b7ca-c3b39fffa2af', + timespan: { lt: '2022-03-22T12:27:02.563Z', gte: '2022-03-22T12:24:02.563Z' }, + type: 'browser', + status: 'down', + }, + error: { type: 'io', message: 'journey did not finish executing, 0 steps ran' }, + url: {}, + observer: { + geo: { + continent_name: 'North America', + city_name: 'Iowa', + country_iso_code: 'US', + name: 'North America - US Central', + location: '41.8780, 93.0977', + }, + hostname: 'cron-686f0ce427dfd7e3-27465864-sqs5l', + ip: ['10.1.9.110'], + mac: ['62:74:e5:88:3b:d7'], + }, + ecs: { version: '8.0.0' }, + config_id: '3a5553a0-a949-11ec-b7ca-c3b39fffa2af', + data_stream: { namespace: 'default', type: 'synthetics', dataset: 'browser' }, + timestamp: '2022-03-22T12:24:02.563Z', +}; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/inline_error.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/inline_error.tsx new file mode 100644 index 00000000000000..187c81ff8c6e9a --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/inline_error.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Ping } from '../../../../common/runtime_types'; +import { StdErrorPopover } from './stderr_logs_popover'; + +export const InlineError = ({ errorSummary }: { errorSummary: Ping }) => { + const [isOpen, setIsOpen] = useState(false); + + const errorMessage = + errorSummary.monitor.type === 'browser' + ? getInlineErrorLabel(errorSummary.error?.message) + : errorSummary.error?.message; + + return ( + + setIsOpen(true)} + color="danger" + /> + + } + /> + ); +}; + +export const getInlineErrorLabel = (message?: string) => { + return i18n.translate('xpack.uptime.monitorList.statusColumn.error.message', { + defaultMessage: '{message}. Click for more details.', + values: { message }, + }); +}; + +export const ERROR_LOGS_LABEL = i18n.translate('xpack.uptime.monitorList.statusColumn.error.logs', { + defaultMessage: 'Error logs', +}); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/invalid_monitors.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/invalid_monitors.tsx new file mode 100644 index 00000000000000..4b524a2b523129 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/invalid_monitors.tsx @@ -0,0 +1,53 @@ +/* + * 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 { MonitorManagementList, MonitorManagementListPageState } from './monitor_list'; +import { MonitorManagementListResult, Ping } from '../../../../common/runtime_types'; + +interface Props { + loading: boolean; + pageState: MonitorManagementListPageState; + monitorSavedObjects?: MonitorManagementListResult['monitors']; + onPageStateChange: (state: MonitorManagementListPageState) => void; + onUpdate: () => void; + errorSummaries: Ping[]; + invalidTotal: number; +} +export const InvalidMonitors = ({ + loading: summariesLoading, + pageState, + onPageStateChange, + onUpdate, + errorSummaries, + invalidTotal, + monitorSavedObjects, +}: Props) => { + const { pageSize, pageIndex } = pageState; + + const startIndex = (pageIndex - 1) * pageSize; + + return ( + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/list_tabs.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/list_tabs.test.tsx new file mode 100644 index 00000000000000..bfac60de96bc76 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/list_tabs.test.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen } from '@testing-library/react'; +import { render } from '../../../lib/helper/rtl_helpers'; +import { MonitorListTabs } from './list_tabs'; + +describe('', () => { + it('calls delete monitor on monitor deletion', () => { + const onPageStateChange = jest.fn(); + render( + + ); + + expect(screen.getByText('All monitors')).toBeInTheDocument(); + expect(screen.getByText('Invalid monitors')).toBeInTheDocument(); + + expect(onPageStateChange).toHaveBeenCalledTimes(1); + expect(onPageStateChange).toHaveBeenCalledWith({ + pageIndex: 1, + pageSize: 10, + sortField: 'name', + sortOrder: 'asc', + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/list_tabs.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/list_tabs.tsx new file mode 100644 index 00000000000000..1aad6d4d888e51 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/list_tabs.tsx @@ -0,0 +1,122 @@ +/* + * 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 { + EuiTabs, + EuiTab, + EuiNotificationBadge, + EuiFlexGroup, + EuiFlexItem, + EuiButton, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useState, Fragment, useEffect } from 'react'; +import { useHistory, useParams } from 'react-router-dom'; +import { useUptimeRefreshContext } from '../../../contexts/uptime_refresh_context'; +import { MonitorManagementListPageState } from './monitor_list'; +import { ConfigKey } from '../../../../common/runtime_types'; + +export const MonitorListTabs = ({ + invalidTotal, + onUpdate, + onPageStateChange, +}: { + invalidTotal: number; + onUpdate: () => void; + onPageStateChange: (state: MonitorManagementListPageState) => void; +}) => { + const [selectedTabId, setSelectedTabId] = useState('all'); + + const { refreshApp } = useUptimeRefreshContext(); + + const history = useHistory(); + + const { type: viewType } = useParams<{ type: 'all' | 'invalid' }>(); + + useEffect(() => { + setSelectedTabId(viewType); + onPageStateChange({ pageIndex: 1, pageSize: 10, sortOrder: 'asc', sortField: ConfigKey.NAME }); + }, [viewType, onPageStateChange]); + + const tabs = [ + { + id: 'all', + name: ALL_MONITORS_LABEL, + content: , + href: history.createHref({ pathname: '/manage-monitors/all' }), + disabled: false, + }, + { + id: 'invalid', + name: INVALID_MONITORS_LABEL, + append: ( + + {invalidTotal} + + ), + href: history.createHref({ pathname: '/manage-monitors/invalid' }), + content: , + disabled: invalidTotal === 0, + }, + ]; + + const onSelectedTabChanged = (id: string) => { + setSelectedTabId(id); + }; + + const renderTabs = () => { + return tabs.map((tab, index) => ( + onSelectedTabChanged(tab.id)} + isSelected={tab.id === selectedTabId} + disabled={tab.disabled} + append={tab.append} + > + {tab.name} + + )); + }; + + return ( + + + {renderTabs()} + + + { + onUpdate(); + refreshApp(); + }} + > + {REFRESH_LABEL} + + + + ); +}; + +export const REFRESH_LABEL = i18n.translate('xpack.uptime.monitorList.refresh', { + defaultMessage: 'Refresh', +}); + +export const INVALID_MONITORS_LABEL = i18n.translate('xpack.uptime.monitorList.invalidMonitors', { + defaultMessage: 'Invalid monitors', +}); + +export const ALL_MONITORS_LABEL = i18n.translate('xpack.uptime.monitorList.allMonitors', { + defaultMessage: 'All monitors', +}); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.test.tsx index d9fb207f4fa20f..ff5d9ebf13ccfe 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.test.tsx @@ -20,6 +20,7 @@ describe('', () => { for (let i = 0; i < 12; i++) { monitors.push({ id: `test-monitor-id-${i}`, + updated_at: '123', attributes: { name: `test-monitor-${i}`, enabled: true, diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.tsx index 5d18fdcaca6fea..8bae4160f6b0c7 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/monitor_list.tsx @@ -21,6 +21,7 @@ import { FetchMonitorManagementListQueryArgs, ICMPSimpleFields, MonitorFields, + Ping, ServiceLocations, SyntheticsMonitorWithId, TCPSimpleFields, @@ -47,6 +48,7 @@ interface Props { monitorList: MonitorManagementListState; onPageStateChange: (state: MonitorManagementListPageState) => void; onUpdate: () => void; + errorSummaries?: Ping[]; } export const MonitorManagementList = ({ @@ -58,13 +60,18 @@ export const MonitorManagementList = ({ }, onPageStateChange, onUpdate, + errorSummaries, }: Props) => { const { basePath } = useContext(UptimeSettingsContext); const isXl = useBreakpoints().up('xl'); const { total } = list as MonitorManagementListState['list']; const monitors: SyntheticsMonitorWithId[] = useMemo( - () => list.monitors.map((monitor) => ({ ...monitor.attributes, id: monitor.id })), + () => + list.monitors.map((monitor) => ({ + ...monitor.attributes, + id: monitor.id, + })), [list.monitors] ); @@ -90,7 +97,7 @@ export const MonitorManagementList = ({ pageIndex: pageIndex - 1, // page index for EuiBasicTable is base 0 pageSize, totalItemCount: total || 0, - pageSizeOptions: [10, 25, 50, 100], + pageSizeOptions: [5, 10, 25, 50, 100], }; const sorting: EuiTableSortingType = { @@ -188,6 +195,8 @@ export const MonitorManagementList = ({ name={fields[ConfigKey.NAME]} isDisabled={!canEdit} onUpdate={onUpdate} + errorSummaries={errorSummaries} + monitors={list.monitors} /> ), }, diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/stderr_logs_popover.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/stderr_logs_popover.tsx new file mode 100644 index 00000000000000..c50cd33b13b1f3 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/stderr_logs_popover.tsx @@ -0,0 +1,55 @@ +/* + * 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 { EuiPopover } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import styled from 'styled-components'; +import { StdErrorLogs } from '../../synthetics/check_steps/stderr_logs'; + +export const StdErrorPopover = ({ + checkGroup, + button, + isOpen, + setIsOpen, + summaryMessage, +}: { + isOpen: boolean; + setIsOpen: (val: boolean) => void; + checkGroup: string; + summaryMessage?: string; + button: JSX.Element; +}) => { + return ( + setIsOpen(false)} button={button}> + + + + + ); +}; + +const Container = styled.div` + width: 650px; + height: 400px; + overflow: scroll; +`; + +export const getInlineErrorLabel = (message?: string) => { + return i18n.translate('xpack.uptime.monitorList.statusColumn.error.messageLabel', { + defaultMessage: '{message}. Click for more details.', + values: { message }, + }); +}; + +export const ERROR_LOGS_LABEL = i18n.translate('xpack.uptime.monitorList.statusColumn.error.logs', { + defaultMessage: 'Error logs', +}); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/test_now_mode/browser/browser_test_results.tsx b/x-pack/plugins/uptime/public/components/monitor_management/test_now_mode/browser/browser_test_results.tsx index c6074626bad1ed..3e798dd3fbe620 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/test_now_mode/browser/browser_test_results.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/test_now_mode/browser/browser_test_results.tsx @@ -75,7 +75,9 @@ export const BrowserTestRunResult = ({ monitorId, isMonitorSaved, onDone }: Prop initialIsOpen={true} > {isStepsLoading && {LOADING_STEPS}} - {isStepsLoadingFailed && {FAILED_TO_RUN}} + {isStepsLoadingFailed && ( + {summaryDoc?.error?.message ?? FAILED_TO_RUN} + )} {stepEnds.length > 0 && stepListData?.steps && ( 0) { summaryDocs.forEach((sDoc) => { - duration += sDoc.monitor.duration!.us; + duration += sDoc.monitor.duration?.us ?? 0; }); } + const summaryDoc = summaryDocs?.[0] as Ping; + return ( @@ -48,7 +50,9 @@ export function TestResultHeader({ doc, title, summaryDocs, journeyStarted, isCo {isCompleted ? ( - {COMPLETED_LABEL} + 0 ? 'danger' : 'success'}> + {summaryDoc?.summary?.down! > 0 ? FAILED_LABEL : COMPLETED_LABEL} + @@ -98,6 +102,10 @@ const COMPLETED_LABEL = i18n.translate('xpack.uptime.monitorManagement.completed defaultMessage: 'COMPLETED', }); +const FAILED_LABEL = i18n.translate('xpack.uptime.monitorManagement.failed', { + defaultMessage: 'FAILED', +}); + export const IN_PROGRESS_LABEL = i18n.translate('xpack.uptime.monitorManagement.inProgress', { defaultMessage: 'IN PROGRESS', }); diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_status_column.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_status_column.tsx index 60baedaa7830c0..896ab6bc662bbd 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_status_column.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_status_column.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useContext } from 'react'; +import React, { useCallback } from 'react'; import moment, { Moment } from 'moment'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; @@ -14,14 +14,13 @@ import { EuiFlexItem, EuiText, EuiToolTip, - EuiBadge, EuiSpacer, EuiHighlight, EuiHorizontalRule, } from '@elastic/eui'; import { useDispatch, useSelector } from 'react-redux'; import { parseTimestamp } from '../parse_timestamp'; -import { DataStream, Ping } from '../../../../../common/runtime_types'; +import { DataStream, Ping, PingError } from '../../../../../common/runtime_types'; import { STATUS, SHORT_TIMESPAN_LOCALE, @@ -29,22 +28,24 @@ import { SHORT_TS_LOCALE, } from '../../../../../common/constants'; -import { UptimeThemeContext } from '../../../../contexts'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { STATUS_DOWN_LABEL, STATUS_UP_LABEL } from '../../../common/translations'; import { MonitorProgress } from './progress/monitor_progress'; import { refreshedMonitorSelector } from '../../../../state/reducers/monitor_list'; import { testNowRunSelector } from '../../../../state/reducers/test_now_runs'; import { clearTestNowMonitorAction } from '../../../../state/actions'; +import { StatusBadge } from './status_badge'; interface MonitorListStatusColumnProps { configId?: string; monitorId?: string; + checkGroup?: string; status: string; monitorType: string; timestamp: string; duration?: number; summaryPings: Ping[]; + summaryError?: PingError; } const StatusColumnFlexG = styled(EuiFlexGroup)` @@ -167,15 +168,13 @@ export const MonitorListStatusColumn = ({ monitorId, status, duration, + checkGroup, + summaryError, summaryPings = [], timestamp: tsString, }: MonitorListStatusColumnProps) => { const timestamp = parseTimestamp(tsString); - const { - colors: { dangerBehindText }, - } = useContext(UptimeThemeContext); - const { statusMessage, locTooltip } = getLocationStatus(summaryPings, status); const dispatch = useDispatch(); @@ -204,12 +203,12 @@ export const MonitorListStatusColumn = ({ stopProgressTrack={stopProgressTrack} /> ) : ( - - {getHealthMessage(status)} - + )} diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/status_badge.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/status_badge.test.tsx new file mode 100644 index 00000000000000..992defffc5552e --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/status_badge.test.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen } from '@testing-library/react'; +import { StatusBadge } from './status_badge'; +import { render } from '../../../../lib/helper/rtl_helpers'; + +describe('', () => { + it('render no error for up status', () => { + render(); + + expect(screen.getByText('Up')).toBeInTheDocument(); + }); + + it('renders errors for downs state', () => { + render( + + ); + + expect(screen.getByText('Down')).toBeInTheDocument(); + expect( + screen.getByLabelText('journey did not run. Click for more details.') + ).toBeInTheDocument(); + }); + + it('renders errors for downs state for http monitor', () => { + render( + + ); + + expect(screen.getByText('Down')).toBeInTheDocument(); + expect(screen.getByLabelText('journey did not run')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/status_badge.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/status_badge.tsx new file mode 100644 index 00000000000000..fe2c7730275db8 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/status_badge.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiBadge, EuiToolTip } from '@elastic/eui'; +import React, { useContext, useState } from 'react'; +import { STATUS } from '../../../../../common/constants'; +import { getHealthMessage } from './monitor_status_column'; +import { UptimeThemeContext } from '../../../../contexts'; +import { PingError } from '../../../../../common/runtime_types'; +import { getInlineErrorLabel } from '../../../monitor_management/monitor_list/inline_error'; +import { StdErrorPopover } from '../../../monitor_management/monitor_list/stderr_logs_popover'; + +export const StatusBadge = ({ + status, + checkGroup, + summaryError, + monitorType, +}: { + status: string; + monitorType: string; + checkGroup?: string; + summaryError?: PingError; +}) => { + const { + colors: { dangerBehindText }, + } = useContext(UptimeThemeContext); + const [isOpen, setIsOpen] = useState(false); + + if (status === STATUS.UP) { + return ( + + {getHealthMessage(status)} + + ); + } + + const errorMessage = + monitorType !== 'browser' ? summaryError?.message : getInlineErrorLabel(summaryError?.message); + + const button = ( + + setIsOpen(true)} + onClickAriaLabel={errorMessage} + > + {getHealthMessage(status)} + + + ); + + if (monitorType !== 'browser') { + return button; + } + + return ( + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx index a2d823cd90af18..552256a6aff1a9 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx @@ -123,7 +123,8 @@ export const MonitorListComponent: ({ state: { timestamp, summaryPings, - monitor: { type, duration }, + monitor: { type, duration, checkGroup }, + error: summaryError, }, configId, }: MonitorSummary @@ -137,6 +138,8 @@ export const MonitorListComponent: ({ monitorType={type} duration={duration?.us} monitorId={monitorId} + checkGroup={checkGroup} + summaryError={summaryError} /> ); }, diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/stderr_logs.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/stderr_logs.tsx new file mode 100644 index 00000000000000..cef4ff550a23d4 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/stderr_logs.tsx @@ -0,0 +1,148 @@ +/* + * 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 { + EuiBasicTableColumn, + EuiButtonEmpty, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiHighlight, + EuiLink, + EuiSpacer, + EuiTitle, + formatDate, +} from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiInMemoryTable } from '@elastic/eui'; +import moment from 'moment'; +import { useSelector } from 'react-redux'; +import { useStdErrorLogs } from './use_std_error_logs'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { ClientPluginsStart } from '../../../apps/plugin'; +import { useFetcher } from '../../../../../observability/public'; +import { selectDynamicSettings } from '../../../state/selectors'; +import { Ping } from '../../../../common/runtime_types'; + +export const StdErrorLogs = ({ + configId, + checkGroup, + timestamp, + title, + summaryMessage, +}: { + configId?: string; + checkGroup?: string; + timestamp?: string; + title?: string; + summaryMessage?: string; +}) => { + const columns = [ + { + field: '@timestamp', + name: TIMESTAMP_LABEL, + sortable: true, + render: (date: string) => formatDate(date, 'dateTime'), + }, + { + field: 'synthetics.payload.message', + name: 'Message', + render: (message: string) => ( + + {message} + + ), + }, + ] as Array>; + + const { items, loading } = useStdErrorLogs({ configId, checkGroup }); + + const { discover, observability } = useKibana().services; + + const { settings } = useSelector(selectDynamicSettings); + + const { data: discoverLink } = useFetcher(async () => { + if (settings?.heartbeatIndices) { + const dataView = await observability.getAppDataView('synthetics', settings?.heartbeatIndices); + return discover.locator?.getUrl({ + query: { language: 'kuery', query: `monitor.check_group: ${checkGroup}` }, + indexPatternId: dataView?.id, + columns: ['synthetics.payload.message', 'error.message'], + timeRange: timestamp + ? { + from: moment(timestamp).subtract(10, 'minutes').toISOString(), + to: moment(timestamp).add(5, 'minutes').toISOString(), + } + : undefined, + }); + } + return ''; + }, [checkGroup, timestamp]); + + const search = { + box: { + incremental: true, + }, + }; + + return ( + <> + + + +

    {title ?? TEST_RUN_LOGS_LABEL}

    +
    +
    + + + + {VIEW_IN_DISCOVER_LABEL} + + + +
    + + +

    {summaryMessage}

    +
    + + + + + + ); +}; + +export const TIMESTAMP_LABEL = i18n.translate('xpack.uptime.monitorList.timestamp', { + defaultMessage: 'Timestamp', +}); + +export const ERROR_SUMMARY_LABEL = i18n.translate('xpack.uptime.monitorList.errorSummary', { + defaultMessage: 'Error summary', +}); + +export const VIEW_IN_DISCOVER_LABEL = i18n.translate('xpack.uptime.monitorList.viewInDiscover', { + defaultMessage: 'View in discover', +}); + +export const TEST_RUN_LOGS_LABEL = i18n.translate('xpack.uptime.monitorList.testRunLogs', { + defaultMessage: 'Test run logs', +}); diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_std_error_logs.ts b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_std_error_logs.ts new file mode 100644 index 00000000000000..fa563b2ef27282 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_std_error_logs.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useSelector } from 'react-redux'; +import { createEsParams, useEsSearch } from '../../../../../observability/public'; +import { selectDynamicSettings } from '../../../state/selectors'; +import { Ping } from '../../../../common/runtime_types'; + +export const useStdErrorLogs = ({ + configId, + checkGroup, +}: { + configId?: string; + checkGroup?: string; +}) => { + const { settings } = useSelector(selectDynamicSettings); + const { data, loading } = useEsSearch( + createEsParams({ + index: !configId && !checkGroup ? '' : settings?.heartbeatIndices, + body: { + size: 1000, + query: { + bool: { + filter: [ + { + term: { + 'synthetics.type': 'stderr', + }, + }, + ...(configId + ? [ + { + term: { + config_id: configId, + }, + }, + ] + : []), + ...(checkGroup + ? [ + { + term: { + 'monitor.check_group': checkGroup, + }, + }, + ] + : []), + ], + }, + }, + }, + }), + [settings?.heartbeatIndices], + { name: 'getStdErrLogs' } + ); + + return { + items: data?.hits.hits.map((hit) => ({ ...(hit._source as Ping), id: hit._id })) ?? [], + loading, + }; +}; diff --git a/x-pack/plugins/uptime/public/contexts/uptime_refresh_context.tsx b/x-pack/plugins/uptime/public/contexts/uptime_refresh_context.tsx index 7f81628129d3e4..12d904ae3c4b5b 100644 --- a/x-pack/plugins/uptime/public/contexts/uptime_refresh_context.tsx +++ b/x-pack/plugins/uptime/public/contexts/uptime_refresh_context.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { createContext, useMemo, useState } from 'react'; +import React, { createContext, useContext, useMemo, useState } from 'react'; interface UptimeRefreshContext { lastRefresh: number; @@ -35,3 +35,5 @@ export const UptimeRefreshContextProvider: React.FC = ({ children }) => { return ; }; + +export const useUptimeRefreshContext = () => useContext(UptimeRefreshContext); diff --git a/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx b/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx index 0ad9dbd6b06e75..d826db82517fc9 100644 --- a/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx +++ b/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx @@ -7,15 +7,18 @@ import React, { useEffect, useReducer, useCallback, Reducer } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { useParams } from 'react-router-dom'; import { useTrackPageview } from '../../../../observability/public'; import { ConfigKey } from '../../../common/runtime_types'; import { getMonitors } from '../../state/actions'; import { monitorManagementListSelector } from '../../state/selectors'; -import { - MonitorManagementList, - MonitorManagementListPageState, -} from '../../components/monitor_management/monitor_list/monitor_list'; +import { MonitorManagementListPageState } from '../../components/monitor_management/monitor_list/monitor_list'; import { useMonitorManagementBreadcrumbs } from './use_monitor_management_breadcrumbs'; +import { useInlineErrors } from '../../components/monitor_management/hooks/use_inline_errors'; +import { MonitorListTabs } from '../../components/monitor_management/monitor_list/list_tabs'; +import { AllMonitors } from '../../components/monitor_management/monitor_list/all_monitors'; +import { InvalidMonitors } from '../../components/monitor_management/monitor_list/invalid_monitors'; +import { useInvalidMonitors } from '../../components/monitor_management/hooks/use_invalid_monitors'; export const MonitorManagementPage: React.FC = () => { const [pageState, dispatchPageAction] = useReducer( @@ -47,17 +50,48 @@ export const MonitorManagementPage: React.FC = () => { const { pageIndex, pageSize, sortField, sortOrder } = pageState as MonitorManagementListPageState; + const { type: viewType } = useParams<{ type: 'all' | 'invalid' }>(); + const { errorSummaries, loading, count } = useInlineErrors({ + onlyInvalidMonitors: viewType === 'invalid', + sortField: pageState.sortField, + sortOrder: pageState.sortOrder, + }); + useEffect(() => { - dispatch(getMonitors({ page: pageIndex, perPage: pageSize, sortField, sortOrder })); - }, [dispatch, pageState, pageIndex, pageSize, sortField, sortOrder]); + if (viewType === 'all') { + dispatch(getMonitors({ page: pageIndex, perPage: pageSize, sortField, sortOrder })); + } + }, [dispatch, pageState, pageIndex, pageSize, sortField, sortOrder, viewType]); + + const { data: monitorSavedObjects, loading: objectsLoading } = useInvalidMonitors(errorSummaries); return ( - + <> + + {viewType === 'all' ? ( + + ) : ( + + )} + ); }; diff --git a/x-pack/plugins/uptime/public/pages/monitor_management/use_monitor_management_breadcrumbs.tsx b/x-pack/plugins/uptime/public/pages/monitor_management/use_monitor_management_breadcrumbs.tsx index e5784591a00fc4..834752c9961537 100644 --- a/x-pack/plugins/uptime/public/pages/monitor_management/use_monitor_management_breadcrumbs.tsx +++ b/x-pack/plugins/uptime/public/pages/monitor_management/use_monitor_management_breadcrumbs.tsx @@ -25,7 +25,8 @@ export const useMonitorManagementBreadcrumbs = ({ useBreadcrumbs([ { text: MONITOR_MANAGEMENT_CRUMB, - href: isAddMonitor || isEditMonitor ? `${appPath}/${MONITOR_MANAGEMENT_ROUTE}` : undefined, + href: + isAddMonitor || isEditMonitor ? `${appPath}/${MONITOR_MANAGEMENT_ROUTE}/all` : undefined, }, ...(isAddMonitor ? [ diff --git a/x-pack/plugins/uptime/public/routes.tsx b/x-pack/plugins/uptime/public/routes.tsx index 5d7e0a46a29d38..e68f25fcbb134e 100644 --- a/x-pack/plugins/uptime/public/routes.tsx +++ b/x-pack/plugins/uptime/public/routes.tsx @@ -237,7 +237,7 @@ const getRoutes = (config: UptimeConfig, canSave: boolean): RouteProps[] => { defaultMessage: 'Manage Monitors | {baseTitle}', values: { baseTitle }, }), - path: MONITOR_MANAGEMENT_ROUTE, + path: MONITOR_MANAGEMENT_ROUTE + '/:type', component: MonitorManagementPage, dataTestSubj: 'uptimeMonitorManagementListPage', telemetryId: UptimePage.MonitorManagement, diff --git a/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts index 5a714fd2514d87..6359a122638f24 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts @@ -90,6 +90,7 @@ export const summaryPingsToSummary = (summaryPings: Ping[]): MonitorSummary => { name: latest.monitor?.name, type: latest.monitor?.type, duration: latest.monitor?.duration, + checkGroup: latest.monitor?.check_group, }, url: latest.url ?? {}, summary: { @@ -104,6 +105,7 @@ export const summaryPingsToSummary = (summaryPings: Ping[]): MonitorSummary => { geo: { name: summaryPings.map((p) => p.observer?.geo?.name ?? '').filter((n) => n !== '') }, }, service: summaryPings.find((p) => p.service?.name)?.service, + error: latest.error, }, }; }; diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/hydrate_saved_object.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/hydrate_saved_object.ts index 3d132e74d24d5a..f240652b276913 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/hydrate_saved_object.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/hydrate_saved_object.ts @@ -43,11 +43,14 @@ export const hydrateSavedObjects = async ({ missingInfoIds ); - const updatedObjects = monitors + const updatedObjects: SyntheticsMonitorSavedObject[] = []; + monitors .filter((monitor) => missingInfoIds.includes(monitor.id)) - .map((monitor) => { + .forEach((monitor) => { let resultAttributes: Partial = monitor.attributes; + let isUpdated = false; + esDocs.forEach((doc) => { // to make sure the document is ingested after the latest update of the monitor const documentIsAfterLatestUpdate = moment(monitor.updated_at).isBefore( @@ -57,15 +60,21 @@ export const hydrateSavedObjects = async ({ if (doc.config_id !== monitor.id) return monitor; if (doc.url?.full) { + isUpdated = true; resultAttributes = { ...resultAttributes, urls: doc.url?.full }; } if (doc.url?.port) { + isUpdated = true; resultAttributes = { ...resultAttributes, ['url.port']: doc.url?.port }; } }); - - return { ...monitor, attributes: resultAttributes }; + if (isUpdated) { + updatedObjects.push({ + ...monitor, + attributes: resultAttributes, + } as SyntheticsMonitorSavedObject); + } }); await server.authSavedObjectsClient?.bulkUpdate(updatedObjects); From 70e0133691cf30dc2ba5d2815f3a48d08db8fc03 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Wed, 23 Mar 2022 16:37:48 -0400 Subject: [PATCH 60/64] [Response Ops] Change search strategy to private (#127792) * Privatize * Add test * Fix types * debug for ci * try fetching version * Use this Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/lib/get_is_kibana_request.test.ts | 34 +++++++++++ .../server/lib/get_is_kibana_request.ts | 17 ++++++ x-pack/plugins/rule_registry/server/plugin.ts | 4 +- .../server/search_strategy/index.ts | 2 +- .../search_strategy/search_strategy.test.ts | 55 ++++++++++++++++- .../server/search_strategy/search_strategy.ts | 15 ++++- x-pack/test/common/services/bsearch_secure.ts | 40 ++++++++++-- .../tests/basic/search_strategy.ts | 61 +++++++++++++++---- 8 files changed, 204 insertions(+), 24 deletions(-) create mode 100644 x-pack/plugins/rule_registry/server/lib/get_is_kibana_request.test.ts create mode 100644 x-pack/plugins/rule_registry/server/lib/get_is_kibana_request.ts diff --git a/x-pack/plugins/rule_registry/server/lib/get_is_kibana_request.test.ts b/x-pack/plugins/rule_registry/server/lib/get_is_kibana_request.test.ts new file mode 100644 index 00000000000000..7dc0f51f15f081 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/lib/get_is_kibana_request.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { getIsKibanaRequest } from './get_is_kibana_request'; + +describe('getIsKibanaRequest', () => { + it('should ensure the request has a kbn version and referer', () => { + expect( + getIsKibanaRequest({ + 'kbn-version': 'foo', + referer: 'somwhere', + }) + ).toBe(true); + }); + + it('should return false if the kbn version is missing', () => { + expect( + getIsKibanaRequest({ + referer: 'somwhere', + }) + ).toBe(false); + }); + + it('should return false if the referer is missing', () => { + expect( + getIsKibanaRequest({ + 'kbn-version': 'foo', + }) + ).toBe(false); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/lib/get_is_kibana_request.ts b/x-pack/plugins/rule_registry/server/lib/get_is_kibana_request.ts new file mode 100644 index 00000000000000..c0961b84c7c283 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/lib/get_is_kibana_request.ts @@ -0,0 +1,17 @@ +/* + * 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 { Headers } from 'kibana/server'; + +/** + * Taken from + * https://github.com/elastic/kibana/blob/ec30f2aeeb10fb64b507935e558832d3ef5abfaa/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.ts#L113-L118 + */ +export const getIsKibanaRequest = (headers?: Headers): boolean => { + // The presence of these two request headers gives us a good indication that this is a first-party request from the Kibana client. + // We can't be 100% certain, but this is a reasonable attempt. + return !!(headers && headers['kbn-version'] && headers.referer); +}; diff --git a/x-pack/plugins/rule_registry/server/plugin.ts b/x-pack/plugins/rule_registry/server/plugin.ts index 292e987879d58f..df32abcc80865a 100644 --- a/x-pack/plugins/rule_registry/server/plugin.ts +++ b/x-pack/plugins/rule_registry/server/plugin.ts @@ -29,7 +29,7 @@ import { AlertsClientFactory } from './alert_data_client/alerts_client_factory'; import { AlertsClient } from './alert_data_client/alerts_client'; import { RacApiRequestHandlerContext, RacRequestHandlerContext } from './types'; import { defineRoutes } from './routes'; -import { ruleRegistrySearchStrategyProvider } from './search_strategy'; +import { ruleRegistrySearchStrategyProvider, RULE_SEARCH_STRATEGY_NAME } from './search_strategy'; export interface RuleRegistryPluginSetupDependencies { security?: SecurityPluginSetup; @@ -115,7 +115,7 @@ export class RuleRegistryPlugin ); plugins.data.search.registerSearchStrategy( - 'ruleRegistryAlertsSearchStrategy', + RULE_SEARCH_STRATEGY_NAME, ruleRegistrySearchStrategy ); }); diff --git a/x-pack/plugins/rule_registry/server/search_strategy/index.ts b/x-pack/plugins/rule_registry/server/search_strategy/index.ts index 63f39430a55224..d6364983f2d26e 100644 --- a/x-pack/plugins/rule_registry/server/search_strategy/index.ts +++ b/x-pack/plugins/rule_registry/server/search_strategy/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { ruleRegistrySearchStrategyProvider } from './search_strategy'; +export { ruleRegistrySearchStrategyProvider, RULE_SEARCH_STRATEGY_NAME } from './search_strategy'; diff --git a/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.test.ts b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.test.ts index 2ea4b4c191c0d9..f5f7d8d164b480 100644 --- a/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.test.ts +++ b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.test.ts @@ -8,7 +8,11 @@ import { of } from 'rxjs'; import { merge } from 'lodash'; import { loggerMock } from '@kbn/logging-mocks'; import { AlertConsumers } from '@kbn/rule-data-utils'; -import { ruleRegistrySearchStrategyProvider, EMPTY_RESPONSE } from './search_strategy'; +import { + ruleRegistrySearchStrategyProvider, + EMPTY_RESPONSE, + RULE_SEARCH_STRATEGY_NAME, +} from './search_strategy'; import { ruleDataServiceMock } from '../rule_data_plugin_service/rule_data_plugin_service.mock'; import { dataPluginMock } from '../../../../../src/plugins/data/server/mocks'; import { SearchStrategyDependencies } from '../../../../../src/plugins/data/server'; @@ -18,6 +22,9 @@ import { spacesMock } from '../../../spaces/server/mocks'; import { RuleRegistrySearchRequest } from '../../common/search_strategy'; import { IndexInfo } from '../rule_data_plugin_service/index_info'; import * as getAuthzFilterImport from '../lib/get_authz_filter'; +import { getIsKibanaRequest } from '../lib/get_is_kibana_request'; + +jest.mock('../lib/get_is_kibana_request'); const getBasicResponse = (overwrites = {}) => { return merge( @@ -89,6 +96,10 @@ describe('ruleRegistrySearchStrategyProvider()', () => { return of(response); }); + (getIsKibanaRequest as jest.Mock).mockImplementation(() => { + return true; + }); + getAuthzFilterSpy = jest .spyOn(getAuthzFilterImport, 'getAuthzFilter') .mockImplementation(async () => { @@ -377,4 +388,46 @@ describe('ruleRegistrySearchStrategyProvider()', () => { (data.search.searchAsInternalUser.search as jest.Mock).mock.calls[0][0].params.body.sort ).toStrictEqual([{ test: { order: 'desc' } }]); }); + + it('should reject, to the best of our ability, public requests', async () => { + (getIsKibanaRequest as jest.Mock).mockImplementation(() => { + return false; + }); + const request: RuleRegistrySearchRequest = { + featureIds: [AlertConsumers.LOGS], + sort: [ + { + test: { + order: 'desc', + }, + }, + ], + }; + const options = {}; + const deps = { + request: {}, + }; + + const strategy = ruleRegistrySearchStrategyProvider( + data, + ruleDataService, + alerting, + logger, + security, + spaces + ); + + let err = null; + try { + await strategy + .search(request, options, deps as unknown as SearchStrategyDependencies) + .toPromise(); + } catch (e) { + err = e; + } + expect(err).not.toBeNull(); + expect(err.message).toBe( + `The ${RULE_SEARCH_STRATEGY_NAME} search strategy is currently only available for internal use.` + ); + }); }); diff --git a/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts index 8cd0a0d410c9b0..da32d68a85f86d 100644 --- a/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts +++ b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts @@ -5,6 +5,7 @@ * 2.0. */ import { map, mergeMap, catchError } from 'rxjs/operators'; +import Boom from '@hapi/boom'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { Logger } from 'src/core/server'; import { from, of } from 'rxjs'; @@ -23,11 +24,14 @@ import { Dataset } from '../rule_data_plugin_service/index_options'; import { MAX_ALERT_SEARCH_SIZE } from '../../common/constants'; import { AlertAuditAction, alertAuditEvent } from '../'; import { getSpacesFilter, getAuthzFilter } from '../lib'; +import { getIsKibanaRequest } from '../lib/get_is_kibana_request'; export const EMPTY_RESPONSE: RuleRegistrySearchResponse = { rawResponse: {} as RuleRegistrySearchResponse['rawResponse'], }; +export const RULE_SEARCH_STRATEGY_NAME = 'privateRuleRegistryAlertsSearchStrategy'; + export const ruleRegistrySearchStrategyProvider = ( data: PluginStart, ruleDataService: IRuleDataService, @@ -40,6 +44,13 @@ export const ruleRegistrySearchStrategyProvider = ( const requestUserEs = data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); return { search: (request, options, deps) => { + // We want to ensure this request came from our UI. We can't really do this + // but we have a best effort we can try + if (!getIsKibanaRequest(deps.request.headers)) { + throw Boom.notFound( + `The ${RULE_SEARCH_STRATEGY_NAME} search strategy is currently only available for internal use.` + ); + } // SIEM uses RBAC fields in their alerts but also utilizes ES DLS which // is different than every other solution so we need to special case // those requests. @@ -48,7 +59,7 @@ export const ruleRegistrySearchStrategyProvider = ( siemRequest = true; } else if (request.featureIds.includes(AlertConsumers.SIEM)) { throw new Error( - 'The ruleRegistryAlertsSearchStrategy search strategy is unable to accommodate requests containing multiple feature IDs and one of those IDs is SIEM.' + `The ${RULE_SEARCH_STRATEGY_NAME} search strategy is unable to accommodate requests containing multiple feature IDs and one of those IDs is SIEM.` ); } @@ -74,7 +85,7 @@ export const ruleRegistrySearchStrategyProvider = ( const indices: string[] = request.featureIds.reduce((accum: string[], featureId) => { if (!isValidFeatureId(featureId)) { logger.warn( - `Found invalid feature '${featureId}' while using rule registry search strategy. No alert data from this feature will be searched.` + `Found invalid feature '${featureId}' while using ${RULE_SEARCH_STRATEGY_NAME} search strategy. No alert data from this feature will be searched.` ); return accum; } diff --git a/x-pack/test/common/services/bsearch_secure.ts b/x-pack/test/common/services/bsearch_secure.ts index 622cca92aead5a..c1aa173280f541 100644 --- a/x-pack/test/common/services/bsearch_secure.ts +++ b/x-pack/test/common/services/bsearch_secure.ts @@ -29,6 +29,8 @@ const getSpaceUrlPrefix = (spaceId?: string): string => { interface SendOptions { supertestWithoutAuth: SuperTest.SuperTest; auth: { username: string; password: string }; + referer?: string; + kibanaVersion?: string; options: object; strategy: string; space?: string; @@ -38,17 +40,45 @@ export const BSecureSearchFactory = (retry: RetryService) => ({ send: async ({ supertestWithoutAuth, auth, + referer, + kibanaVersion, options, strategy, space, }: SendOptions): Promise => { const spaceUrl = getSpaceUrlPrefix(space); const { body } = await retry.try(async () => { - const result = await supertestWithoutAuth - .post(`${spaceUrl}/internal/search/${strategy}`) - .auth(auth.username, auth.password) - .set('kbn-xsrf', 'true') - .send(options); + let result; + const url = `${spaceUrl}/internal/search/${strategy}`; + if (referer && kibanaVersion) { + result = await supertestWithoutAuth + .post(url) + .auth(auth.username, auth.password) + .set('referer', referer) + .set('kbn-version', kibanaVersion) + .set('kbn-xsrf', 'true') + .send(options); + } else if (referer) { + result = await supertestWithoutAuth + .post(url) + .auth(auth.username, auth.password) + .set('referer', referer) + .set('kbn-xsrf', 'true') + .send(options); + } else if (kibanaVersion) { + result = await supertestWithoutAuth + .post(url) + .auth(auth.username, auth.password) + .set('kbn-version', kibanaVersion) + .set('kbn-xsrf', 'true') + .send(options); + } else { + result = await supertestWithoutAuth + .post(url) + .auth(auth.username, auth.password) + .set('kbn-xsrf', 'true') + .send(options); + } if (result.status === 500 || result.status === 200) { return result; } diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts index 745995588d8b3d..2c203a4ffbcd30 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts @@ -26,18 +26,28 @@ import { logsOnlySpacesAll, } from '../../../common/lib/authentication/users'; +type RuleRegistrySearchResponseWithErrors = RuleRegistrySearchResponse & { + statusCode: number; + message: string; +}; + // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - // const bsearch = getService('bsearch'); const secureBsearch = getService('secureBsearch'); const log = getService('log'); + const kbnClient = getService('kibanaServer'); const SPACE1 = 'space1'; describe('ruleRegistryAlertsSearchStrategy', () => { + let kibanaVersion: string; + before(async () => { + kibanaVersion = await kbnClient.version.get(); + }); + describe('logs', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts'); @@ -53,10 +63,12 @@ export default ({ getService }: FtrProviderContext) => { username: logsOnlySpacesAll.username, password: logsOnlySpacesAll.password, }, + referer: 'test', + kibanaVersion, options: { featureIds: [AlertConsumers.LOGS], }, - strategy: 'ruleRegistryAlertsSearchStrategy', + strategy: 'privateRuleRegistryAlertsSearchStrategy', }); expect(result.rawResponse.hits.total).to.eql(5); const consumers = result.rawResponse.hits.hits.map((hit) => { @@ -72,6 +84,8 @@ export default ({ getService }: FtrProviderContext) => { username: logsOnlySpacesAll.username, password: logsOnlySpacesAll.password, }, + referer: 'test', + kibanaVersion, options: { featureIds: [AlertConsumers.LOGS], pagination: { @@ -86,7 +100,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], }, - strategy: 'ruleRegistryAlertsSearchStrategy', + strategy: 'privateRuleRegistryAlertsSearchStrategy', }); expect(result.rawResponse.hits.total).to.eql(5); expect(result.rawResponse.hits.hits.length).to.eql(2); @@ -94,9 +108,26 @@ export default ({ getService }: FtrProviderContext) => { const second = result.rawResponse.hits.hits[1].fields?.['kibana.alert.evaluation.value']; expect(first > second).to.be(true); }); + + it('should reject public requests', async () => { + const result = await secureBsearch.send({ + supertestWithoutAuth, + auth: { + username: logsOnlySpacesAll.username, + password: logsOnlySpacesAll.password, + }, + options: { + featureIds: [AlertConsumers.LOGS], + }, + strategy: 'privateRuleRegistryAlertsSearchStrategy', + }); + expect(result.statusCode).to.be(500); + expect(result.message).to.be( + `The privateRuleRegistryAlertsSearchStrategy search strategy is currently only available for internal use.` + ); + }); }); - // TODO: need xavier's help here describe('siem', () => { before(async () => { await createSignalsIndex(supertest, log); @@ -126,10 +157,12 @@ export default ({ getService }: FtrProviderContext) => { username: obsOnlySpacesAllEsRead.username, password: obsOnlySpacesAllEsRead.password, }, + referer: 'test', + kibanaVersion, options: { featureIds: [AlertConsumers.SIEM], }, - strategy: 'ruleRegistryAlertsSearchStrategy', + strategy: 'privateRuleRegistryAlertsSearchStrategy', }); expect(result.rawResponse.hits.total).to.eql(1); const consumers = result.rawResponse.hits.hits.map( @@ -139,24 +172,22 @@ export default ({ getService }: FtrProviderContext) => { }); it('should throw an error when trying to to search for more than just siem', async () => { - type RuleRegistrySearchResponseWithErrors = RuleRegistrySearchResponse & { - statusCode: number; - message: string; - }; const result = await secureBsearch.send({ supertestWithoutAuth, auth: { username: obsOnlySpacesAllEsRead.username, password: obsOnlySpacesAllEsRead.password, }, + referer: 'test', + kibanaVersion, options: { featureIds: [AlertConsumers.SIEM, AlertConsumers.LOGS], }, - strategy: 'ruleRegistryAlertsSearchStrategy', + strategy: 'privateRuleRegistryAlertsSearchStrategy', }); expect(result.statusCode).to.be(500); expect(result.message).to.be( - `The ruleRegistryAlertsSearchStrategy search strategy is unable to accommodate requests containing multiple feature IDs and one of those IDs is SIEM.` + `The privateRuleRegistryAlertsSearchStrategy search strategy is unable to accommodate requests containing multiple feature IDs and one of those IDs is SIEM.` ); }); }); @@ -176,10 +207,12 @@ export default ({ getService }: FtrProviderContext) => { username: obsOnlySpacesAll.username, password: obsOnlySpacesAll.password, }, + referer: 'test', + kibanaVersion, options: { featureIds: [AlertConsumers.APM], }, - strategy: 'ruleRegistryAlertsSearchStrategy', + strategy: 'privateRuleRegistryAlertsSearchStrategy', space: SPACE1, }); expect(result.rawResponse.hits.total).to.eql(2); @@ -198,10 +231,12 @@ export default ({ getService }: FtrProviderContext) => { username: obsOnlySpacesAll.username, password: obsOnlySpacesAll.password, }, + referer: 'test', + kibanaVersion, options: { featureIds: [], }, - strategy: 'ruleRegistryAlertsSearchStrategy', + strategy: 'privateRuleRegistryAlertsSearchStrategy', }); expect(result.rawResponse).to.eql({}); }); From 5b4642b3658af666fcba6d5176b1dfe4202b84c0 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Wed, 23 Mar 2022 16:06:40 -0500 Subject: [PATCH 61/64] [build] Cross compile docker images (#128272) * [build] Cross compile docker images * typo * debug * Revert "[build] Cross compile docker images" This reverts commit 621780eb1d85076893e8a45b000b9886126c3153. * revert * support docker-cross-compile flag * fix types/tests * fix more tests * download cloud dependencies based on cross compile flag * fix array * fix more tests --- src/dev/build/args.test.ts | 7 +++++ src/dev/build/args.ts | 3 ++ src/dev/build/build_distributables.ts | 1 + src/dev/build/cli.ts | 1 + src/dev/build/lib/build.test.ts | 1 + src/dev/build/lib/config.test.ts | 1 + src/dev/build/lib/config.ts | 11 ++++++++ src/dev/build/lib/runner.test.ts | 1 + .../tasks/download_cloud_dependencies.ts | 28 +++++++++++-------- .../nodejs/download_node_builds_task.test.ts | 1 + .../nodejs/extract_node_builds_task.test.ts | 1 + .../verify_existing_node_builds_task.test.ts | 1 + .../tasks/os_packages/docker_generator/run.ts | 3 +- .../templates/build_docker_sh.template.ts | 3 +- 14 files changed, 50 insertions(+), 13 deletions(-) diff --git a/src/dev/build/args.test.ts b/src/dev/build/args.test.ts index c06c13230c63f7..b0f39840ba4400 100644 --- a/src/dev/build/args.test.ts +++ b/src/dev/build/args.test.ts @@ -36,6 +36,7 @@ it('build default and oss dist for current platform, without packages, by defaul "createGenericFolders": true, "createPlatformFolders": true, "createRpmPackage": false, + "dockerCrossCompile": false, "dockerPush": false, "dockerTagQualifier": null, "downloadCloudDependencies": true, @@ -67,6 +68,7 @@ it('builds packages if --all-platforms is passed', () => { "createGenericFolders": true, "createPlatformFolders": true, "createRpmPackage": true, + "dockerCrossCompile": false, "dockerPush": false, "dockerTagQualifier": null, "downloadCloudDependencies": true, @@ -98,6 +100,7 @@ it('limits packages if --rpm passed with --all-platforms', () => { "createGenericFolders": true, "createPlatformFolders": true, "createRpmPackage": true, + "dockerCrossCompile": false, "dockerPush": false, "dockerTagQualifier": null, "downloadCloudDependencies": true, @@ -129,6 +132,7 @@ it('limits packages if --deb passed with --all-platforms', () => { "createGenericFolders": true, "createPlatformFolders": true, "createRpmPackage": false, + "dockerCrossCompile": false, "dockerPush": false, "dockerTagQualifier": null, "downloadCloudDependencies": true, @@ -161,6 +165,7 @@ it('limits packages if --docker passed with --all-platforms', () => { "createGenericFolders": true, "createPlatformFolders": true, "createRpmPackage": false, + "dockerCrossCompile": false, "dockerPush": false, "dockerTagQualifier": null, "downloadCloudDependencies": true, @@ -200,6 +205,7 @@ it('limits packages if --docker passed with --skip-docker-ubi and --all-platform "createGenericFolders": true, "createPlatformFolders": true, "createRpmPackage": false, + "dockerCrossCompile": false, "dockerPush": false, "dockerTagQualifier": null, "downloadCloudDependencies": true, @@ -232,6 +238,7 @@ it('limits packages if --all-platforms passed with --skip-docker-ubuntu', () => "createGenericFolders": true, "createPlatformFolders": true, "createRpmPackage": true, + "dockerCrossCompile": false, "dockerPush": false, "dockerTagQualifier": null, "downloadCloudDependencies": true, diff --git a/src/dev/build/args.ts b/src/dev/build/args.ts index 03fe49b72c9544..2bad2c0721e2e0 100644 --- a/src/dev/build/args.ts +++ b/src/dev/build/args.ts @@ -22,6 +22,7 @@ export function readCliArgs(argv: string[]) { 'skip-os-packages', 'rpm', 'deb', + 'docker-cross-compile', 'docker-images', 'docker-push', 'skip-docker-contexts', @@ -52,6 +53,7 @@ export function readCliArgs(argv: string[]) { rpm: null, deb: null, 'docker-images': null, + 'docker-cross-compile': false, 'docker-push': false, 'docker-tag-qualifier': null, 'version-qualifier': '', @@ -112,6 +114,7 @@ export function readCliArgs(argv: string[]) { const buildOptions: BuildOptions = { isRelease: Boolean(flags.release), versionQualifier: flags['version-qualifier'], + dockerCrossCompile: Boolean(flags['docker-cross-compile']), dockerPush: Boolean(flags['docker-push']), dockerTagQualifier: flags['docker-tag-qualifier'], initialize: !Boolean(flags['skip-initialize']), diff --git a/src/dev/build/build_distributables.ts b/src/dev/build/build_distributables.ts index 4fb849988cb607..d2b2d24667bce5 100644 --- a/src/dev/build/build_distributables.ts +++ b/src/dev/build/build_distributables.ts @@ -13,6 +13,7 @@ import * as Tasks from './tasks'; export interface BuildOptions { isRelease: boolean; + dockerCrossCompile: boolean; dockerPush: boolean; dockerTagQualifier: string | null; downloadFreshNode: boolean; diff --git a/src/dev/build/cli.ts b/src/dev/build/cli.ts index ffcbb68215ab7c..561e2aea5c15d3 100644 --- a/src/dev/build/cli.ts +++ b/src/dev/build/cli.ts @@ -39,6 +39,7 @@ if (showHelp) { --rpm {dim Only build the rpm packages} --deb {dim Only build the deb packages} --docker-images {dim Only build the Docker images} + --docker-cross-compile {dim Produce arm64 and amd64 Docker images} --docker-contexts {dim Only build the Docker build contexts} --skip-docker-ubi {dim Don't build the docker ubi image} --skip-docker-ubuntu {dim Don't build the docker ubuntu image} diff --git a/src/dev/build/lib/build.test.ts b/src/dev/build/lib/build.test.ts index 8ea2a20d839600..3da87ff13b1eea 100644 --- a/src/dev/build/lib/build.test.ts +++ b/src/dev/build/lib/build.test.ts @@ -32,6 +32,7 @@ const config = new Config( buildSha: 'abcd1234', buildVersion: '8.0.0', }, + false, '', false, true diff --git a/src/dev/build/lib/config.test.ts b/src/dev/build/lib/config.test.ts index 3f90c8738d8e21..2195406270bddc 100644 --- a/src/dev/build/lib/config.test.ts +++ b/src/dev/build/lib/config.test.ts @@ -29,6 +29,7 @@ const setup = async ({ targetAllPlatforms = true }: { targetAllPlatforms?: boole return await Config.create({ isRelease: true, targetAllPlatforms, + dockerCrossCompile: false, dockerPush: false, dockerTagQualifier: '', }); diff --git a/src/dev/build/lib/config.ts b/src/dev/build/lib/config.ts index 650af04dfd54bb..2bab1d28f9ef70 100644 --- a/src/dev/build/lib/config.ts +++ b/src/dev/build/lib/config.ts @@ -17,6 +17,7 @@ interface Options { isRelease: boolean; targetAllPlatforms: boolean; versionQualifier?: string; + dockerCrossCompile: boolean; dockerTagQualifier: string | null; dockerPush: boolean; } @@ -35,6 +36,7 @@ export class Config { isRelease, targetAllPlatforms, versionQualifier, + dockerCrossCompile, dockerTagQualifier, dockerPush, }: Options) { @@ -51,6 +53,7 @@ export class Config { versionQualifier, pkg, }), + dockerCrossCompile, dockerTagQualifier, dockerPush, isRelease @@ -63,6 +66,7 @@ export class Config { private readonly nodeVersion: string, private readonly repoRoot: string, private readonly versionInfo: VersionInfo, + private readonly dockerCrossCompile: boolean, private readonly dockerTagQualifier: string | null, private readonly dockerPush: boolean, public readonly isRelease: boolean @@ -96,6 +100,13 @@ export class Config { return this.dockerPush; } + /** + * Get docker cross compile + */ + getDockerCrossCompile() { + return this.dockerCrossCompile; + } + /** * Convert an absolute path to a relative path, based from the repo */ diff --git a/src/dev/build/lib/runner.test.ts b/src/dev/build/lib/runner.test.ts index 7c49c35446833b..94ff3cb3381763 100644 --- a/src/dev/build/lib/runner.test.ts +++ b/src/dev/build/lib/runner.test.ts @@ -50,6 +50,7 @@ const setup = async () => { isRelease: true, targetAllPlatforms: true, versionQualifier: '-SNAPSHOT', + dockerCrossCompile: false, dockerPush: false, dockerTagQualifier: '', }); diff --git a/src/dev/build/tasks/download_cloud_dependencies.ts b/src/dev/build/tasks/download_cloud_dependencies.ts index 6ecc09c21ddce6..31873550f6b4ac 100644 --- a/src/dev/build/tasks/download_cloud_dependencies.ts +++ b/src/dev/build/tasks/download_cloud_dependencies.ts @@ -20,18 +20,24 @@ export const DownloadCloudDependencies: Task = { const version = config.getBuildVersion(); const buildId = id.match(/[0-9]\.[0-9]\.[0-9]-[0-9a-z]{8}/); const buildIdUrl = buildId ? `${buildId[0]}/` : ''; - const architecture = process.arch === 'arm64' ? 'arm64' : 'x86_64'; - const url = `https://${subdomain}-no-kpi.elastic.co/${buildIdUrl}downloads/beats/${beat}/${beat}-${version}-linux-${architecture}.tar.gz`; - const checksum = await downloadToString({ log, url: url + '.sha512', expectStatus: 200 }); - const destination = config.resolveFromRepo('.beats', Path.basename(url)); - return downloadToDisk({ - log, - url, - destination, - shaChecksum: checksum.split(' ')[0], - shaAlgorithm: 'sha512', - maxAttempts: 3, + + const localArchitecture = [process.arch === 'arm64' ? 'arm64' : 'x86_64']; + const allArchitectures = ['arm64', 'x86_64']; + const architectures = config.getDockerCrossCompile() ? allArchitectures : localArchitecture; + const downloads = architectures.map(async (arch) => { + const url = `https://${subdomain}-no-kpi.elastic.co/${buildIdUrl}downloads/beats/${beat}/${beat}-${version}-linux-${arch}.tar.gz`; + const checksum = await downloadToString({ log, url: url + '.sha512', expectStatus: 200 }); + const destination = config.resolveFromRepo('.beats', Path.basename(url)); + return downloadToDisk({ + log, + url, + destination, + shaChecksum: checksum.split(' ')[0], + shaAlgorithm: 'sha512', + maxAttempts: 3, + }); }); + return Promise.all(downloads); }; let buildId = ''; diff --git a/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts b/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts index b1309bd05c6032..c3b9cd5f8c6b1c 100644 --- a/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts +++ b/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts @@ -39,6 +39,7 @@ async function setup({ failOnUrl }: { failOnUrl?: string } = {}) { const config = await Config.create({ isRelease: true, targetAllPlatforms: true, + dockerCrossCompile: false, dockerPush: false, dockerTagQualifier: '', }); diff --git a/src/dev/build/tasks/nodejs/extract_node_builds_task.test.ts b/src/dev/build/tasks/nodejs/extract_node_builds_task.test.ts index fb0891c24f3b0e..0041829984aa7b 100644 --- a/src/dev/build/tasks/nodejs/extract_node_builds_task.test.ts +++ b/src/dev/build/tasks/nodejs/extract_node_builds_task.test.ts @@ -43,6 +43,7 @@ async function setup() { const config = await Config.create({ isRelease: true, targetAllPlatforms: true, + dockerCrossCompile: false, dockerPush: false, dockerTagQualifier: '', }); diff --git a/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts b/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts index 3a71a2b06fe918..85458c29ddcff8 100644 --- a/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts +++ b/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts @@ -48,6 +48,7 @@ async function setup(actualShaSums?: Record) { const config = await Config.create({ isRelease: true, targetAllPlatforms: true, + dockerCrossCompile: false, dockerPush: false, dockerTagQualifier: '', }); diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts index 332605e926537b..3152f07628fc96 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -76,6 +76,7 @@ export async function runDockerGenerator( const dockerPush = config.getDockerPush(); const dockerTagQualifier = config.getDockerTagQualfiier(); + const dockerCrossCompile = config.getDockerCrossCompile(); const publicArtifactSubdomain = config.isRelease ? 'artifacts' : 'snapshots-no-kpi'; const scope: TemplateContext = { @@ -110,7 +111,7 @@ export async function runDockerGenerator( arm64: 'aarch64', }; const buildArchitectureSupported = hostTarget[process.arch] === flags.architecture; - if (flags.architecture && !buildArchitectureSupported) { + if (flags.architecture && !buildArchitectureSupported && !dockerCrossCompile) { return; } diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts index de267055665854..a14de2a0581ff6 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts @@ -23,6 +23,7 @@ function generator({ const dockerTargetName = `${imageTag}${imageFlavor}:${version}${ dockerTagQualifier ? '-' + dockerTagQualifier : '' }`; + const dockerArchitecture = architecture === 'aarch64' ? 'linux/arm64' : 'linux/amd64'; return dedent(` #!/usr/bin/env bash # @@ -59,7 +60,7 @@ function generator({ retry_docker_pull ${baseOSImage} echo "Building: kibana${imageFlavor}-docker"; \\ - docker build -t ${dockerTargetName} -f Dockerfile . || exit 1; + docker buildx build --platform ${dockerArchitecture} -t ${dockerTargetName} -f Dockerfile . || exit 1; docker save ${dockerTargetName} | gzip -c > ${dockerTargetFilename} From cab3041613b3015cd3399d27ca4673cdedb4d1c7 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 23 Mar 2022 17:12:46 -0400 Subject: [PATCH 62/64] [Fleet] Do not allow to edit output for managed policies (#128298) --- .../agent_policy_advanced_fields/index.tsx | 2 + .../server/routes/agent_policy/handlers.ts | 4 +- .../fleet/server/services/agent_policy.ts | 23 +++++- .../server/services/preconfiguration.test.ts | 5 +- .../fleet/server/services/preconfiguration.ts | 15 +++- .../server/types/rest_spec/agent_policy.ts | 4 +- .../apis/agent_policy/agent_policy.ts | 77 +++++++++++++------ .../apis/package_policy/delete.ts | 2 + 8 files changed, 102 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx index 1ba7f09d0333de..9fdcc0f73297f5 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx @@ -309,6 +309,7 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent = isInvalid={Boolean(touchedFields.data_output_id && validation.data_output_id)} > = isInvalid={Boolean(touchedFields.monitoring_output_id && validation.monitoring_output_id)} > , - options?: { user?: AuthenticatedUser } + options?: { user?: AuthenticatedUser; force?: boolean } ): Promise { if (agentPolicy.name) { await this.requireUniqueName(soClient, { @@ -352,6 +354,23 @@ class AgentPolicyService { name: agentPolicy.name, }); } + + const existingAgentPolicy = await this.get(soClient, id, true); + + if (!existingAgentPolicy) { + throw new Error('Agent policy not found'); + } + + if (existingAgentPolicy.is_managed && !options?.force) { + Object.entries(agentPolicy) + .filter(([key]) => !KEY_EDITABLE_FOR_MANAGED_POLICIES.includes(key)) + .forEach(([key, val]) => { + if (!isEqual(existingAgentPolicy[key as keyof AgentPolicy], val)) { + throw new HostedAgentPolicyRestrictionRelatedError(`Cannot update ${key}`); + } + }); + } + return this._update(soClient, esClient, id, agentPolicy, options?.user); } diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index 27919d7bf10111..862b589896793c 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -689,7 +689,10 @@ describe('policy preconfiguration', () => { name: 'Renamed Test policy', description: 'Renamed Test policy description', unenroll_timeout: 999, - }) + }), + { + force: true, + } ); expect(policies.length).toEqual(1); expect(policies[0].id).toBe('test-id'); diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index 6f8c8bbc6a20d6..c11925fa8f2f3e 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -159,7 +159,10 @@ export async function ensurePreconfiguredPackagesAndPolicies( soClient, esClient, String(preconfiguredAgentPolicy.id), - fields + fields, + { + force: true, + } ); return { created, policy: updatedPolicy }; } @@ -254,7 +257,15 @@ export async function ensurePreconfiguredPackagesAndPolicies( // Add the is_managed flag after configuring package policies to avoid errors if (shouldAddIsManagedFlag) { - await agentPolicyService.update(soClient, esClient, policy!.id, { is_managed: true }); + await agentPolicyService.update( + soClient, + esClient, + policy!.id, + { is_managed: true }, + { + force: true, + } + ); } } } diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts index 64d142f150bfd0..042129e1e09144 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts @@ -32,7 +32,9 @@ export const CreateAgentPolicyRequestSchema = { export const UpdateAgentPolicyRequestSchema = { ...GetOneAgentPolicyRequestSchema, - body: NewAgentPolicySchema, + body: NewAgentPolicySchema.extends({ + force: schema.maybe(schema.boolean()), + }), }; export const CopyAgentPolicyRequestSchema = { diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts index 0e3cd9796626d6..6c2c2c7bc8b483 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts @@ -478,6 +478,38 @@ export default function (providerContext: FtrProviderContext) { }); }); + it('should return a 409 if policy already exists with name given', async () => { + const sharedBody = { + name: 'Initial name', + description: 'Initial description', + namespace: 'default', + }; + + await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send(sharedBody) + .expect(200); + + const { body } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send(sharedBody) + .expect(409); + + expect(body.message).to.match(/already exists?/); + + // same name, different namespace + sharedBody.namespace = 'different'; + await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send(sharedBody) + .expect(409); + + expect(body.message).to.match(/already exists?/); + }); + it('sets given is_managed value', async () => { const { body: { item: createdPolicy }, @@ -504,6 +536,7 @@ export default function (providerContext: FtrProviderContext) { name: 'TEST2', namespace: 'default', is_managed: false, + force: true, }) .expect(200); @@ -513,36 +546,33 @@ export default function (providerContext: FtrProviderContext) { expect(policy2.is_managed).to.equal(false); }); - it('should return a 409 if policy already exists with name given', async () => { - const sharedBody = { - name: 'Initial name', - description: 'Initial description', - namespace: 'default', - }; - - await supertest + it('should return a 400 if trying to update a managed policy', async () => { + const { + body: { item: originalPolicy }, + } = await supertest .post(`/api/fleet/agent_policies`) .set('kbn-xsrf', 'xxxx') - .send(sharedBody) + .send({ + name: `Managed policy ${Date.now()}`, + description: 'Initial description', + namespace: 'default', + is_managed: true, + }) .expect(200); const { body } = await supertest - .post(`/api/fleet/agent_policies`) + .put(`/api/fleet/agent_policies/${originalPolicy.id}`) .set('kbn-xsrf', 'xxxx') - .send(sharedBody) - .expect(409); - - expect(body.message).to.match(/already exists?/); - - // same name, different namespace - sharedBody.namespace = 'different'; - await supertest - .post(`/api/fleet/agent_policies`) - .set('kbn-xsrf', 'xxxx') - .send(sharedBody) - .expect(409); + .send({ + name: 'Updated name', + description: 'Initial description', + namespace: 'default', + }) + .expect(400); - expect(body.message).to.match(/already exists?/); + expect(body.message).to.equal( + 'Cannot update name in Fleet because the agent policy is managed by an external orchestration solution, such as Elastic Cloud, Kubernetes, etc. Please make changes using your orchestration solution.' + ); }); }); @@ -586,6 +616,7 @@ export default function (providerContext: FtrProviderContext) { name: 'Regular policy', namespace: 'default', is_managed: false, + force: true, }) .expect(200); diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts b/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts index 5a5fb68a1dbc79..1f7377ba189ba8 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts @@ -48,6 +48,7 @@ export default function (providerContext: FtrProviderContext) { name: 'Test policy', namespace: 'default', is_managed: false, + force: true, }); } } @@ -138,6 +139,7 @@ export default function (providerContext: FtrProviderContext) { name: agentPolicy.name, namespace: agentPolicy.namespace, is_managed: false, + force: true, }) .expect(200); }); From 2f9e6eeacfac1d53a8ed91c8d7ddfe845a4e12cd Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 23 Mar 2022 22:16:50 +0100 Subject: [PATCH 63/64] [Lens] Manual Annotations (#126456) * Add event annotation service structure * adding annotation layer to lens. passing event annotation service * simplify initial Dimensions * add annotations to lens * no datasource layer * group the annotations into numerical icons * color icons in tooltip, add the annotation icon, fix date interval bug * display old time axis for annotations * error in annotation dimension when date histogram is removed * refactor: use the same methods for annotations and reference lines * wip * only check activeData for dataLayers * added new icons for annotations * refactor icons * uniqueLabels * unique Labels * diff config from args * change timestamp format * added expression event_annotation_group * names refactor * ea service adding help descriptions * rotate icon * added tests * fix button problem * dnd problem * dnd fix * tests for dimension trigger * tests for unique labels * [CI] Auto-commit changed files from 'node scripts/build_plugin_list_docs' * type * add new button test * remove noDatasource from config (only needed when initializing a layer or dimension in getSupportedLayers) * addressing Joe's and Michael comments * remove hexagon and square, address Stratoula's feedback * stroke for icons & icon fill * fix tests * fix small things * align the set with tsvb * align IconSelect * fix i18nrc * Update src/plugins/event_annotation/public/event_annotation_service/index.tsx Co-authored-by: Alexey Antonov * refactor empty button * CR * date cr * remove DimensionEditorSection * change to emptyShade for traingle fill * Update x-pack/plugins/lens/public/app_plugin/app.scss Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Alexey Antonov --- .i18nrc.json | 17 +- docs/developer/plugin-list.asciidoc | 4 + packages/kbn-optimizer/limits.yml | 1 + src/plugins/event_annotation/README.md | 3 + .../common/event_annotation_group/index.ts | 52 +++ src/plugins/event_annotation/common/index.ts | 13 + .../common/manual_event_annotation/index.ts | 82 +++++ .../common/manual_event_annotation/types.ts | 15 + src/plugins/event_annotation/common/types.ts | 29 ++ src/plugins/event_annotation/jest.config.js | 18 + src/plugins/event_annotation/kibana.json | 17 + .../public/event_annotation_service/README.md | 3 + .../event_annotation_service/helpers.ts | 9 + .../public/event_annotation_service/index.tsx | 20 ++ .../event_annotation_service/service.tsx | 49 +++ .../public/event_annotation_service/types.ts | 14 + src/plugins/event_annotation/public/index.ts | 17 + src/plugins/event_annotation/public/mocks.ts | 12 + src/plugins/event_annotation/public/plugin.ts | 39 +++ src/plugins/event_annotation/server/index.ts | 10 + src/plugins/event_annotation/server/plugin.ts | 30 ++ src/plugins/event_annotation/tsconfig.json | 22 ++ x-pack/plugins/lens/common/constants.ts | 1 + .../layer_config/annotation_layer_config.ts | 67 ++++ .../xy_chart/layer_config/index.ts | 7 +- .../common/expressions/xy_chart/xy_args.ts | 5 +- .../common/expressions/xy_chart/xy_chart.ts | 8 +- x-pack/plugins/lens/kibana.json | 3 +- .../plugins/lens/public/app_plugin/app.scss | 7 + .../public/assets/annotation_icons/circle.tsx | 31 ++ .../public/assets/annotation_icons/index.tsx | 9 + .../assets/annotation_icons/triangle.tsx | 30 ++ .../public/assets/chart_bar_annotations.tsx | 37 ++ .../buttons/draggable_dimension_button.tsx | 5 +- .../buttons/drop_targets_utils.tsx | 12 +- .../buttons/empty_dimension_button.tsx | 79 +++-- .../config_panel/config_panel.test.tsx | 75 +++- .../config_panel/config_panel.tsx | 94 +++--- .../config_panel/layer_panel.test.tsx | 82 +++-- .../editor_frame/config_panel/layer_panel.tsx | 233 +++++++------ x-pack/plugins/lens/public/expressions.ts | 2 + .../dimension_editor.tsx | 136 ++++---- .../droppable/get_drop_props.ts | 13 +- .../indexpattern.test.ts | 4 - .../indexpattern_datasource/indexpattern.tsx | 2 +- .../lens/public/pie_visualization/toolbar.tsx | 24 +- x-pack/plugins/lens/public/plugin.ts | 14 +- .../shared_components/dimension_section.scss | 24 ++ .../shared_components/dimension_section.tsx | 29 ++ .../lens/public/shared_components/index.ts | 1 + .../public/state_management/lens_slice.ts | 89 +++-- x-pack/plugins/lens/public/types.ts | 39 ++- .../visualizations/gauge/visualization.tsx | 6 - .../__snapshots__/expression.test.tsx.snap | 213 ++++++++++++ .../annotations/config_panel/icon_set.ts | 97 ++++++ .../annotations/config_panel/index.scss | 3 + .../annotations/config_panel/index.tsx | 186 ++++++++++ .../annotations/expression.scss | 37 ++ .../annotations/expression.tsx | 233 +++++++++++++ .../annotations/helpers.test.ts | 210 ++++++++++++ .../xy_visualization/annotations/helpers.tsx | 240 +++++++++++++ .../xy_visualization/annotations_helpers.tsx | 253 ++++++++++++++ .../xy_visualization/color_assignment.ts | 35 +- .../xy_visualization/expression.test.tsx | 238 +++++++++++-- .../public/xy_visualization/expression.tsx | 110 ++++-- .../expression_reference_lines.tsx | 228 ++----------- .../lens/public/xy_visualization/index.ts | 7 +- .../reference_line_helpers.tsx | 24 +- .../public/xy_visualization/state_helpers.ts | 5 +- .../xy_visualization/to_expression.test.ts | 2 + .../public/xy_visualization/to_expression.ts | 179 +++++++--- .../xy_visualization/visualization.test.ts | 319 +++++++++++++++++- .../public/xy_visualization/visualization.tsx | 111 ++++-- .../visualization_helpers.tsx | 41 ++- .../xy_config_panel/color_picker.tsx | 9 +- .../xy_config_panel/layer_header.tsx | 16 +- .../xy_config_panel/reference_line_panel.tsx | 1 + .../xy_config_panel/shared/icon_select.tsx | 32 +- .../shared/line_style_settings.tsx | 7 +- .../shared/marker_decoration_settings.tsx | 33 +- .../xy_visualization/xy_suggestions.test.ts | 58 +++- .../public/xy_visualization/xy_suggestions.ts | 5 +- .../lens/server/expressions/expressions.ts | 2 + x-pack/plugins/lens/tsconfig.json | 110 ++++-- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 87 files changed, 3885 insertions(+), 806 deletions(-) create mode 100644 src/plugins/event_annotation/README.md create mode 100644 src/plugins/event_annotation/common/event_annotation_group/index.ts create mode 100644 src/plugins/event_annotation/common/index.ts create mode 100644 src/plugins/event_annotation/common/manual_event_annotation/index.ts create mode 100644 src/plugins/event_annotation/common/manual_event_annotation/types.ts create mode 100644 src/plugins/event_annotation/common/types.ts create mode 100644 src/plugins/event_annotation/jest.config.js create mode 100644 src/plugins/event_annotation/kibana.json create mode 100644 src/plugins/event_annotation/public/event_annotation_service/README.md create mode 100644 src/plugins/event_annotation/public/event_annotation_service/helpers.ts create mode 100644 src/plugins/event_annotation/public/event_annotation_service/index.tsx create mode 100644 src/plugins/event_annotation/public/event_annotation_service/service.tsx create mode 100644 src/plugins/event_annotation/public/event_annotation_service/types.ts create mode 100644 src/plugins/event_annotation/public/index.ts create mode 100644 src/plugins/event_annotation/public/mocks.ts create mode 100644 src/plugins/event_annotation/public/plugin.ts create mode 100644 src/plugins/event_annotation/server/index.ts create mode 100644 src/plugins/event_annotation/server/plugin.ts create mode 100644 src/plugins/event_annotation/tsconfig.json create mode 100644 x-pack/plugins/lens/common/expressions/xy_chart/layer_config/annotation_layer_config.ts create mode 100644 x-pack/plugins/lens/public/assets/annotation_icons/circle.tsx create mode 100644 x-pack/plugins/lens/public/assets/annotation_icons/index.tsx create mode 100644 x-pack/plugins/lens/public/assets/annotation_icons/triangle.tsx create mode 100644 x-pack/plugins/lens/public/assets/chart_bar_annotations.tsx create mode 100644 x-pack/plugins/lens/public/shared_components/dimension_section.scss create mode 100644 x-pack/plugins/lens/public/shared_components/dimension_section.tsx create mode 100644 x-pack/plugins/lens/public/xy_visualization/annotations/config_panel/icon_set.ts create mode 100644 x-pack/plugins/lens/public/xy_visualization/annotations/config_panel/index.scss create mode 100644 x-pack/plugins/lens/public/xy_visualization/annotations/config_panel/index.tsx create mode 100644 x-pack/plugins/lens/public/xy_visualization/annotations/expression.scss create mode 100644 x-pack/plugins/lens/public/xy_visualization/annotations/expression.tsx create mode 100644 x-pack/plugins/lens/public/xy_visualization/annotations/helpers.test.ts create mode 100644 x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx create mode 100644 x-pack/plugins/lens/public/xy_visualization/annotations_helpers.tsx diff --git a/.i18nrc.json b/.i18nrc.json index 402932902f249c..71b68d2c51d859 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -31,6 +31,7 @@ "expressions": "src/plugins/expressions", "expressionShape": "src/plugins/expression_shape", "expressionTagcloud": "src/plugins/chart_expressions/expression_tagcloud", + "eventAnnotation": "src/plugins/event_annotation", "fieldFormats": "src/plugins/field_formats", "flot": "packages/kbn-flot-charts/lib", "home": "src/plugins/home", @@ -50,7 +51,10 @@ "kibana-react": "src/plugins/kibana_react", "kibanaOverview": "src/plugins/kibana_overview", "lists": "packages/kbn-securitysolution-list-utils/src", - "management": ["src/legacy/core_plugins/management", "src/plugins/management"], + "management": [ + "src/legacy/core_plugins/management", + "src/plugins/management" + ], "monaco": "packages/kbn-monaco/src", "navigation": "src/plugins/navigation", "newsfeed": "src/plugins/newsfeed", @@ -62,8 +66,13 @@ "sharedUX": "src/plugins/shared_ux", "sharedUXComponents": "packages/kbn-shared-ux-components/src", "statusPage": "src/legacy/core_plugins/status_page", - "telemetry": ["src/plugins/telemetry", "src/plugins/telemetry_management_section"], - "timelion": ["src/plugins/vis_types/timelion"], + "telemetry": [ + "src/plugins/telemetry", + "src/plugins/telemetry_management_section" + ], + "timelion": [ + "src/plugins/vis_types/timelion" + ], "uiActions": "src/plugins/ui_actions", "uiActionsExamples": "examples/ui_action_examples", "usageCollection": "src/plugins/usage_collection", @@ -83,4 +92,4 @@ "visualizations": "src/plugins/visualizations" }, "translations": [] -} +} \ No newline at end of file diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index bf81ab1e0bec44..aefaf4eab40fa6 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -94,6 +94,10 @@ This API doesn't support angular, for registering angular dev tools, bootstrap a |This plugin contains reusable code in the form of self-contained modules (or libraries). Each of these modules exports a set of functionality relevant to the domain of the module. +|{kib-repo}blob/{branch}/src/plugins/event_annotation/README.md[eventAnnotation] +|The Event Annotation service contains expressions for event annotations + + |{kib-repo}blob/{branch}/src/plugins/expression_error/README.md[expressionError] |Expression Error plugin adds an error renderer to the expression plugin. The renderer will display the error image. diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 396ffd45992842..526c1ff5dad826 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -124,3 +124,4 @@ pageLoadAssetSize: sessionView: 77750 cloudSecurityPosture: 19109 visTypeGauge: 24113 + eventAnnotation: 19334 diff --git a/src/plugins/event_annotation/README.md b/src/plugins/event_annotation/README.md new file mode 100644 index 00000000000000..a7a85d3ab36415 --- /dev/null +++ b/src/plugins/event_annotation/README.md @@ -0,0 +1,3 @@ +# Event Annotation service + +The Event Annotation service contains expressions for event annotations diff --git a/src/plugins/event_annotation/common/event_annotation_group/index.ts b/src/plugins/event_annotation/common/event_annotation_group/index.ts new file mode 100644 index 00000000000000..85f1d9dff900cd --- /dev/null +++ b/src/plugins/event_annotation/common/event_annotation_group/index.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 type { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { i18n } from '@kbn/i18n'; +import type { EventAnnotationOutput } from '../manual_event_annotation/types'; + +export interface EventAnnotationGroupOutput { + type: 'event_annotation_group'; + annotations: EventAnnotationOutput[]; +} + +export interface EventAnnotationGroupArgs { + annotations: EventAnnotationOutput[]; +} + +export function eventAnnotationGroup(): ExpressionFunctionDefinition< + 'event_annotation_group', + null, + EventAnnotationGroupArgs, + EventAnnotationGroupOutput +> { + return { + name: 'event_annotation_group', + aliases: [], + type: 'event_annotation_group', + inputTypes: ['null'], + help: i18n.translate('eventAnnotation.group.description', { + defaultMessage: 'Event annotation group', + }), + args: { + annotations: { + types: ['manual_event_annotation'], + help: i18n.translate('eventAnnotation.group.args.annotationConfigs', { + defaultMessage: 'Annotation configs', + }), + multi: true, + }, + }, + fn: (input, args) => { + return { + type: 'event_annotation_group', + annotations: args.annotations, + }; + }, + }; +} diff --git a/src/plugins/event_annotation/common/index.ts b/src/plugins/event_annotation/common/index.ts new file mode 100644 index 00000000000000..332fa19150aad2 --- /dev/null +++ b/src/plugins/event_annotation/common/index.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 type { EventAnnotationArgs, EventAnnotationOutput } from './manual_event_annotation/types'; +export { manualEventAnnotation } from './manual_event_annotation'; +export { eventAnnotationGroup } from './event_annotation_group'; +export type { EventAnnotationGroupArgs } from './event_annotation_group'; +export type { EventAnnotationConfig } from './types'; diff --git a/src/plugins/event_annotation/common/manual_event_annotation/index.ts b/src/plugins/event_annotation/common/manual_event_annotation/index.ts new file mode 100644 index 00000000000000..108df93b341802 --- /dev/null +++ b/src/plugins/event_annotation/common/manual_event_annotation/index.ts @@ -0,0 +1,82 @@ +/* + * 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 { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { i18n } from '@kbn/i18n'; +import type { EventAnnotationArgs, EventAnnotationOutput } from './types'; +export const manualEventAnnotation: ExpressionFunctionDefinition< + 'manual_event_annotation', + null, + EventAnnotationArgs, + EventAnnotationOutput +> = { + name: 'manual_event_annotation', + aliases: [], + type: 'manual_event_annotation', + help: i18n.translate('eventAnnotation.manualAnnotation.description', { + defaultMessage: `Configure manual annotation`, + }), + inputTypes: ['null'], + args: { + time: { + types: ['string'], + help: i18n.translate('eventAnnotation.manualAnnotation.args.time', { + defaultMessage: `Timestamp for annotation`, + }), + }, + label: { + types: ['string'], + help: i18n.translate('eventAnnotation.manualAnnotation.args.label', { + defaultMessage: `The name of the annotation`, + }), + }, + color: { + types: ['string'], + help: i18n.translate('eventAnnotation.manualAnnotation.args.color', { + defaultMessage: 'The color of the line', + }), + }, + lineStyle: { + types: ['string'], + options: ['solid', 'dotted', 'dashed'], + help: i18n.translate('eventAnnotation.manualAnnotation.args.lineStyle', { + defaultMessage: 'The style of the annotation line', + }), + }, + lineWidth: { + types: ['number'], + help: i18n.translate('eventAnnotation.manualAnnotation.args.lineWidth', { + defaultMessage: 'The width of the annotation line', + }), + }, + icon: { + types: ['string'], + help: i18n.translate('eventAnnotation.manualAnnotation.args.icon', { + defaultMessage: 'An optional icon used for annotation lines', + }), + }, + textVisibility: { + types: ['boolean'], + help: i18n.translate('eventAnnotation.manualAnnotation.args.textVisibility', { + defaultMessage: 'Visibility of the label on the annotation line', + }), + }, + isHidden: { + types: ['boolean'], + help: i18n.translate('eventAnnotation.manualAnnotation.args.isHidden', { + defaultMessage: `Switch to hide annotation`, + }), + }, + }, + fn: function fn(input: unknown, args: EventAnnotationArgs) { + return { + type: 'manual_event_annotation', + ...args, + }; + }, +}; diff --git a/src/plugins/event_annotation/common/manual_event_annotation/types.ts b/src/plugins/event_annotation/common/manual_event_annotation/types.ts new file mode 100644 index 00000000000000..e1bed4a592d234 --- /dev/null +++ b/src/plugins/event_annotation/common/manual_event_annotation/types.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { StyleProps } from '../types'; + +export type EventAnnotationArgs = { + time: string; +} & StyleProps; + +export type EventAnnotationOutput = EventAnnotationArgs & { type: 'manual_event_annotation' }; diff --git a/src/plugins/event_annotation/common/types.ts b/src/plugins/event_annotation/common/types.ts new file mode 100644 index 00000000000000..95275804d1d1f9 --- /dev/null +++ b/src/plugins/event_annotation/common/types.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 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 type LineStyle = 'solid' | 'dashed' | 'dotted'; +export type AnnotationType = 'manual'; +export type KeyType = 'point_in_time'; + +export interface StyleProps { + label: string; + color?: string; + icon?: string; + lineWidth?: number; + lineStyle?: LineStyle; + textVisibility?: boolean; + isHidden?: boolean; +} + +export type EventAnnotationConfig = { + id: string; + key: { + type: KeyType; + timestamp: string; + }; +} & StyleProps; diff --git a/src/plugins/event_annotation/jest.config.js b/src/plugins/event_annotation/jest.config.js new file mode 100644 index 00000000000000..a6ea4a6b430dfb --- /dev/null +++ b/src/plugins/event_annotation/jest.config.js @@ -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 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 = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/event_annotation'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/event_annotation', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/event_annotation/{common,public,server}/**/*.{ts,tsx}', + ], +}; diff --git a/src/plugins/event_annotation/kibana.json b/src/plugins/event_annotation/kibana.json new file mode 100644 index 00000000000000..5a0c49be09ba3e --- /dev/null +++ b/src/plugins/event_annotation/kibana.json @@ -0,0 +1,17 @@ +{ + "id": "eventAnnotation", + "version": "kibana", + "server": true, + "ui": true, + "description": "The Event Annotation service contains expressions for event annotations", + "extraPublicDirs": [ + "common" + ], + "requiredPlugins": [ + "expressions" + ], + "owner": { + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" + } +} \ No newline at end of file diff --git a/src/plugins/event_annotation/public/event_annotation_service/README.md b/src/plugins/event_annotation/public/event_annotation_service/README.md new file mode 100644 index 00000000000000..a7a85d3ab36415 --- /dev/null +++ b/src/plugins/event_annotation/public/event_annotation_service/README.md @@ -0,0 +1,3 @@ +# Event Annotation service + +The Event Annotation service contains expressions for event annotations diff --git a/src/plugins/event_annotation/public/event_annotation_service/helpers.ts b/src/plugins/event_annotation/public/event_annotation_service/helpers.ts new file mode 100644 index 00000000000000..aed33da8405743 --- /dev/null +++ b/src/plugins/event_annotation/public/event_annotation_service/helpers.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { euiLightVars } from '@kbn/ui-theme'; +export const defaultAnnotationColor = euiLightVars.euiColorAccent; diff --git a/src/plugins/event_annotation/public/event_annotation_service/index.tsx b/src/plugins/event_annotation/public/event_annotation_service/index.tsx new file mode 100644 index 00000000000000..e967a7cb0f0a26 --- /dev/null +++ b/src/plugins/event_annotation/public/event_annotation_service/index.tsx @@ -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 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 { EventAnnotationServiceType } from './types'; + +export class EventAnnotationService { + private eventAnnotationService?: EventAnnotationServiceType; + public async getService() { + if (!this.eventAnnotationService) { + const { getEventAnnotationService } = await import('./service'); + this.eventAnnotationService = getEventAnnotationService(); + } + return this.eventAnnotationService; + } +} diff --git a/src/plugins/event_annotation/public/event_annotation_service/service.tsx b/src/plugins/event_annotation/public/event_annotation_service/service.tsx new file mode 100644 index 00000000000000..3d81ea6a3e3a6c --- /dev/null +++ b/src/plugins/event_annotation/public/event_annotation_service/service.tsx @@ -0,0 +1,49 @@ +/* + * 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 { EventAnnotationServiceType } from './types'; +import { defaultAnnotationColor } from './helpers'; + +export function hasIcon(icon: string | undefined): icon is string { + return icon != null && icon !== 'empty'; +} + +export function getEventAnnotationService(): EventAnnotationServiceType { + return { + toExpression: ({ + label, + isHidden, + color, + lineStyle, + lineWidth, + icon, + textVisibility, + time, + }) => { + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'manual_event_annotation', + arguments: { + time: [time], + label: [label], + color: [color || defaultAnnotationColor], + lineWidth: [lineWidth || 1], + lineStyle: [lineStyle || 'solid'], + icon: hasIcon(icon) ? [icon] : ['triangle'], + textVisibility: [textVisibility || false], + isHidden: [Boolean(isHidden)], + }, + }, + ], + }; + }, + }; +} diff --git a/src/plugins/event_annotation/public/event_annotation_service/types.ts b/src/plugins/event_annotation/public/event_annotation_service/types.ts new file mode 100644 index 00000000000000..bb0b6eb4cc200b --- /dev/null +++ b/src/plugins/event_annotation/public/event_annotation_service/types.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 { ExpressionAstExpression } from '../../../expressions/common/ast'; +import { EventAnnotationArgs } from '../../common'; + +export interface EventAnnotationServiceType { + toExpression: (props: EventAnnotationArgs) => ExpressionAstExpression; +} diff --git a/src/plugins/event_annotation/public/index.ts b/src/plugins/event_annotation/public/index.ts new file mode 100644 index 00000000000000..c15429c94cbe41 --- /dev/null +++ b/src/plugins/event_annotation/public/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +// TODO: https://github.com/elastic/kibana/issues/110891 +/* eslint-disable @kbn/eslint/no_export_all */ + +import { EventAnnotationPlugin } from './plugin'; +export const plugin = () => new EventAnnotationPlugin(); +export type { EventAnnotationPluginSetup, EventAnnotationPluginStart } from './plugin'; +export * from './event_annotation_service/types'; +export { EventAnnotationService } from './event_annotation_service'; +export { defaultAnnotationColor } from './event_annotation_service/helpers'; diff --git a/src/plugins/event_annotation/public/mocks.ts b/src/plugins/event_annotation/public/mocks.ts new file mode 100644 index 00000000000000..e78d4e8f75de79 --- /dev/null +++ b/src/plugins/event_annotation/public/mocks.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { getEventAnnotationService } from './event_annotation_service/service'; + +// not really mocking but avoiding async loading +export const eventAnnotationServiceMock = getEventAnnotationService(); diff --git a/src/plugins/event_annotation/public/plugin.ts b/src/plugins/event_annotation/public/plugin.ts new file mode 100644 index 00000000000000..83cdc0546a7f5b --- /dev/null +++ b/src/plugins/event_annotation/public/plugin.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { Plugin, CoreSetup } from 'kibana/public'; +import { ExpressionsSetup } from '../../expressions/public'; +import { manualEventAnnotation, eventAnnotationGroup } from '../common'; +import { EventAnnotationService } from './event_annotation_service'; + +interface SetupDependencies { + expressions: ExpressionsSetup; +} + +/** @public */ +export type EventAnnotationPluginSetup = EventAnnotationService; + +/** @public */ +export type EventAnnotationPluginStart = EventAnnotationService; + +/** @public */ +export class EventAnnotationPlugin + implements Plugin +{ + private readonly eventAnnotationService = new EventAnnotationService(); + + public setup(core: CoreSetup, dependencies: SetupDependencies): EventAnnotationPluginSetup { + dependencies.expressions.registerFunction(manualEventAnnotation); + dependencies.expressions.registerFunction(eventAnnotationGroup); + return this.eventAnnotationService; + } + + public start(): EventAnnotationPluginStart { + return this.eventAnnotationService; + } +} diff --git a/src/plugins/event_annotation/server/index.ts b/src/plugins/event_annotation/server/index.ts new file mode 100644 index 00000000000000..d9d13045ed10a4 --- /dev/null +++ b/src/plugins/event_annotation/server/index.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 { EventAnnotationServerPlugin } from './plugin'; +export const plugin = () => new EventAnnotationServerPlugin(); diff --git a/src/plugins/event_annotation/server/plugin.ts b/src/plugins/event_annotation/server/plugin.ts new file mode 100644 index 00000000000000..ef4e0216fb5ac8 --- /dev/null +++ b/src/plugins/event_annotation/server/plugin.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { CoreSetup, Plugin } from 'kibana/server'; +import { manualEventAnnotation, eventAnnotationGroup } from '../common'; +import { ExpressionsServerSetup } from '../../expressions/server'; + +interface SetupDependencies { + expressions: ExpressionsServerSetup; +} + +export class EventAnnotationServerPlugin implements Plugin { + public setup(core: CoreSetup, dependencies: SetupDependencies) { + dependencies.expressions.registerFunction(manualEventAnnotation); + dependencies.expressions.registerFunction(eventAnnotationGroup); + + return {}; + } + + public start() { + return {}; + } + + public stop() {} +} diff --git a/src/plugins/event_annotation/tsconfig.json b/src/plugins/event_annotation/tsconfig.json new file mode 100644 index 00000000000000..ca3d65a13b214b --- /dev/null +++ b/src/plugins/event_annotation/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*" + ], + "references": [ + { + "path": "../../core/tsconfig.json" + }, + { + "path": "../expressions/tsconfig.json" + } + ] +} \ No newline at end of file diff --git a/x-pack/plugins/lens/common/constants.ts b/x-pack/plugins/lens/common/constants.ts index 1504e33ecacab6..d0bfecbd386bee 100644 --- a/x-pack/plugins/lens/common/constants.ts +++ b/x-pack/plugins/lens/common/constants.ts @@ -45,6 +45,7 @@ export const LegendDisplay = { export const layerTypes = { DATA: 'data', REFERENCELINE: 'referenceLine', + ANNOTATIONS: 'annotations', } as const; // might collide with user-supplied field names, try to make as unique as possible diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/layer_config/annotation_layer_config.ts b/x-pack/plugins/lens/common/expressions/xy_chart/layer_config/annotation_layer_config.ts new file mode 100644 index 00000000000000..45b4bf31c0cdc7 --- /dev/null +++ b/x-pack/plugins/lens/common/expressions/xy_chart/layer_config/annotation_layer_config.ts @@ -0,0 +1,67 @@ +/* + * 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 { + EventAnnotationConfig, + EventAnnotationOutput, +} from '../../../../../../../src/plugins/event_annotation/common'; +import type { ExpressionFunctionDefinition } from '../../../../../../../src/plugins/expressions/common'; +import { layerTypes } from '../../../constants'; + +export interface XYAnnotationLayerConfig { + layerId: string; + layerType: typeof layerTypes.ANNOTATIONS; + annotations: EventAnnotationConfig[]; + hide?: boolean; +} + +export interface AnnotationLayerArgs { + annotations: EventAnnotationOutput[]; + layerId: string; + layerType: typeof layerTypes.ANNOTATIONS; + hide?: boolean; +} +export type XYAnnotationLayerArgsResult = AnnotationLayerArgs & { + type: 'lens_xy_annotation_layer'; +}; +export function annotationLayerConfig(): ExpressionFunctionDefinition< + 'lens_xy_annotation_layer', + null, + AnnotationLayerArgs, + XYAnnotationLayerArgsResult +> { + return { + name: 'lens_xy_annotation_layer', + aliases: [], + type: 'lens_xy_annotation_layer', + inputTypes: ['null'], + help: 'Annotation layer in lens', + args: { + layerId: { + types: ['string'], + help: '', + }, + layerType: { types: ['string'], options: [layerTypes.ANNOTATIONS], help: '' }, + hide: { + types: ['boolean'], + default: false, + help: 'Show details', + }, + annotations: { + types: ['manual_event_annotation'], + help: '', + multi: true, + }, + }, + fn: (input, args) => { + return { + type: 'lens_xy_annotation_layer', + ...args, + }; + }, + }; +} diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/layer_config/index.ts b/x-pack/plugins/lens/common/expressions/xy_chart/layer_config/index.ts index 0b27ce7d6ed854..df27229bdb81f7 100644 --- a/x-pack/plugins/lens/common/expressions/xy_chart/layer_config/index.ts +++ b/x-pack/plugins/lens/common/expressions/xy_chart/layer_config/index.ts @@ -6,7 +6,12 @@ */ import { XYDataLayerConfig } from './data_layer_config'; import { XYReferenceLineLayerConfig } from './reference_line_layer_config'; +import { XYAnnotationLayerConfig } from './annotation_layer_config'; export * from './data_layer_config'; export * from './reference_line_layer_config'; +export * from './annotation_layer_config'; -export type XYLayerConfig = XYDataLayerConfig | XYReferenceLineLayerConfig; +export type XYLayerConfig = + | XYDataLayerConfig + | XYReferenceLineLayerConfig + | XYAnnotationLayerConfig; diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts b/x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts index 940896a2079e6f..4520f0c99c3e99 100644 --- a/x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts +++ b/x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts @@ -9,13 +9,14 @@ import type { AxisExtentConfigResult, AxisTitlesVisibilityConfigResult } from '. import type { FittingFunction } from './fitting_function'; import type { EndValue } from './end_value'; import type { GridlinesConfigResult } from './grid_lines_config'; -import type { DataLayerArgs } from './layer_config'; +import type { AnnotationLayerArgs, DataLayerArgs } from './layer_config'; import type { LegendConfigResult } from './legend_config'; import type { TickLabelsConfigResult } from './tick_labels_config'; import type { LabelsOrientationConfigResult } from './labels_orientation_config'; import type { ValueLabelConfig } from '../../types'; export type XYCurveType = 'LINEAR' | 'CURVE_MONOTONE_X'; +export type XYLayerArgs = DataLayerArgs | AnnotationLayerArgs; // Arguments to XY chart expression, with computed properties export interface XYArgs { @@ -28,7 +29,7 @@ export interface XYArgs { yRightExtent: AxisExtentConfigResult; legend: LegendConfigResult; valueLabels: ValueLabelConfig; - layers: DataLayerArgs[]; + layers: XYLayerArgs[]; fittingFunction?: FittingFunction; endValue?: EndValue; emphasizeFitting?: boolean; diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/xy_chart.ts b/x-pack/plugins/lens/common/expressions/xy_chart/xy_chart.ts index d0f278d382be9d..6d73e8eb9ba5fc 100644 --- a/x-pack/plugins/lens/common/expressions/xy_chart/xy_chart.ts +++ b/x-pack/plugins/lens/common/expressions/xy_chart/xy_chart.ts @@ -128,8 +128,12 @@ export const xyChart: ExpressionFunctionDefinition< }), }, layers: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - types: ['lens_xy_data_layer', 'lens_xy_referenceLine_layer'] as any, + types: [ + 'lens_xy_data_layer', + 'lens_xy_referenceLine_layer', + 'lens_xy_annotation_layer', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ] as any, help: 'Layers of visual series', multi: true, }, diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index 17a58a0f967702..18f33adf408400 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -21,7 +21,8 @@ "presentationUtil", "dataViewFieldEditor", "expressionGauge", - "expressionHeatmap" + "expressionHeatmap", + "eventAnnotation" ], "optionalPlugins": [ "usageCollection", diff --git a/x-pack/plugins/lens/public/app_plugin/app.scss b/x-pack/plugins/lens/public/app_plugin/app.scss index 83b0a39be9229c..5e859c1a938180 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.scss +++ b/x-pack/plugins/lens/public/app_plugin/app.scss @@ -38,6 +38,13 @@ fill: makeGraphicContrastColor($euiColorVis0, $euiColorDarkShade); } } +.lensAnnotationIconNoFill { + fill: none; +} + +.lensAnnotationIconFill { + fill: $euiColorGhost; +} // Less-than-ideal styles to add a vertical divider after this button. Consider restructuring markup for better semantics and styling options in the future. .lnsNavItem__goBack { diff --git a/x-pack/plugins/lens/public/assets/annotation_icons/circle.tsx b/x-pack/plugins/lens/public/assets/annotation_icons/circle.tsx new file mode 100644 index 00000000000000..fe19dc7e4c8fc1 --- /dev/null +++ b/x-pack/plugins/lens/public/assets/annotation_icons/circle.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as React from 'react'; +import { EuiIconProps } from '@elastic/eui'; +import classnames from 'classnames'; + +export const IconCircle = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + +); diff --git a/x-pack/plugins/lens/public/assets/annotation_icons/index.tsx b/x-pack/plugins/lens/public/assets/annotation_icons/index.tsx new file mode 100644 index 00000000000000..9e641d495582f6 --- /dev/null +++ b/x-pack/plugins/lens/public/assets/annotation_icons/index.tsx @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { IconCircle } from './circle'; +export { IconTriangle } from './triangle'; diff --git a/x-pack/plugins/lens/public/assets/annotation_icons/triangle.tsx b/x-pack/plugins/lens/public/assets/annotation_icons/triangle.tsx new file mode 100644 index 00000000000000..9924c049004cfc --- /dev/null +++ b/x-pack/plugins/lens/public/assets/annotation_icons/triangle.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as React from 'react'; +import { EuiIconProps } from '@elastic/eui'; +import classnames from 'classnames'; + +export const IconTriangle = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + +); diff --git a/x-pack/plugins/lens/public/assets/chart_bar_annotations.tsx b/x-pack/plugins/lens/public/assets/chart_bar_annotations.tsx new file mode 100644 index 00000000000000..63fc9023533f65 --- /dev/null +++ b/x-pack/plugins/lens/public/assets/chart_bar_annotations.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const LensIconChartBarAnnotations = ({ + title, + titleId, + ...props +}: Omit) => ( + + {title ? {title} : null} + + + + + +); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx index e88b04588d2e0d..f0e0911b708fdf 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx @@ -18,6 +18,7 @@ import { getCustomDropTarget, getAdditionalClassesOnDroppable, getAdditionalClassesOnEnter, + getDropProps, } from './drop_targets_utils'; export function DraggableDimensionButton({ @@ -59,8 +60,8 @@ export function DraggableDimensionButton({ }) { const { dragging } = useContext(DragContext); - const dropProps = layerDatasource.getDropProps({ - ...layerDatasourceDropProps, + const dropProps = getDropProps(layerDatasource, { + ...(layerDatasourceDropProps || {}), dragging, columnId, filterOperations: group.filterOperations, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx index 7d92eb9d22cbb8..a293af4d11bfeb 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx @@ -9,7 +9,7 @@ import React from 'react'; import classNames from 'classnames'; import { EuiIcon, EuiFlexItem, EuiFlexGroup, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DropType } from '../../../../types'; +import { Datasource, DropType, GetDropProps } from '../../../../types'; function getPropsForDropType(type: 'swap' | 'duplicate' | 'combine') { switch (type) { @@ -129,3 +129,13 @@ export const getAdditionalClassesOnDroppable = (dropType?: string) => { return 'lnsDragDrop-notCompatible'; } }; + +export const getDropProps = ( + layerDatasource: Datasource, + layerDatasourceDropProps: GetDropProps +) => { + if (layerDatasource) { + return layerDatasource.getDropProps(layerDatasourceDropProps); + } + return; +}; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx index 1ba3ff8f6ac34c..f2118bda216b8f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx @@ -14,7 +14,11 @@ import { DragDrop, DragDropIdentifier, DragContext } from '../../../../drag_drop import { Datasource, VisualizationDimensionGroupConfig, DropType } from '../../../../types'; import { LayerDatasourceDropProps } from '../types'; -import { getCustomDropTarget, getAdditionalClassesOnDroppable } from './drop_targets_utils'; +import { + getCustomDropTarget, + getAdditionalClassesOnDroppable, + getDropProps, +} from './drop_targets_utils'; const label = i18n.translate('xpack.lens.indexPattern.emptyDimensionButton', { defaultMessage: 'Empty dimension', @@ -24,32 +28,47 @@ interface EmptyButtonProps { columnId: string; onClick: (id: string) => void; group: VisualizationDimensionGroupConfig; + labels?: { + ariaLabel: (label: string) => string; + label: JSX.Element | string; + }; } -const DefaultEmptyButton = ({ columnId, group, onClick }: EmptyButtonProps) => ( - + i18n.translate('xpack.lens.indexPattern.addColumnAriaLabel', { defaultMessage: 'Add or drag-and-drop a field to {groupLabel}', - values: { groupLabel: group.groupLabel }, - })} - data-test-subj="lns-empty-dimension" - onClick={() => { - onClick(columnId); - }} - > + values: { groupLabel: l }, + }), + label: ( - -); + ), +}; + +const DefaultEmptyButton = ({ columnId, group, onClick }: EmptyButtonProps) => { + const { buttonAriaLabel, buttonLabel } = group.labels || {}; + return ( + { + onClick(columnId); + }} + > + {buttonLabel || defaultButtonLabels.label} + + ); +}; const SuggestedValueButton = ({ columnId, group, onClick }: EmptyButtonProps) => ( contentProps={{ className: 'lnsLayerPanel__triggerTextContent', }} - aria-label={i18n.translate('xpack.lens.indexPattern.removeColumnAriaLabel', { - defaultMessage: 'Add or drag-and-drop a field to {groupLabel}', - values: { groupLabel: group.groupLabel }, + aria-label={i18n.translate('xpack.lens.indexPattern.suggestedValueAriaLabel', { + defaultMessage: 'Suggested value: {value} for {groupLabel}', + values: { value: group.suggestedValue?.(), groupLabel: group.groupLabel }, })} data-test-subj="lns-empty-dimension-suggested-value" onClick={() => { @@ -112,8 +131,8 @@ export function EmptyDimensionButton({ setNewColumnId(generateId()); }, [itemIndex]); - const dropProps = layerDatasource.getDropProps({ - ...layerDatasourceDropProps, + const dropProps = getDropProps(layerDatasource, { + ...(layerDatasourceDropProps || {}), dragging, columnId: newColumnId, filterOperations: group.filterOperations, @@ -151,6 +170,12 @@ export function EmptyDimensionButton({ [value, onDrop] ); + const buttonProps: EmptyButtonProps = { + columnId: value.columnId, + onClick, + group, + }; + return (
    {typeof group.suggestedValue?.() === 'number' ? ( - + ) : ( - + )}
    diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx index cd26cd3197587b..b234b18f5262f7 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx @@ -20,7 +20,7 @@ import { LayerPanel } from './layer_panel'; import { coreMock } from 'src/core/public/mocks'; import { generateId } from '../../../id_generator'; import { mountWithProvider } from '../../../mocks'; -import { layerTypes } from '../../../../common'; +import { LayerType, layerTypes } from '../../../../common'; import { ReactWrapper } from 'enzyme'; import { addLayer } from '../../../state_management'; @@ -231,14 +231,17 @@ describe('ConfigPanel', () => { }); describe('initial default value', () => { - function clickToAddLayer(instance: ReactWrapper) { + function clickToAddLayer( + instance: ReactWrapper, + layerType: LayerType = layerTypes.REFERENCELINE + ) { act(() => { instance.find('[data-test-subj="lnsLayerAddButton"]').first().simulate('click'); }); instance.update(); act(() => { instance - .find(`[data-test-subj="lnsLayerAddButton-${layerTypes.REFERENCELINE}"]`) + .find(`[data-test-subj="lnsLayerAddButton-${layerType}"]`) .first() .simulate('click'); }); @@ -288,8 +291,6 @@ describe('ConfigPanel', () => { { groupId: 'testGroup', columnId: 'myColumn', - dataType: 'number', - label: 'Initial value', staticValue: 100, }, ], @@ -319,8 +320,6 @@ describe('ConfigPanel', () => { { groupId: 'testGroup', columnId: 'myColumn', - dataType: 'number', - label: 'Initial value', staticValue: 100, }, ], @@ -335,9 +334,7 @@ describe('ConfigPanel', () => { expect(lensStore.dispatch).toHaveBeenCalledTimes(1); expect(datasourceMap.testDatasource.initializeDimension).toHaveBeenCalledWith({}, 'newId', { columnId: 'myColumn', - dataType: 'number', groupId: 'testGroup', - label: 'Initial value', staticValue: 100, }); }); @@ -354,8 +351,6 @@ describe('ConfigPanel', () => { { groupId: 'a', columnId: 'newId', - dataType: 'number', - label: 'Initial value', staticValue: 100, }, ], @@ -374,11 +369,65 @@ describe('ConfigPanel', () => { { groupId: 'a', columnId: 'newId', - dataType: 'number', - label: 'Initial value', staticValue: 100, } ); }); + + it('When visualization is `noDatasource` should not run datasource methods', async () => { + const datasourceMap = mockDatasourceMap(); + + const visualizationMap = mockVisualizationMap(); + visualizationMap.testVis.setDimension = jest.fn(); + visualizationMap.testVis.getSupportedLayers = jest.fn(() => [ + { + type: layerTypes.DATA, + label: 'Data Layer', + initialDimensions: [ + { + groupId: 'testGroup', + columnId: 'myColumn', + staticValue: 100, + }, + ], + }, + { + type: layerTypes.REFERENCELINE, + label: 'Reference layer', + }, + { + type: layerTypes.ANNOTATIONS, + label: 'Annotations Layer', + noDatasource: true, + initialDimensions: [ + { + groupId: 'a', + columnId: 'newId', + staticValue: 100, + }, + ], + }, + ]); + + datasourceMap.testDatasource.initializeDimension = jest.fn(); + const props = getDefaultProps({ visualizationMap, datasourceMap }); + const { instance, lensStore } = await prepareAndMountComponent(props); + await clickToAddLayer(instance, layerTypes.ANNOTATIONS); + expect(lensStore.dispatch).toHaveBeenCalledTimes(1); + + expect(visualizationMap.testVis.setDimension).toHaveBeenCalledWith({ + columnId: 'newId', + frame: { + activeData: undefined, + datasourceLayers: { + a: expect.anything(), + }, + }, + groupId: 'a', + layerId: 'newId', + prevState: undefined, + }); + expect(datasourceMap.testDatasource.initializeDimension).not.toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index d3574abe4f57af..163d1b8ce8e616 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -135,61 +135,57 @@ export function LayerPanels( [dispatchLens] ); - const datasourcePublicAPIs = props.framePublicAPI.datasourceLayers; - return ( - {layerIds.map((layerId, layerIndex) => - datasourcePublicAPIs[layerId] ? ( - { - // avoid state update if the datasource does not support initializeDimension - if ( - activeDatasourceId != null && - datasourceMap[activeDatasourceId]?.initializeDimension - ) { - dispatchLens( - setLayerDefaultDimension({ - layerId, - columnId, - groupId, - }) - ); - } - }} - onRemoveLayer={() => { + {layerIds.map((layerId, layerIndex) => ( + { + // avoid state update if the datasource does not support initializeDimension + if ( + activeDatasourceId != null && + datasourceMap[activeDatasourceId]?.initializeDimension + ) { dispatchLens( - removeOrClearLayer({ - visualizationId: activeVisualization.id, + setLayerDefaultDimension({ layerId, - layerIds, + columnId, + groupId, }) ); - removeLayerRef(layerId); - }} - toggleFullscreen={toggleFullscreen} - /> - ) : null - )} + } + }} + onRemoveLayer={() => { + dispatchLens( + removeOrClearLayer({ + visualizationId: activeVisualization.id, + layerId, + layerIds, + }) + ); + removeLayerRef(layerId); + }} + toggleFullscreen={toggleFullscreen} + /> + ))} Hello!, + style: {}, + }, +}; + describe('LayerPanel', () => { let mockVisualization: jest.Mocked; let mockVisualization2: jest.Mocked; let mockDatasource: DatasourceMock; + mockDatasource = createMockDatasource('testDatasource'); let frame: FramePublicAPI; function getDefaultProps() { @@ -611,17 +623,6 @@ describe('LayerPanel', () => { nextLabel: '', }); - const draggingField = { - field: { name: 'dragged' }, - indexPatternId: 'a', - id: '1', - humanData: { label: 'Label' }, - ghost: { - children: , - style: {}, - }, - }; - const { instance } = await mountWithProvider( @@ -666,17 +667,6 @@ describe('LayerPanel', () => { columnId !== 'a' ? { dropTypes: ['field_replace'], nextLabel: '' } : undefined ); - const draggingField = { - field: { name: 'dragged' }, - indexPatternId: 'a', - id: '1', - humanData: { label: 'Label' }, - ghost: { - children: , - style: {}, - }, - }; - const { instance } = await mountWithProvider( @@ -985,4 +975,52 @@ describe('LayerPanel', () => { ); }); }); + describe('dimension trigger', () => { + it('should render datasource dimension trigger if there is layer datasource', async () => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [{ columnId: 'x' }], + filterOperations: () => true, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroup', + }, + ], + }); + await mountWithProvider(); + expect(mockDatasource.renderDimensionTrigger).toHaveBeenCalled(); + }); + + it('should render visualization dimension trigger if there is no layer datasource', async () => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [{ columnId: 'x' }], + filterOperations: () => true, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + const props = getDefaultProps(); + const propsWithVisOnlyLayer = { + ...props, + framePublicAPI: { ...props.framePublicAPI, datasourceLayers: {} }, + }; + + mockVisualization.renderDimensionTrigger = jest.fn(); + mockVisualization.getUniqueLabels = jest.fn(() => ({ + x: 'A', + })); + + await mountWithProvider(); + expect(mockDatasource.renderDimensionTrigger).not.toHaveBeenCalled(); + expect(mockVisualization.renderDimensionTrigger).toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 404a40832fc2f1..366d3f93bf8428 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -81,10 +81,10 @@ export function LayerPanel( updateDatasourceAsync, visualizationState, } = props; - const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; - const dateRange = useLensSelector(selectResolvedDateRange); + const datasourceStates = useLensSelector(selectDatasourceStates); const isFullscreen = useLensSelector(selectIsFullscreenDatasource); + const dateRange = useLensSelector(selectResolvedDateRange); useEffect(() => { setActiveDimension(initialActiveDimensionState); @@ -104,8 +104,10 @@ export function LayerPanel( activeData: props.framePublicAPI.activeData, }; - const datasourceId = datasourcePublicAPI.datasourceId; - const layerDatasourceState = datasourceStates[datasourceId].state; + const datasourcePublicAPI = framePublicAPI.datasourceLayers?.[layerId]; + const datasourceId = datasourcePublicAPI?.datasourceId; + const layerDatasourceState = datasourceStates?.[datasourceId]?.state; + const layerDatasource = props.datasourceMap[datasourceId]; const layerDatasourceDropProps = useMemo( () => ({ @@ -118,12 +120,9 @@ export function LayerPanel( [layerId, layerDatasourceState, datasourceId, updateDatasource] ); - const layerDatasource = props.datasourceMap[datasourceId]; - const layerDatasourceConfigProps = { ...layerDatasourceDropProps, frame: props.framePublicAPI, - activeData: props.framePublicAPI.activeData, dateRange, }; @@ -137,11 +136,15 @@ export function LayerPanel( activeVisualization, ] ); + + const columnLabelMap = + !layerDatasource && activeVisualization.getUniqueLabels + ? activeVisualization.getUniqueLabels(props.visualizationState) + : layerDatasource?.uniqueLabels?.(layerDatasourceConfigProps?.state); + const isEmptyLayer = !groups.some((d) => d.accessors.length > 0); const { activeId, activeGroup } = activeDimension; - const columnLabelMap = layerDatasource.uniqueLabels(layerDatasourceConfigProps.state); - const { setDimension, removeDimension } = activeVisualization; const allAccessors = groups.flatMap((group) => @@ -154,7 +157,7 @@ export function LayerPanel( registerNewRef: registerNewButtonRef, } = useFocusUpdate(allAccessors); - const layerDatasourceOnDrop = layerDatasource.onDrop; + const layerDatasourceOnDrop = layerDatasource?.onDrop; const onDrop = useMemo(() => { return ( @@ -180,16 +183,18 @@ export function LayerPanel( const filterOperations = group?.filterOperations || (() => false); - const dropResult = layerDatasourceOnDrop({ - ...layerDatasourceDropProps, - droppedItem, - columnId, - layerId: targetLayerId, - filterOperations, - dimensionGroups: groups, - groupId, - dropType, - }); + const dropResult = layerDatasource + ? layerDatasourceOnDrop({ + ...layerDatasourceDropProps, + droppedItem, + columnId, + layerId: targetLayerId, + filterOperations, + dimensionGroups: groups, + groupId, + dropType, + }) + : false; if (dropResult) { let previousColumn = typeof droppedItem.column === 'string' ? droppedItem.column : undefined; @@ -241,6 +246,7 @@ export function LayerPanel( removeDimension, layerDatasourceDropProps, setNextFocusedButtonId, + layerDatasource, ]); const isDimensionPanelOpen = Boolean(activeId); @@ -340,43 +346,45 @@ export function LayerPanel( /> - {layerDatasource && ( - { - const newState = - typeof updater === 'function' ? updater(layerDatasourceState) : updater; - // Look for removed columns - const nextPublicAPI = layerDatasource.getPublicAPI({ - state: newState, - layerId, - }); - const nextTable = new Set( - nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) - ); - const removed = datasourcePublicAPI - .getTableSpec() - .map(({ columnId }) => columnId) - .filter((columnId) => !nextTable.has(columnId)); - let nextVisState = props.visualizationState; - removed.forEach((columnId) => { - nextVisState = activeVisualization.removeDimension({ + <> + + { + const newState = + typeof updater === 'function' ? updater(layerDatasourceState) : updater; + // Look for removed columns + const nextPublicAPI = layerDatasource.getPublicAPI({ + state: newState, layerId, - columnId, - prevState: nextVisState, - frame: framePublicAPI, }); - }); + const nextTable = new Set( + nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) + ); + const removed = datasourcePublicAPI + .getTableSpec() + .map(({ columnId }) => columnId) + .filter((columnId) => !nextTable.has(columnId)); + let nextVisState = props.visualizationState; + removed.forEach((columnId) => { + nextVisState = activeVisualization.removeDimension({ + layerId, + columnId, + prevState: nextVisState, + frame: framePublicAPI, + }); + }); - props.updateAll(datasourceId, newState, nextVisState); - }, - }} - /> + props.updateAll(datasourceId, newState, nextVisState); + }, + }} + /> + )}
@@ -401,7 +409,6 @@ export function LayerPanel( : i18n.translate('xpack.lens.editorFrame.requiresFieldWarningLabel', { defaultMessage: 'Requires field', }); - const isOptional = !group.required && !group.suggestedValue; return ( {group.accessors.map((accessorConfig, accessorIndex) => { const { columnId } = accessorConfig; - return ( { setActiveDimension({ @@ -478,42 +484,66 @@ export function LayerPanel( }} onRemoveClick={(id: string) => { trackUiEvent('indexpattern_dimension_removed'); - props.updateAll( - datasourceId, - layerDatasource.removeColumn({ - layerId, - columnId: id, - prevState: layerDatasourceState, - }), - activeVisualization.removeDimension({ - layerId, - columnId: id, - prevState: props.visualizationState, - frame: framePublicAPI, - }) - ); + if (datasourceId && layerDatasource) { + props.updateAll( + datasourceId, + layerDatasource.removeColumn({ + layerId, + columnId: id, + prevState: layerDatasourceState, + }), + activeVisualization.removeDimension({ + layerId, + columnId: id, + prevState: props.visualizationState, + frame: framePublicAPI, + }) + ); + } else { + props.updateVisualization( + activeVisualization.removeDimension({ + layerId, + columnId: id, + prevState: props.visualizationState, + frame: framePublicAPI, + }) + ); + } removeButtonRef(id); }} invalid={ - !layerDatasource.isValidColumn( + layerDatasource && + !layerDatasource?.isValidColumn( layerDatasourceState, layerId, columnId ) } > - + {layerDatasource ? ( + + ) : ( + <> + {activeVisualization?.renderDimensionTrigger?.({ + columnId, + label: columnLabelMap[columnId], + hideTooltip, + invalid: group.invalid, + invalidMessage: group.invalidMessage, + })} + + )}
@@ -536,7 +566,7 @@ export function LayerPanel( setActiveDimension({ activeGroup: group, activeId: id, - isNew: !group.supportStaticValue, + isNew: !group.supportStaticValue && Boolean(layerDatasource), }); }} onDrop={onDrop} @@ -555,22 +585,25 @@ export function LayerPanel( isFullscreen={isFullscreen} groupLabel={activeGroup?.groupLabel || ''} handleClose={() => { - if ( - layerDatasource.canCloseDimensionEditor && - !layerDatasource.canCloseDimensionEditor(layerDatasourceState) - ) { - return false; - } - if (layerDatasource.updateStateOnCloseDimension) { - const newState = layerDatasource.updateStateOnCloseDimension({ - state: layerDatasourceState, - layerId, - columnId: activeId!, - }); - if (newState) { - props.updateDatasource(datasourceId, newState); + if (layerDatasource) { + if ( + layerDatasource.canCloseDimensionEditor && + !layerDatasource.canCloseDimensionEditor(layerDatasourceState) + ) { + return false; + } + if (layerDatasource.updateStateOnCloseDimension) { + const newState = layerDatasource.updateStateOnCloseDimension({ + state: layerDatasourceState, + layerId, + columnId: activeId!, + }); + if (newState) { + props.updateDatasource(datasourceId, newState); + } } } + setActiveDimension(initialActiveDimensionState); if (isFullscreen) { toggleFullscreen(); @@ -579,7 +612,7 @@ export function LayerPanel( }} panel={
- {activeGroup && activeId && ( + {activeGroup && activeId && layerDatasource && ( - + - - color)} - type={FIXED_PROGRESSION} - onClick={() => { - setIsPaletteOpen(!isPaletteOpen); - }} - /> - - - { - setIsPaletteOpen(!isPaletteOpen); - }} - size="xs" - flush="both" - > - {i18n.translate('xpack.lens.paletteHeatmapGradient.customize', { - defaultMessage: 'Edit', - })} - - setIsPaletteOpen(!isPaletteOpen)} - > - {activePalette && ( - { - // make sure to always have a list of stops - if (newPalette.params && !newPalette.params.stops) { - newPalette.params.stops = displayStops; - } - (newPalette as HeatmapVisualizationState['palette'])!.accessor = accessor; - setState({ - ...state, - palette: newPalette as HeatmapVisualizationState['palette'], - }); - }} - /> - )} - - - - + + + color)} + type={FIXED_PROGRESSION} + onClick={() => { + setIsPaletteOpen(!isPaletteOpen); + }} + /> + + + { + setIsPaletteOpen(!isPaletteOpen); + }} + size="xs" + flush="both" + > + {i18n.translate('xpack.lens.paletteHeatmapGradient.customize', { + defaultMessage: 'Edit', + })} + + setIsPaletteOpen(!isPaletteOpen)} + > + {activePalette && ( + { + // make sure to always have a list of stops + if (newPalette.params && !newPalette.params.stops) { + newPalette.params.stops = displayStops; + } + (newPalette as HeatmapVisualizationState['palette'])!.accessor = accessor; + setState({ + ...state, + palette: newPalette as HeatmapVisualizationState['palette'], + }); + }} + /> + )} + + + + + ); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts index f3c48bace4a5f2..3318b8c30909e1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts @@ -89,12 +89,13 @@ export function getDropProps(props: GetDropProps) { ) { const sourceColumn = state.layers[dragging.layerId].columns[dragging.columnId]; const targetColumn = state.layers[layerId].columns[columnId]; - const layerIndexPattern = state.indexPatterns[state.layers[layerId].indexPatternId]; - const isSameGroup = groupId === dragging.groupId; if (isSameGroup) { - return getDropPropsForSameGroup(targetColumn); - } else if (filterOperations(sourceColumn)) { + return getDropPropsForSameGroup(!targetColumn); + } + const layerIndexPattern = state.indexPatterns[state.layers[layerId].indexPatternId]; + + if (filterOperations(sourceColumn)) { return getDropPropsForCompatibleGroup( props.dimensionGroups, dragging.columnId, @@ -164,8 +165,8 @@ function getDropPropsForField({ return; } -function getDropPropsForSameGroup(targetColumn?: GenericIndexPatternColumn): DropProps { - return targetColumn ? { dropTypes: ['reorder'] } : { dropTypes: ['duplicate_compatible'] }; +function getDropPropsForSameGroup(isNew?: boolean): DropProps { + return !isNew ? { dropTypes: ['reorder'] } : { dropTypes: ['duplicate_compatible'] }; } function getDropPropsForCompatibleGroup( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index f19658d468d5fb..6bdd41d8db6314 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -2626,9 +2626,7 @@ describe('IndexPattern Data Source', () => { expect( indexPatternDatasource.initializeDimension!(state, 'first', { columnId: 'newStatic', - label: 'MyNewColumn', groupId: 'a', - dataType: 'number', }) ).toBe(state); }); @@ -2655,9 +2653,7 @@ describe('IndexPattern Data Source', () => { expect( indexPatternDatasource.initializeDimension!(state, 'first', { columnId: 'newStatic', - label: 'MyNewColumn', groupId: 'a', - dataType: 'number', staticValue: 0, // use a falsy value to check also this corner case }) ).toEqual({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index cf77d1c9c1cc29..d0b644e2bf9b4e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -230,7 +230,7 @@ export function getIndexPatternDatasource({ }); }, - initializeDimension(state, layerId, { columnId, groupId, label, dataType, staticValue }) { + initializeDimension(state, layerId, { columnId, groupId, staticValue }) { const indexPattern = state.indexPatterns[state.layers[layerId]?.indexPatternId]; if (staticValue == null) { return state; diff --git a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx index 2c038b09379999..d1f16ac5f9c411 100644 --- a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx @@ -22,8 +22,12 @@ import { DEFAULT_PERCENT_DECIMALS } from './constants'; import { PartitionChartsMeta } from './partition_charts_meta'; import { LegendDisplay, PieVisualizationState, SharedPieLayerState } from '../../common'; import { VisualizationDimensionEditorProps, VisualizationToolbarProps } from '../types'; -import { ToolbarPopover, LegendSettingsPopover, useDebouncedValue } from '../shared_components'; -import { PalettePicker } from '../shared_components'; +import { + ToolbarPopover, + LegendSettingsPopover, + useDebouncedValue, + PalettePicker, +} from '../shared_components'; import { getDefaultVisualValuesForLayer } from '../shared_components/datasource_default_values'; import { shouldShowValuesInLegend } from './render_helpers'; @@ -298,14 +302,12 @@ export function DimensionEditor( } ) { return ( - <> - { - props.setState({ ...props.state, palette: newPalette }); - }} - /> - + { + props.setState({ ...props.state, palette: newPalette }); + }} + /> ); } diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 4d883c3a27c5e7..d2bb7cdbb4344f 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -33,6 +33,7 @@ import type { NavigationPublicPluginStart } from '../../../../src/plugins/naviga import type { UrlForwardingSetup } from '../../../../src/plugins/url_forwarding/public'; import type { GlobalSearchPluginSetup } from '../../global_search/public'; import type { ChartsPluginSetup, ChartsPluginStart } from '../../../../src/plugins/charts/public'; +import type { EventAnnotationPluginSetup } from '../../../../src/plugins/event_annotation/public'; import type { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public'; import { EmbeddableStateTransfer } from '../../../../src/plugins/embeddable/public'; import type { EditorFrameService as EditorFrameServiceType } from './editor_frame_service'; @@ -104,6 +105,7 @@ export interface LensPluginSetupDependencies { embeddable?: EmbeddableSetup; visualizations: VisualizationsSetup; charts: ChartsPluginSetup; + eventAnnotation: EventAnnotationPluginSetup; globalSearch?: GlobalSearchPluginSetup; usageCollection?: UsageCollectionSetup; discover?: DiscoverSetup; @@ -120,6 +122,7 @@ export interface LensPluginStartDependencies { visualizations: VisualizationsStart; embeddable: EmbeddableStart; charts: ChartsPluginStart; + eventAnnotation: EventAnnotationPluginSetup; savedObjectsTagging?: SavedObjectTaggingPluginStart; presentationUtil: PresentationUtilPluginStart; dataViewFieldEditor: IndexPatternFieldEditorStart; @@ -235,6 +238,7 @@ export class LensPlugin { embeddable, visualizations, charts, + eventAnnotation, globalSearch, usageCollection, }: LensPluginSetupDependencies @@ -251,7 +255,8 @@ export class LensPlugin { charts, expressions, fieldFormats, - plugins.fieldFormats.deserialize + plugins.fieldFormats.deserialize, + eventAnnotation ); const visualizationMap = await this.editorFrameService!.loadVisualizations(); @@ -311,7 +316,8 @@ export class LensPlugin { charts, expressions, fieldFormats, - deps.fieldFormats.deserialize + deps.fieldFormats.deserialize, + eventAnnotation ), ensureDefaultDataView(), ]); @@ -368,7 +374,8 @@ export class LensPlugin { charts: ChartsPluginSetup, expressions: ExpressionsServiceSetup, fieldFormats: FieldFormatsSetup, - formatFactory: FormatFactory + formatFactory: FormatFactory, + eventAnnotation: EventAnnotationPluginSetup ) { const { DatatableVisualization, @@ -402,6 +409,7 @@ export class LensPlugin { charts, editorFrame: editorFrameSetupInterface, formatFactory, + eventAnnotation, }; this.indexpatternDatasource.setup(core, dependencies); this.xyVisualization.setup(core, dependencies); diff --git a/x-pack/plugins/lens/public/shared_components/dimension_section.scss b/x-pack/plugins/lens/public/shared_components/dimension_section.scss new file mode 100644 index 00000000000000..7781c91785d67d --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/dimension_section.scss @@ -0,0 +1,24 @@ +.lnsDimensionEditorSection { + padding-top: $euiSize; + padding-bottom: $euiSize; +} + +.lnsDimensionEditorSection:first-child { + padding-top: 0; +} + +.lnsDimensionEditorSection:first-child .lnsDimensionEditorSection__border { + display: none; +} + +.lnsDimensionEditorSection__border { + position: relative; + &:before { + content: ''; + position: absolute; + top: -$euiSize; + right: -$euiSize; + left: -$euiSize; + border-top: 1px solid $euiColorLightShade; + } +} \ No newline at end of file diff --git a/x-pack/plugins/lens/public/shared_components/dimension_section.tsx b/x-pack/plugins/lens/public/shared_components/dimension_section.tsx new file mode 100644 index 00000000000000..d56e08db4b0378 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/dimension_section.tsx @@ -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 { EuiTitle } from '@elastic/eui'; +import React from 'react'; +import './dimension_section.scss'; + +export const DimensionEditorSection = ({ + children, + title, +}: { + title?: string; + children?: React.ReactNode | React.ReactNode[]; +}) => { + return ( +
+
+ {title && ( + +

{title}

+
+ )} + {children} +
+ ); +}; diff --git a/x-pack/plugins/lens/public/shared_components/index.ts b/x-pack/plugins/lens/public/shared_components/index.ts index 6140e54b43dc73..b2428532a72c95 100644 --- a/x-pack/plugins/lens/public/shared_components/index.ts +++ b/x-pack/plugins/lens/public/shared_components/index.ts @@ -17,5 +17,6 @@ export { LegendActionPopover } from './legend_action_popover'; export { NameInput } from './name_input'; export { ValueLabelsSettings } from './value_labels_settings'; export { AxisTitleSettings } from './axis_title_settings'; +export { DimensionEditorSection } from './dimension_section'; export * from './static_header'; export * from './vis_label'; 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 56ff89f506c858..959db8ca006fed 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -619,30 +619,39 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { return state; } - const activeDatasource = datasourceMap[state.activeDatasourceId]; const activeVisualization = visualizationMap[state.visualization.activeId]; - - const datasourceState = activeDatasource.insertLayer( - state.datasourceStates[state.activeDatasourceId].state, - layerId - ); - const visualizationState = activeVisualization.appendLayer!( state.visualization.state, layerId, layerType ); + const framePublicAPI = { + // any better idea to avoid `as`? + activeData: state.activeData + ? (current(state.activeData) as TableInspectorAdapter) + : undefined, + datasourceLayers: getDatasourceLayers(state.datasourceStates, datasourceMap), + }; + + const activeDatasource = datasourceMap[state.activeDatasourceId]; + const { noDatasource } = + activeVisualization + .getSupportedLayers(visualizationState, framePublicAPI) + .find(({ type }) => type === layerType) || {}; + + const datasourceState = + !noDatasource && activeDatasource + ? activeDatasource.insertLayer( + state.datasourceStates[state.activeDatasourceId].state, + layerId + ) + : state.datasourceStates[state.activeDatasourceId].state; + const { activeDatasourceState, activeVisualizationState } = addInitialValueIfAvailable({ datasourceState, visualizationState, - framePublicAPI: { - // any better idea to avoid `as`? - activeData: state.activeData - ? (current(state.activeData) as TableInspectorAdapter) - : undefined, - datasourceLayers: getDatasourceLayers(state.datasourceStates, datasourceMap), - }, + framePublicAPI, activeVisualization, activeDatasource, layerId, @@ -710,39 +719,49 @@ function addInitialValueIfAvailable({ framePublicAPI: FramePublicAPI; visualizationState: unknown; datasourceState: unknown; - activeDatasource: Datasource; + activeDatasource?: Datasource; activeVisualization: Visualization; layerId: string; layerType: string; columnId?: string; groupId?: string; }) { - const layerInfo = activeVisualization - .getSupportedLayers(visualizationState, framePublicAPI) - .find(({ type }) => type === layerType); + const { initialDimensions, noDatasource } = + activeVisualization + .getSupportedLayers(visualizationState, framePublicAPI) + .find(({ type }) => type === layerType) || {}; - if (layerInfo?.initialDimensions && activeDatasource?.initializeDimension) { + if (initialDimensions) { const info = groupId - ? layerInfo.initialDimensions.find(({ groupId: id }) => id === groupId) - : // pick the first available one if not passed - layerInfo.initialDimensions[0]; + ? initialDimensions.find(({ groupId: id }) => id === groupId) + : initialDimensions[0]; // pick the first available one if not passed if (info) { - return { - activeDatasourceState: activeDatasource.initializeDimension(datasourceState, layerId, { - ...info, - columnId: columnId || info.columnId, - }), - activeVisualizationState: activeVisualization.setDimension({ - groupId: info.groupId, - layerId, - columnId: columnId || info.columnId, - prevState: visualizationState, - frame: framePublicAPI, - }), - }; + const activeVisualizationState = activeVisualization.setDimension({ + groupId: info.groupId, + layerId, + columnId: columnId || info.columnId, + prevState: visualizationState, + frame: framePublicAPI, + }); + + if (!noDatasource && activeDatasource?.initializeDimension) { + return { + activeDatasourceState: activeDatasource.initializeDimension(datasourceState, layerId, { + ...info, + columnId: columnId || info.columnId, + }), + activeVisualizationState, + }; + } else { + return { + activeDatasourceState: datasourceState, + activeVisualizationState, + }; + } } } + return { activeDatasourceState: datasourceState, activeVisualizationState: visualizationState, diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 9bea94bd723d3d..cfa23320dc5615 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -198,6 +198,12 @@ interface ChartSettings { }; } +export type GetDropProps = DatasourceDimensionDropProps & { + groupId: string; + dragging: DragContextState['dragging']; + prioritizedOperation?: string; +}; + /** * Interface for the datasource registry */ @@ -227,10 +233,8 @@ export interface Datasource { layerId: string, value: { columnId: string; - label: string; - dataType: string; - staticValue?: unknown; groupId: string; + staticValue?: unknown; } ) => T; @@ -251,11 +255,7 @@ export interface Datasource { props: DatasourceLayerPanelProps ) => ((cleanupElement: Element) => void) | void; getDropProps: ( - props: DatasourceDimensionDropProps & { - groupId: string; - dragging: DragContextState['dragging']; - prioritizedOperation?: string; - } + props: GetDropProps ) => { dropTypes: DropType[]; nextLabel?: string } | undefined; onDrop: (props: DatasourceDimensionDropHandlerProps) => false | true | { deleted: string }; /** @@ -585,6 +585,7 @@ export type VisualizationDimensionGroupConfig = SharedDimensionProps & { supportStaticValue?: boolean; paramEditorCustomProps?: ParamEditorCustomProps; supportFieldFormat?: boolean; + labels?: { buttonAriaLabel: string; buttonLabel: string }; }; interface VisualizationDimensionChangeProps { @@ -786,14 +787,13 @@ export interface Visualization { type: LayerType; label: string; icon?: IconType; + noDatasource?: boolean; disabled?: boolean; toolTipContent?: string; initialDimensions?: Array<{ - groupId: string; columnId: string; - dataType: string; - label: string; - staticValue: unknown; + groupId: string; + staticValue?: unknown; }>; }>; getLayerType: (layerId: string, state?: T) => LayerType | undefined; @@ -858,7 +858,20 @@ export interface Visualization { domElement: Element, props: VisualizationDimensionEditorProps ) => ((cleanupElement: Element) => void) | void; - + /** + * Renders dimension trigger. Used only for noDatasource layers + */ + renderDimensionTrigger?: (props: { + columnId: string; + label: string; + hideTooltip?: boolean; + invalid?: boolean; + invalidMessage?: string; + }) => JSX.Element | null; + /** + * Creates map of columns ids and unique lables. Used only for noDatasource layers + */ + getUniqueLabels?: (state: T) => Record; /** * The frame will call this function on all visualizations at different times. The * main use cases where visualization suggestions are requested are: diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx index e1885fafab5e0c..1770bac893b67b 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx @@ -399,22 +399,16 @@ export const getGaugeVisualization = ({ { groupId: 'min', columnId: generateId(), - dataType: 'number', - label: 'minAccessor', staticValue: minValue, }, { groupId: 'max', columnId: generateId(), - dataType: 'number', - label: 'maxAccessor', staticValue: maxValue, }, { groupId: 'goal', columnId: generateId(), - dataType: 'number', - label: 'goalAccessor', staticValue: goalValue, }, ] diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap index 504a553c5a631e..fdde8eb6ad3f2e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap @@ -1,5 +1,218 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`xy_expression XYChart component annotations should render basic annotation 1`] = ` + + } + markerBody={ + + } + markerPosition="top" + style={ + Object { + "line": Object { + "dash": undefined, + "opacity": 1, + "stroke": "#f04e98", + "strokeWidth": 1, + }, + } + } +/> +`; + +exports[`xy_expression XYChart component annotations should render grouped annotations preserving the shared styles 1`] = ` + + } + markerBody={ + + } + markerPosition="top" + style={ + Object { + "line": Object { + "dash": Array [ + 9, + 3, + ], + "opacity": 1, + "stroke": "red", + "strokeWidth": 3, + }, + } + } +/> +`; + +exports[`xy_expression XYChart component annotations should render grouped annotations with default styles 1`] = ` + + } + markerBody={ + + } + markerPosition="top" + style={ + Object { + "line": Object { + "dash": undefined, + "opacity": 1, + "stroke": "#f04e98", + "strokeWidth": 1, + }, + } + } +/> +`; + +exports[`xy_expression XYChart component annotations should render simplified annotation when hide is true 1`] = ` + + } + markerBody={ + + } + markerPosition="top" + style={ + Object { + "line": Object { + "dash": undefined, + "opacity": 1, + "stroke": "#f04e98", + "strokeWidth": 1, + }, + } + } +/> +`; + exports[`xy_expression XYChart component it renders area 1`] = ` & { + formatFactory: FormatFactory; + paletteService: PaletteRegistry; + } +) => { + const { state, setState, layerId, accessor } = props; + const isHorizontal = isHorizontalChart(state.layers); + + const { inputValue: localState, handleInputChange: setLocalState } = useDebouncedValue({ + value: state, + onChange: setState, + }); + + const index = localState.layers.findIndex((l) => l.layerId === layerId); + const localLayer = localState.layers.find( + (l) => l.layerId === layerId + ) as XYAnnotationLayerConfig; + + const currentAnnotations = localLayer.annotations?.find((c) => c.id === accessor); + + const setAnnotations = useCallback( + (annotations: Partial | undefined) => { + if (annotations == null) { + return; + } + const newConfigs = [...(localLayer.annotations || [])]; + const existingIndex = newConfigs.findIndex((c) => c.id === accessor); + if (existingIndex !== -1) { + newConfigs[existingIndex] = { ...newConfigs[existingIndex], ...annotations }; + } else { + return; // that should never happen because annotations are created before annotations panel is opened + } + setLocalState(updateLayer(localState, { ...localLayer, annotations: newConfigs }, index)); + }, + [accessor, index, localState, localLayer, setLocalState] + ); + + return ( + <> + + { + if (date) { + setAnnotations({ + key: { + ...(currentAnnotations?.key || { type: 'point_in_time' }), + timestamp: date.toISOString(), + }, + }); + } + }} + label={i18n.translate('xpack.lens.xyChart.annotationDate', { + defaultMessage: 'Annotation date', + })} + /> + + + { + setAnnotations({ label: value }); + }} + /> + + + + setAnnotations({ isHidden: ev.target.checked })} + /> + + + ); +}; + +const ConfigPanelDatePicker = ({ + value, + label, + onChange, +}: { + value: moment.Moment; + label: string; + onChange: (val: moment.Moment | null) => void; +}) => { + return ( + + + + ); +}; + +const ConfigPanelHideSwitch = ({ + value, + onChange, +}: { + value: boolean; + onChange: (event: EuiSwitchEvent) => void; +}) => { + return ( + + + + ); +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/annotations/expression.scss b/x-pack/plugins/lens/public/xy_visualization/annotations/expression.scss new file mode 100644 index 00000000000000..fc2b1204bb1d00 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/annotations/expression.scss @@ -0,0 +1,37 @@ +.lnsXyDecorationRotatedWrapper { + display: inline-block; + overflow: hidden; + line-height: 1.5; + + .lnsXyDecorationRotatedWrapper__label { + display: inline-block; + white-space: nowrap; + transform: translate(0, 100%) rotate(-90deg); + transform-origin: 0 0; + + &::after { + content: ''; + float: left; + margin-top: 100%; + } + } +} + +.lnsXyAnnotationNumberIcon { + border-radius: $euiSize; + min-width: $euiSize; + height: $euiSize; + background-color: currentColor; +} + +.lnsXyAnnotationNumberIcon__text { + font-weight: 500; + font-size: 9px; + letter-spacing: -.5px; + line-height: 11px; +} + +.lnsXyAnnotationIcon_rotate90 { + transform: rotate(45deg); + transform-origin: center; +} diff --git a/x-pack/plugins/lens/public/xy_visualization/annotations/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/annotations/expression.tsx new file mode 100644 index 00000000000000..c36488f29d2389 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/annotations/expression.tsx @@ -0,0 +1,233 @@ +/* + * 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 './expression.scss'; +import React from 'react'; +import { snakeCase } from 'lodash'; +import { + AnnotationDomainType, + AnnotationTooltipFormatter, + LineAnnotation, + Position, +} from '@elastic/charts'; +import type { FieldFormat } from 'src/plugins/field_formats/common'; +import type { EventAnnotationArgs } from 'src/plugins/event_annotation/common'; +import moment from 'moment'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { defaultAnnotationColor } from '../../../../../../src/plugins/event_annotation/public'; +import type { AnnotationLayerArgs } from '../../../common/expressions'; +import { hasIcon } from '../xy_config_panel/shared/icon_select'; +import { + mapVerticalToHorizontalPlacement, + LINES_MARKER_SIZE, + MarkerBody, + Marker, + AnnotationIcon, +} from '../annotations_helpers'; + +const getRoundedTimestamp = (timestamp: number, firstTimestamp?: number, minInterval?: number) => { + if (!firstTimestamp || !minInterval) { + return timestamp; + } + return timestamp - ((timestamp - firstTimestamp) % minInterval); +}; + +export interface AnnotationsProps { + groupedAnnotations: CollectiveConfig[]; + formatter?: FieldFormat; + isHorizontal: boolean; + paddingMap: Partial>; + hide?: boolean; + minInterval?: number; + isBarChart?: boolean; +} + +interface CollectiveConfig extends EventAnnotationArgs { + roundedTimestamp: number; + axisMode: 'bottom'; + customTooltipDetails?: AnnotationTooltipFormatter | undefined; +} + +const groupVisibleConfigsByInterval = ( + layers: AnnotationLayerArgs[], + minInterval?: number, + firstTimestamp?: number +) => { + return layers + .flatMap(({ annotations }) => annotations.filter((a) => !a.isHidden)) + .reduce>((acc, current) => { + const roundedTimestamp = getRoundedTimestamp( + moment(current.time).valueOf(), + firstTimestamp, + minInterval + ); + return { + ...acc, + [roundedTimestamp]: acc[roundedTimestamp] ? [...acc[roundedTimestamp], current] : [current], + }; + }, {}); +}; + +const createCustomTooltipDetails = + ( + config: EventAnnotationArgs[], + formatter?: FieldFormat + ): AnnotationTooltipFormatter | undefined => + () => { + return ( +
+ {config.map(({ icon, label, time, color }) => ( +
+ + {hasIcon(icon) && ( + + + + )} + {label} + + {formatter?.convert(time) || String(time)} +
+ ))} +
+ ); + }; + +function getCommonProperty( + configArr: EventAnnotationArgs[], + propertyName: K, + fallbackValue: T +) { + const firstStyle = configArr[0][propertyName]; + if (configArr.every((config) => firstStyle === config[propertyName])) { + return firstStyle; + } + return fallbackValue; +} + +const getCommonStyles = (configArr: EventAnnotationArgs[]) => { + return { + color: getCommonProperty( + configArr, + 'color', + defaultAnnotationColor + ), + lineWidth: getCommonProperty(configArr, 'lineWidth', 1), + lineStyle: getCommonProperty(configArr, 'lineStyle', 'solid'), + textVisibility: getCommonProperty(configArr, 'textVisibility', false), + }; +}; + +export const getAnnotationsGroupedByInterval = ( + layers: AnnotationLayerArgs[], + minInterval?: number, + firstTimestamp?: number, + formatter?: FieldFormat +) => { + const visibleGroupedConfigs = groupVisibleConfigsByInterval(layers, minInterval, firstTimestamp); + let collectiveConfig: CollectiveConfig; + return Object.entries(visibleGroupedConfigs).map(([roundedTimestamp, configArr]) => { + collectiveConfig = { + ...configArr[0], + roundedTimestamp: Number(roundedTimestamp), + axisMode: 'bottom', + }; + if (configArr.length > 1) { + const commonStyles = getCommonStyles(configArr); + collectiveConfig = { + ...collectiveConfig, + ...commonStyles, + icon: String(configArr.length), + customTooltipDetails: createCustomTooltipDetails(configArr, formatter), + }; + } + return collectiveConfig; + }); +}; + +export const Annotations = ({ + groupedAnnotations, + formatter, + isHorizontal, + paddingMap, + hide, + minInterval, + isBarChart, +}: AnnotationsProps) => { + return ( + <> + {groupedAnnotations.map((annotation) => { + const markerPositionVertical = Position.Top; + const markerPosition = isHorizontal + ? mapVerticalToHorizontalPlacement(markerPositionVertical) + : markerPositionVertical; + const hasReducedPadding = paddingMap[markerPositionVertical] === LINES_MARKER_SIZE; + const id = snakeCase(annotation.label); + const { roundedTimestamp, time: exactTimestamp } = annotation; + const isGrouped = Boolean(annotation.customTooltipDetails); + const header = + formatter?.convert(isGrouped ? roundedTimestamp : exactTimestamp) || + moment(isGrouped ? roundedTimestamp : exactTimestamp).toISOString(); + const strokeWidth = annotation.lineWidth || 1; + return ( + + ) : undefined + } + markerBody={ + !hide ? ( + + ) : undefined + } + markerPosition={markerPosition} + dataValues={[ + { + dataValue: moment( + isBarChart && minInterval ? roundedTimestamp + minInterval / 2 : roundedTimestamp + ).valueOf(), + header, + details: annotation.label, + }, + ]} + customTooltipDetails={annotation.customTooltipDetails} + style={{ + line: { + strokeWidth, + stroke: annotation.color || defaultAnnotationColor, + dash: + annotation.lineStyle === 'dashed' + ? [strokeWidth * 3, strokeWidth] + : annotation.lineStyle === 'dotted' + ? [strokeWidth, strokeWidth] + : undefined, + opacity: 1, + }, + }} + /> + ); + })} + + ); +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.test.ts b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.test.ts new file mode 100644 index 00000000000000..fbf13db7fa7a51 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.test.ts @@ -0,0 +1,210 @@ +/* + * 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 { FramePublicAPI } from '../../types'; +import { getStaticDate } from './helpers'; + +describe('annotations helpers', () => { + describe('getStaticDate', () => { + it('should return `now` value on when nothing is configured', () => { + jest.spyOn(Date, 'now').mockReturnValue(new Date('2022-04-08T11:01:58.135Z').valueOf()); + expect(getStaticDate([], undefined)).toBe('2022-04-08T11:01:58.135Z'); + }); + it('should return `now` value on when there is no active data', () => { + expect( + getStaticDate( + [ + { + layerId: 'layerId', + accessors: ['b'], + seriesType: 'bar_stacked', + layerType: 'data', + xAccessor: 'a', + }, + ], + undefined + ) + ).toBe('2022-04-08T11:01:58.135Z'); + }); + + it('should return timestamp value for single active data point', () => { + const activeData = { + layerId: { + type: 'datatable', + rows: [ + { + a: 1646002800000, + b: 1050, + }, + ], + columns: [ + { + id: 'a', + name: 'order_date per week', + meta: { type: 'date' }, + }, + { + id: 'b', + name: 'Count of records', + meta: { type: 'number', params: { id: 'number' } }, + }, + ], + }, + }; + expect( + getStaticDate( + [ + { + layerId: 'layerId', + accessors: ['b'], + seriesType: 'bar_stacked', + layerType: 'data', + xAccessor: 'a', + }, + ], + activeData as FramePublicAPI['activeData'] + ) + ).toBe('2022-02-27T23:00:00.000Z'); + }); + + it('should correctly calculate middle value for active data', () => { + const activeData = { + layerId: { + type: 'datatable', + rows: [ + { + a: 1648206000000, + b: 19, + }, + { + a: 1648249200000, + b: 73, + }, + { + a: 1648292400000, + b: 69, + }, + { + a: 1648335600000, + b: 7, + }, + ], + columns: [ + { + id: 'a', + name: 'order_date per week', + meta: { type: 'date' }, + }, + { + id: 'b', + name: 'Count of records', + meta: { type: 'number', params: { id: 'number' } }, + }, + ], + }, + }; + expect( + getStaticDate( + [ + { + layerId: 'layerId', + accessors: ['b'], + seriesType: 'bar_stacked', + layerType: 'data', + xAccessor: 'a', + }, + ], + activeData as FramePublicAPI['activeData'] + ) + ).toBe('2022-03-26T05:00:00.000Z'); + }); + + it('should calculate middle date point correctly for multiple layers', () => { + const activeData = { + layerId: { + type: 'datatable', + rows: [ + { + a: 1648206000000, + b: 19, + }, + { + a: 1648249200000, + b: 73, + }, + { + a: 1648292400000, + b: 69, + }, + { + a: 1648335600000, + b: 7, + }, + ], + columns: [ + { + id: 'a', + name: 'order_date per week', + meta: { type: 'date' }, + }, + { + id: 'b', + name: 'Count of records', + meta: { type: 'number', params: { id: 'number' } }, + }, + ], + }, + layerId2: { + type: 'datatable', + rows: [ + { + d: 1548206000000, + c: 19, + }, + { + d: 1548249200000, + c: 73, + }, + ], + columns: [ + { + id: 'd', + name: 'order_date per week', + meta: { type: 'date' }, + }, + { + id: 'c', + name: 'Count of records', + meta: { type: 'number', params: { id: 'number' } }, + }, + ], + }, + }; + expect( + getStaticDate( + [ + { + layerId: 'layerId', + accessors: ['b'], + seriesType: 'bar_stacked', + layerType: 'data', + xAccessor: 'a', + }, + { + layerId: 'layerId2', + accessors: ['c'], + seriesType: 'bar_stacked', + layerType: 'data', + xAccessor: 'd', + }, + ], + activeData as FramePublicAPI['activeData'] + ) + ).toBe('2020-08-24T12:06:40.000Z'); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx new file mode 100644 index 00000000000000..321090c94241ad --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx @@ -0,0 +1,240 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { layerTypes } from '../../../common'; +import type { + XYDataLayerConfig, + XYAnnotationLayerConfig, + XYLayerConfig, +} from '../../../common/expressions'; +import type { FramePublicAPI, Visualization } from '../../types'; +import { isHorizontalChart } from '../state_helpers'; +import type { XYState } from '../types'; +import { + checkScaleOperation, + getAnnotationsLayers, + getAxisName, + getDataLayers, + isAnnotationsLayer, +} from '../visualization_helpers'; +import { LensIconChartBarAnnotations } from '../../assets/chart_bar_annotations'; +import { generateId } from '../../id_generator'; +import { defaultAnnotationColor } from '../../../../../../src/plugins/event_annotation/public'; +import { defaultAnnotationLabel } from './config_panel'; + +const MAX_DATE = 8640000000000000; +const MIN_DATE = -8640000000000000; + +export function getStaticDate( + dataLayers: XYDataLayerConfig[], + activeData: FramePublicAPI['activeData'] +) { + const fallbackValue = moment().toISOString(); + + const dataLayersId = dataLayers.map(({ layerId }) => layerId); + if ( + !activeData || + Object.entries(activeData) + .filter(([key]) => dataLayersId.includes(key)) + .every(([, { rows }]) => !rows || !rows.length) + ) { + return fallbackValue; + } + + const minDate = dataLayersId.reduce((acc, lId) => { + const xAccessor = dataLayers.find((dataLayer) => dataLayer.layerId === lId)?.xAccessor!; + const firstTimestamp = activeData[lId]?.rows?.[0]?.[xAccessor]; + return firstTimestamp && firstTimestamp < acc ? firstTimestamp : acc; + }, MAX_DATE); + + const maxDate = dataLayersId.reduce((acc, lId) => { + const xAccessor = dataLayers.find((dataLayer) => dataLayer.layerId === lId)?.xAccessor!; + const lastTimestamp = activeData[lId]?.rows?.[activeData?.[lId]?.rows?.length - 1]?.[xAccessor]; + return lastTimestamp && lastTimestamp > acc ? lastTimestamp : acc; + }, MIN_DATE); + const middleDate = (minDate + maxDate) / 2; + return moment(middleDate).toISOString(); +} + +export const getAnnotationsSupportedLayer = ( + state?: XYState, + frame?: Pick +) => { + const dataLayers = getDataLayers(state?.layers || []); + + const hasDateHistogram = Boolean( + dataLayers.length && + dataLayers.every( + (dataLayer) => + dataLayer.xAccessor && + checkScaleOperation('interval', 'date', frame?.datasourceLayers || {})(dataLayer) + ) + ); + const initialDimensions = + state && hasDateHistogram + ? [ + { + groupId: 'xAnnotations', + columnId: generateId(), + }, + ] + : undefined; + + return { + type: layerTypes.ANNOTATIONS, + label: i18n.translate('xpack.lens.xyChart.addAnnotationsLayerLabel', { + defaultMessage: 'Annotations', + }), + icon: LensIconChartBarAnnotations, + disabled: !hasDateHistogram, + toolTipContent: !hasDateHistogram + ? i18n.translate('xpack.lens.xyChart.addAnnotationsLayerLabelDisabledHelp', { + defaultMessage: 'Annotations require a time based chart to work. Add a date histogram.', + }) + : undefined, + initialDimensions, + noDatasource: true, + }; +}; + +export const setAnnotationsDimension: Visualization['setDimension'] = ({ + prevState, + layerId, + columnId, + previousColumn, + frame, +}) => { + const foundLayer = prevState.layers.find((l) => l.layerId === layerId); + if (!foundLayer || !isAnnotationsLayer(foundLayer)) { + return prevState; + } + const dataLayers = getDataLayers(prevState.layers); + const newLayer = { ...foundLayer } as XYAnnotationLayerConfig; + + const hasConfig = newLayer.annotations?.some(({ id }) => id === columnId); + const previousConfig = previousColumn + ? newLayer.annotations?.find(({ id }) => id === previousColumn) + : false; + if (!hasConfig) { + const newTimestamp = getStaticDate(dataLayers, frame?.activeData); + newLayer.annotations = [ + ...(newLayer.annotations || []), + { + label: defaultAnnotationLabel, + key: { + type: 'point_in_time', + timestamp: newTimestamp, + }, + icon: 'triangle', + ...previousConfig, + id: columnId, + }, + ]; + } + return { + ...prevState, + layers: prevState.layers.map((l) => (l.layerId === layerId ? newLayer : l)), + }; +}; + +export const getAnnotationsAccessorColorConfig = (layer: XYAnnotationLayerConfig) => { + return layer.annotations.map((annotation) => { + return { + columnId: annotation.id, + triggerIcon: annotation.isHidden ? ('invisible' as const) : ('color' as const), + color: annotation?.color || defaultAnnotationColor, + }; + }); +}; + +export const getAnnotationsConfiguration = ({ + state, + frame, + layer, +}: { + state: XYState; + frame: FramePublicAPI; + layer: XYAnnotationLayerConfig; +}) => { + const dataLayers = getDataLayers(state.layers); + + const hasDateHistogram = Boolean( + dataLayers.length && + dataLayers.every( + (dataLayer) => + dataLayer.xAccessor && + checkScaleOperation('interval', 'date', frame?.datasourceLayers || {})(dataLayer) + ) + ); + + const groupLabel = getAxisName('x', { isHorizontal: isHorizontalChart(state.layers) }); + + const emptyButtonLabels = { + buttonAriaLabel: i18n.translate('xpack.lens.indexPattern.addColumnAriaLabelClick', { + defaultMessage: 'Add an annotation to {groupLabel}', + values: { groupLabel }, + }), + buttonLabel: i18n.translate('xpack.lens.configure.emptyConfigClick', { + defaultMessage: 'Add an annotation', + }), + }; + + return { + groups: [ + { + groupId: 'xAnnotations', + groupLabel, + accessors: getAnnotationsAccessorColorConfig(layer), + dataTestSubj: 'lnsXY_xAnnotationsPanel', + invalid: !hasDateHistogram, + invalidMessage: i18n.translate('xpack.lens.xyChart.addAnnotationsLayerLabelDisabledHelp', { + defaultMessage: 'Annotations require a time based chart to work. Add a date histogram.', + }), + required: false, + requiresPreviousColumnOnDuplicate: true, + supportsMoreColumns: true, + supportFieldFormat: false, + enableDimensionEditor: true, + filterOperations: () => false, + labels: emptyButtonLabels, + }, + ], + }; +}; + +export const getUniqueLabels = (layers: XYLayerConfig[]) => { + const annotationLayers = getAnnotationsLayers(layers); + const columnLabelMap = {} as Record; + const counts = {} as Record; + + const makeUnique = (label: string) => { + let uniqueLabel = label; + + while (counts[uniqueLabel] >= 0) { + const num = ++counts[uniqueLabel]; + uniqueLabel = i18n.translate('xpack.lens.uniqueLabel', { + defaultMessage: '{label} [{num}]', + values: { label, num }, + }); + } + + counts[uniqueLabel] = 0; + return uniqueLabel; + }; + + annotationLayers.forEach((layer) => { + if (!layer.annotations) { + return; + } + layer.annotations.forEach((l) => { + columnLabelMap[l.id] = makeUnique(l.label); + }); + }); + return columnLabelMap; +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/annotations_helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/annotations_helpers.tsx new file mode 100644 index 00000000000000..ddbdfc91f4a3e2 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/annotations_helpers.tsx @@ -0,0 +1,253 @@ +/* + * 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 './expression_reference_lines.scss'; +import React from 'react'; +import { EuiFlexGroup, EuiIcon, EuiIconProps, EuiText } from '@elastic/eui'; +import { Position } from '@elastic/charts'; +import classnames from 'classnames'; +import type { IconPosition, YAxisMode, YConfig } from '../../common/expressions'; +import { hasIcon } from './xy_config_panel/shared/icon_select'; +import { annotationsIconSet } from './annotations/config_panel/icon_set'; + +export const LINES_MARKER_SIZE = 20; + +export const computeChartMargins = ( + referenceLinePaddings: Partial>, + labelVisibility: Partial>, + titleVisibility: Partial>, + axesMap: Record<'left' | 'right', unknown>, + isHorizontal: boolean +) => { + const result: Partial> = {}; + if (!labelVisibility?.x && !titleVisibility?.x && referenceLinePaddings.bottom) { + const placement = isHorizontal ? mapVerticalToHorizontalPlacement('bottom') : 'bottom'; + result[placement] = referenceLinePaddings.bottom; + } + if ( + referenceLinePaddings.left && + (isHorizontal || (!labelVisibility?.yLeft && !titleVisibility?.yLeft)) + ) { + const placement = isHorizontal ? mapVerticalToHorizontalPlacement('left') : 'left'; + result[placement] = referenceLinePaddings.left; + } + if ( + referenceLinePaddings.right && + (isHorizontal || !axesMap.right || (!labelVisibility?.yRight && !titleVisibility?.yRight)) + ) { + const placement = isHorizontal ? mapVerticalToHorizontalPlacement('right') : 'right'; + result[placement] = referenceLinePaddings.right; + } + // there's no top axis, so just check if a margin has been computed + if (referenceLinePaddings.top) { + const placement = isHorizontal ? mapVerticalToHorizontalPlacement('top') : 'top'; + result[placement] = referenceLinePaddings.top; + } + return result; +}; + +// Note: it does not take into consideration whether the reference line is in view or not + +export const getLinesCausedPaddings = ( + visualConfigs: Array< + Pick | undefined + >, + axesMap: Record<'left' | 'right', unknown> +) => { + // collect all paddings for the 4 axis: if any text is detected double it. + const paddings: Partial> = {}; + const icons: Partial> = {}; + visualConfigs?.forEach((config) => { + if (!config) { + return; + } + const { axisMode, icon, iconPosition, textVisibility } = config; + if (axisMode && (hasIcon(icon) || textVisibility)) { + const placement = getBaseIconPlacement(iconPosition, axesMap, axisMode); + paddings[placement] = Math.max( + paddings[placement] || 0, + LINES_MARKER_SIZE * (textVisibility ? 2 : 1) // double the padding size if there's text + ); + icons[placement] = (icons[placement] || 0) + (hasIcon(icon) ? 1 : 0); + } + }); + // post-process the padding based on the icon presence: + // if no icon is present for the placement, just reduce the padding + (Object.keys(paddings) as Position[]).forEach((placement) => { + if (!icons[placement]) { + paddings[placement] = LINES_MARKER_SIZE; + } + }); + return paddings; +}; + +export function mapVerticalToHorizontalPlacement(placement: Position) { + switch (placement) { + case Position.Top: + return Position.Right; + case Position.Bottom: + return Position.Left; + case Position.Left: + return Position.Bottom; + case Position.Right: + return Position.Top; + } +} + +// if there's just one axis, put it on the other one +// otherwise use the same axis +// this function assume the chart is vertical +export function getBaseIconPlacement( + iconPosition: IconPosition | undefined, + axesMap?: Record, + axisMode?: YAxisMode +) { + if (iconPosition === 'auto') { + if (axisMode === 'bottom') { + return Position.Top; + } + if (axesMap) { + if (axisMode === 'left') { + return axesMap.right ? Position.Left : Position.Right; + } + return axesMap.left ? Position.Right : Position.Left; + } + } + + if (iconPosition === 'left') { + return Position.Left; + } + if (iconPosition === 'right') { + return Position.Right; + } + if (iconPosition === 'below') { + return Position.Bottom; + } + return Position.Top; +} + +export function MarkerBody({ + label, + isHorizontal, +}: { + label: string | undefined; + isHorizontal: boolean; +}) { + if (!label) { + return null; + } + if (isHorizontal) { + return ( +
+ {label} +
+ ); + } + return ( +
+
+ {label} +
+
+ ); +} + +const isNumericalString = (value: string) => !isNaN(Number(value)); + +function NumberIcon({ number }: { number: number }) { + return ( + + + {number < 10 ? number : `9+`} + + + ); +} + +interface MarkerConfig { + axisMode?: YAxisMode; + icon?: string; + textVisibility?: boolean; + iconPosition?: IconPosition; +} + +export const AnnotationIcon = ({ + type, + rotateClassName = '', + isHorizontal, + renderedInChart, + ...rest +}: { + type: string; + rotateClassName?: string; + isHorizontal?: boolean; + renderedInChart?: boolean; +} & EuiIconProps) => { + if (isNumericalString(type)) { + return ; + } + const iconConfig = annotationsIconSet.find((i) => i.value === type); + if (!iconConfig) { + return null; + } + return ( + + ); +}; + +export function Marker({ + config, + isHorizontal, + hasReducedPadding, + label, + rotateClassName, +}: { + config: MarkerConfig; + isHorizontal: boolean; + hasReducedPadding: boolean; + label?: string; + rotateClassName?: string; +}) { + if (hasIcon(config.icon)) { + return ( + + ); + } + + // if there's some text, check whether to show it as marker, or just show some padding for the icon + if (config.textVisibility) { + if (hasReducedPadding) { + return ; + } + return ; + } + return null; +} diff --git a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts index 82c1106e72a088..f8d5805279a2ee 100644 --- a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts +++ b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts @@ -13,7 +13,9 @@ import type { AccessorConfig, FramePublicAPI } from '../types'; import { getColumnToLabelMap } from './state_helpers'; import { FormatFactory, LayerType } from '../../common'; import type { XYLayerConfig } from '../../common/expressions'; -import { isDataLayer, isReferenceLayer } from './visualization_helpers'; +import { isReferenceLayer, isAnnotationsLayer } from './visualization_helpers'; +import { getAnnotationsAccessorColorConfig } from './annotations/helpers'; +import { getReferenceLineAccessorColorConfig } from './reference_line_helpers'; const isPrimitive = (value: unknown): boolean => value != null && typeof value !== 'object'; @@ -42,15 +44,13 @@ export function getColorAssignments( ): ColorAssignments { const layersPerPalette: Record = {}; - layers - .filter((layer) => isDataLayer(layer)) - .forEach((layer) => { - const palette = layer.palette?.name || 'default'; - if (!layersPerPalette[palette]) { - layersPerPalette[palette] = []; - } - layersPerPalette[palette].push(layer); - }); + layers.forEach((layer) => { + const palette = layer.palette?.name || 'default'; + if (!layersPerPalette[palette]) { + layersPerPalette[palette] = []; + } + layersPerPalette[palette].push(layer); + }); return mapValues(layersPerPalette, (paletteLayers) => { const seriesPerLayer = paletteLayers.map((layer, layerIndex) => { @@ -102,17 +102,6 @@ export function getColorAssignments( }); } -const getReferenceLineAccessorColorConfig = (layer: XYLayerConfig) => { - return layer.accessors.map((accessor) => { - const currentYConfig = layer.yConfig?.find((yConfig) => yConfig.forAccessor === accessor); - return { - columnId: accessor, - triggerIcon: 'color' as const, - color: currentYConfig?.color || defaultReferenceLineColor, - }; - }); -}; - export function getAccessorColorConfig( colorAssignments: ColorAssignments, frame: Pick, @@ -122,7 +111,9 @@ export function getAccessorColorConfig( if (isReferenceLayer(layer)) { return getReferenceLineAccessorColorConfig(layer); } - + if (isAnnotationsLayer(layer)) { + return getAnnotationsAccessorColorConfig(layer); + } const layerContainsSplits = Boolean(layer.splitAccessor); const currentPalette: PaletteOutput = layer.palette || { type: 'palette', name: 'default' }; const totalSeriesCount = colorAssignments[currentPalette.name]?.totalSeriesCount; 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 654a0f1b94a148..03a180cc20a08b 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx @@ -20,12 +20,13 @@ import { HorizontalAlignment, VerticalAlignment, LayoutDirection, + LineAnnotation, } from '@elastic/charts'; import { PaletteOutput } from 'src/plugins/charts/public'; import { calculateMinInterval, XYChart, XYChartRenderProps } from './expression'; import type { LensMultiTable } from '../../common'; import { layerTypes } from '../../common'; -import { xyChart } from '../../common/expressions'; +import { AnnotationLayerArgs, xyChart } from '../../common/expressions'; import { dataLayerConfig, legendConfig, @@ -41,12 +42,14 @@ import { } from '../../common/expressions'; import { Datatable, DatatableRow } from '../../../../../src/plugins/expressions/public'; import React from 'react'; -import { shallow } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; import { EmptyPlaceholder } from '../../../../../src/plugins/charts/public'; import { XyEndzones } from './x_domain'; +import { eventAnnotationServiceMock } from '../../../../../src/plugins/event_annotation/public/mocks'; +import { EventAnnotationOutput } from 'src/plugins/event_annotation/common'; const onClickValue = jest.fn(); const onSelectRange = jest.fn(); @@ -536,6 +539,7 @@ describe('xy_expression', () => { onSelectRange, syncColors: false, useLegacyTimeAxis: false, + eventAnnotationService: eventAnnotationServiceMock, }; }); @@ -546,7 +550,7 @@ describe('xy_expression', () => { ); expect(component).toMatchSnapshot(); @@ -613,7 +617,9 @@ describe('xy_expression', () => { }} args={{ ...args, - layers: [{ ...args.layers[0], seriesType: 'line', xScaleType: 'time' }], + layers: [ + { ...(args.layers[0] as DataLayerArgs), seriesType: 'line', xScaleType: 'time' }, + ], }} minInterval={undefined} /> @@ -802,7 +808,7 @@ describe('xy_expression', () => { ...args, layers: [ { - ...args.layers[0], + ...(args.layers[0] as DataLayerArgs), seriesType: 'line', xScaleType: 'time', isHistogram: true, @@ -878,7 +884,7 @@ describe('xy_expression', () => { ...args, layers: [ { - ...args.layers[0], + ...(args.layers[0] as DataLayerArgs), seriesType: 'bar', xScaleType: 'time', isHistogram: true, @@ -975,7 +981,7 @@ describe('xy_expression', () => { }, layers: [ { - ...args.layers[0], + ...(args.layers[0] as DataLayerArgs), seriesType: 'area', }, ], @@ -1006,7 +1012,7 @@ describe('xy_expression', () => { }, layers: [ { - ...args.layers[0], + ...(args.layers[0] as DataLayerArgs), seriesType: 'bar', }, ], @@ -1083,7 +1089,9 @@ describe('xy_expression', () => { }} args={{ ...args, - layers: [{ ...args.layers[0], seriesType: 'line', xScaleType: 'linear' }], + layers: [ + { ...(args.layers[0] as DataLayerArgs), seriesType: 'line', xScaleType: 'linear' }, + ], }} /> ); @@ -1102,7 +1110,12 @@ describe('xy_expression', () => { args={{ ...args, layers: [ - { ...args.layers[0], seriesType: 'line', xScaleType: 'linear', isHistogram: true }, + { + ...(args.layers[0] as DataLayerArgs), + seriesType: 'line', + xScaleType: 'linear', + isHistogram: true, + }, ], }} /> @@ -1150,7 +1163,7 @@ describe('xy_expression', () => { ); expect(component).toMatchSnapshot(); @@ -1165,7 +1178,7 @@ describe('xy_expression', () => { ); expect(component).toMatchSnapshot(); @@ -1180,7 +1193,10 @@ describe('xy_expression', () => { ); expect(component).toMatchSnapshot(); @@ -1678,7 +1694,10 @@ describe('xy_expression', () => { ); expect(component).toMatchSnapshot(); @@ -1693,7 +1712,10 @@ describe('xy_expression', () => { ); expect(component).toMatchSnapshot(); @@ -1710,7 +1732,9 @@ describe('xy_expression', () => { data={data} args={{ ...args, - layers: [{ ...args.layers[0], seriesType: 'bar_horizontal_stacked' }], + layers: [ + { ...(args.layers[0] as DataLayerArgs), seriesType: 'bar_horizontal_stacked' }, + ], }} /> ); @@ -1732,7 +1756,7 @@ describe('xy_expression', () => { ...args, layers: [ { - ...args.layers[0], + ...(args.layers[0] as DataLayerArgs), xAccessor: undefined, splitAccessor: 'e', seriesType: 'bar_stacked', @@ -1762,7 +1786,7 @@ describe('xy_expression', () => { accessors: ['b'], seriesType: 'bar', isHistogram: true, - }; + } as DataLayerArgs; delete firstLayer.splitAccessor; const component = shallow( @@ -1772,7 +1796,11 @@ describe('xy_expression', () => { test('it does not apply histogram mode to more than one bar series for unstacked bar chart', () => { const { data, args } = sampleArgs(); - const firstLayer: DataLayerArgs = { ...args.layers[0], seriesType: 'bar', isHistogram: true }; + const firstLayer: DataLayerArgs = { + ...args.layers[0], + seriesType: 'bar', + isHistogram: true, + } as DataLayerArgs; delete firstLayer.splitAccessor; const component = shallow( @@ -1787,13 +1815,13 @@ describe('xy_expression', () => { ...args.layers[0], seriesType: 'line', isHistogram: true, - }; + } as DataLayerArgs; delete firstLayer.splitAccessor; const secondLayer: DataLayerArgs = { ...args.layers[0], seriesType: 'line', isHistogram: true, - }; + } as DataLayerArgs; delete secondLayer.splitAccessor; const component = shallow( { ...args, layers: [ { - ...args.layers[0], + ...(args.layers[0] as DataLayerArgs), seriesType: 'bar_stacked', isHistogram: true, }, @@ -1836,7 +1864,9 @@ describe('xy_expression', () => { data={data} args={{ ...args, - layers: [{ ...args.layers[0], seriesType: 'bar', isHistogram: true }], + layers: [ + { ...(args.layers[0] as DataLayerArgs), seriesType: 'bar', isHistogram: true }, + ], }} /> ); @@ -2232,7 +2262,10 @@ describe('xy_expression', () => { ); expect(component.find(LineSeries).at(0).prop('xScaleType')).toEqual(ScaleType.Ordinal); @@ -2246,7 +2279,7 @@ describe('xy_expression', () => { ); expect(component.find(LineSeries).at(0).prop('yScaleType')).toEqual(ScaleType.Sqrt); @@ -2268,7 +2301,7 @@ describe('xy_expression', () => { ); expect(getFormatSpy).toHaveBeenCalledWith({ @@ -2678,7 +2711,9 @@ describe('xy_expression', () => { data={{ ...data }} args={{ ...args, - layers: [{ ...args.layers[0], accessors: ['a'], splitAccessor: undefined }], + layers: [ + { ...(args.layers[0] as DataLayerArgs), accessors: ['a'], splitAccessor: undefined }, + ], legend: { ...args.legend, isVisible: true, showSingleSeries: true }, }} /> @@ -2696,7 +2731,13 @@ describe('xy_expression', () => { data={{ ...data }} args={{ ...args, - layers: [{ ...args.layers[0], accessors: ['a'], splitAccessor: undefined }], + layers: [ + { + ...(args.layers[0] as DataLayerArgs), + accessors: ['a'], + splitAccessor: undefined, + }, + ], legend: { ...args.legend, isVisible: true, isInside: true }, }} /> @@ -2782,7 +2823,7 @@ describe('xy_expression', () => { test('it should apply None fitting function if not specified', () => { const { data, args } = sampleArgs(); - args.layers[0].accessors = ['a']; + (args.layers[0] as DataLayerArgs).accessors = ['a']; const component = shallow( @@ -2920,6 +2961,139 @@ describe('xy_expression', () => { }, ]); }); + + describe('annotations', () => { + const sampleStyledAnnotation: EventAnnotationOutput = { + time: '2022-03-18T08:25:00.000Z', + label: 'Event 1', + icon: 'triangle', + type: 'manual_event_annotation', + color: 'red', + lineStyle: 'dashed', + lineWidth: 3, + }; + const sampleAnnotationLayers: AnnotationLayerArgs[] = [ + { + layerType: layerTypes.ANNOTATIONS, + layerId: 'annotation', + annotations: [ + { + time: '2022-03-18T08:25:17.140Z', + label: 'Annotation', + type: 'manual_event_annotation', + }, + ], + }, + ]; + function sampleArgsWithAnnotation(annotationLayers = sampleAnnotationLayers) { + const { args } = sampleArgs(); + return { + data: dateHistogramData, + args: { + ...args, + layers: [dateHistogramLayer, ...annotationLayers], + } as XYArgs, + }; + } + test('should render basic annotation', () => { + const { data, args } = sampleArgsWithAnnotation(); + const component = mount(); + expect(component.find('LineAnnotation')).toMatchSnapshot(); + }); + test('should render simplified annotation when hide is true', () => { + const { data, args } = sampleArgsWithAnnotation(); + args.layers[0].hide = true; + const component = mount(); + expect(component.find('LineAnnotation')).toMatchSnapshot(); + }); + + test('should render grouped annotations preserving the shared styles', () => { + const { data, args } = sampleArgsWithAnnotation([ + { + layerType: layerTypes.ANNOTATIONS, + layerId: 'annotation', + annotations: [ + sampleStyledAnnotation, + { ...sampleStyledAnnotation, time: '2022-03-18T08:25:00.020Z', label: 'Event 2' }, + { + ...sampleStyledAnnotation, + time: '2022-03-18T08:25:00.001Z', + label: 'Event 3', + }, + ], + }, + ]); + const component = mount(); + const groupedAnnotation = component.find(LineAnnotation); + + expect(groupedAnnotation.length).toEqual(1); + // styles are passed because they are shared, dataValues & header is rounded to the interval + expect(groupedAnnotation).toMatchSnapshot(); + // renders numeric icon for grouped annotations + const marker = mount(
{groupedAnnotation.prop('marker')}
); + const numberIcon = marker.find('NumberIcon'); + expect(numberIcon.length).toEqual(1); + expect(numberIcon.text()).toEqual('3'); + + // checking tooltip + const renderLinks = mount(
{groupedAnnotation.prop('customTooltipDetails')!()}
); + expect(renderLinks.text()).toEqual( + ' Event 1 2022-03-18T08:25:00.000Z Event 2 2022-03-18T08:25:00.020Z Event 3 2022-03-18T08:25:00.001Z' + ); + }); + test('should render grouped annotations with default styles', () => { + const { data, args } = sampleArgsWithAnnotation([ + { + layerType: layerTypes.ANNOTATIONS, + layerId: 'annotation', + annotations: [sampleStyledAnnotation], + }, + { + layerType: layerTypes.ANNOTATIONS, + layerId: 'annotation', + annotations: [ + { + ...sampleStyledAnnotation, + icon: 'square', + color: 'blue', + lineStyle: 'dotted', + lineWidth: 10, + time: '2022-03-18T08:25:00.001Z', + label: 'Event 2', + }, + ], + }, + ]); + const component = mount(); + const groupedAnnotation = component.find(LineAnnotation); + + expect(groupedAnnotation.length).toEqual(1); + // styles are default because they are different for both annotations + expect(groupedAnnotation).toMatchSnapshot(); + }); + test('should not render hidden annotations', () => { + const { data, args } = sampleArgsWithAnnotation([ + { + layerType: layerTypes.ANNOTATIONS, + layerId: 'annotation', + annotations: [ + sampleStyledAnnotation, + { ...sampleStyledAnnotation, time: '2022-03-18T08:30:00.020Z', label: 'Event 2' }, + { + ...sampleStyledAnnotation, + time: '2022-03-18T08:35:00.001Z', + label: 'Event 3', + isHidden: true, + }, + ], + }, + ]); + const component = mount(); + const annotations = component.find(LineAnnotation); + + expect(annotations.length).toEqual(2); + }); + }); }); describe('calculateMinInterval', () => { @@ -2927,7 +3101,7 @@ describe('xy_expression', () => { beforeEach(() => { xyProps = sampleArgs(); - xyProps.args.layers[0].xScaleType = 'time'; + (xyProps.args.layers[0] as DataLayerArgs).xScaleType = 'time'; }); it('should use first valid layer and determine interval', async () => { xyProps.data.tables.first.columns[2].meta.source = 'esaggs'; @@ -2942,7 +3116,7 @@ describe('xy_expression', () => { }); it('should return interval of number histogram if available on first x axis columns', async () => { - xyProps.args.layers[0].xScaleType = 'linear'; + (xyProps.args.layers[0] as DataLayerArgs).xScaleType = 'linear'; xyProps.data.tables.first.columns[2].meta = { source: 'esaggs', type: 'number', @@ -2984,7 +3158,7 @@ describe('xy_expression', () => { }); it('should return undefined if x axis is not a date', async () => { - xyProps.args.layers[0].xScaleType = 'ordinal'; + (xyProps.args.layers[0] as DataLayerArgs).xScaleType = 'ordinal'; xyProps.data.tables.first.columns.splice(2, 1); const result = await calculateMinInterval(xyProps); expect(result).toEqual(undefined); diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 72a3f5f4f69767..8b62b8d0c120ce 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -50,11 +50,17 @@ import { i18n } from '@kbn/i18n'; import { RenderMode } from 'src/plugins/expressions'; import { ThemeServiceStart } from 'kibana/public'; import { FieldFormat } from 'src/plugins/field_formats/common'; +import { EventAnnotationServiceType } from '../../../../../src/plugins/event_annotation/public'; import { EmptyPlaceholder } from '../../../../../src/plugins/charts/public'; import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; import type { ILensInterpreterRenderHandlers, LensFilterEvent, LensBrushEvent } from '../types'; import type { LensMultiTable, FormatFactory } from '../../common'; -import type { DataLayerArgs, SeriesType, XYChartProps } from '../../common/expressions'; +import type { + DataLayerArgs, + SeriesType, + XYChartProps, + XYLayerArgs, +} from '../../common/expressions'; import { visualizationTypes } from './types'; import { VisualizationContainer } from '../visualization_container'; import { isHorizontalChart, getSeriesColor } from './state_helpers'; @@ -72,13 +78,17 @@ import { getAxesConfiguration, GroupsConfiguration, validateExtent } from './axe import { getColorAssignments } from './color_assignment'; import { getXDomain, XyEndzones } from './x_domain'; import { getLegendAction } from './get_legend_action'; -import { - computeChartMargins, - getReferenceLineRequiredPaddings, - ReferenceLineAnnotations, -} from './expression_reference_lines'; +import { ReferenceLineAnnotations } from './expression_reference_lines'; + +import { computeChartMargins, getLinesCausedPaddings } from './annotations_helpers'; + +import { Annotations, getAnnotationsGroupedByInterval } from './annotations/expression'; import { computeOverallDataDomain } from './reference_line_helpers'; -import { getReferenceLayers, isDataLayer } from './visualization_helpers'; +import { + getReferenceLayers, + getDataLayersArgs, + getAnnotationsLayersArgs, +} from './visualization_helpers'; declare global { interface Window { @@ -104,6 +114,7 @@ export type XYChartRenderProps = XYChartProps & { onSelectRange: (data: LensBrushEvent['data']) => void; renderMode: RenderMode; syncColors: boolean; + eventAnnotationService: EventAnnotationServiceType; }; export function calculateMinInterval({ args: { layers }, data }: XYChartProps) { @@ -140,6 +151,7 @@ export const getXyChartRenderer = (dependencies: { timeZone: string; useLegacyTimeAxis: boolean; kibanaTheme: ThemeServiceStart; + eventAnnotationService: EventAnnotationServiceType; }): ExpressionRenderDefinition => ({ name: 'lens_xy_chart_renderer', displayName: 'XY chart', @@ -170,6 +182,7 @@ export const getXyChartRenderer = (dependencies: { chartsActiveCursorService={dependencies.chartsActiveCursorService} chartsThemeService={dependencies.chartsThemeService} paletteService={dependencies.paletteService} + eventAnnotationService={dependencies.eventAnnotationService} timeZone={dependencies.timeZone} useLegacyTimeAxis={dependencies.useLegacyTimeAxis} minInterval={calculateMinInterval(config)} @@ -265,7 +278,9 @@ export function XYChart({ }); if (filteredLayers.length === 0) { - const icon: IconType = getIconForSeriesType(layers?.[0]?.seriesType || 'bar'); + const icon: IconType = getIconForSeriesType( + getDataLayersArgs(layers)?.[0]?.seriesType || 'bar' + ); return ; } @@ -353,7 +368,23 @@ export function XYChart({ }; const referenceLineLayers = getReferenceLayers(layers); - const referenceLinePaddings = getReferenceLineRequiredPaddings(referenceLineLayers, yAxesMap); + const annotationsLayers = getAnnotationsLayersArgs(layers); + const firstTable = data.tables[filteredLayers[0].layerId]; + + const xColumnId = firstTable.columns.find((col) => col.id === filteredLayers[0].xAccessor)?.id; + + const groupedAnnotations = getAnnotationsGroupedByInterval( + annotationsLayers, + minInterval, + xColumnId ? firstTable.rows[0]?.[xColumnId] : undefined, + xAxisFormatter + ); + const visualConfigs = [ + ...referenceLineLayers.flatMap(({ yConfig }) => yConfig), + ...groupedAnnotations, + ].filter(Boolean); + + const linesPaddings = getLinesCausedPaddings(visualConfigs, yAxesMap); const getYAxesStyle = (groupId: 'left' | 'right') => { const tickVisible = @@ -369,9 +400,9 @@ export function XYChart({ ? args.labelsOrientation?.yRight || 0 : args.labelsOrientation?.yLeft || 0, padding: - referenceLinePaddings[groupId] != null + linesPaddings[groupId] != null ? { - inner: referenceLinePaddings[groupId], + inner: linesPaddings[groupId], } : undefined, }, @@ -382,9 +413,9 @@ export function XYChart({ : axisTitlesVisibilitySettings?.yLeft, // if labels are not visible add the padding to the title padding: - !tickVisible && referenceLinePaddings[groupId] != null + !tickVisible && linesPaddings[groupId] != null ? { - inner: referenceLinePaddings[groupId], + inner: linesPaddings[groupId], } : undefined, }, @@ -458,7 +489,7 @@ export function XYChart({ const valueLabelsStyling = shouldShowValueLabels && valueLabels !== 'hide' && getValueLabelsStyling(shouldRotate); - const colorAssignments = getColorAssignments(args.layers, data, formatFactory); + const colorAssignments = getColorAssignments(getDataLayersArgs(args.layers), data, formatFactory); const clickHandler: ElementClickListener = ([[geometry, series]]) => { // for xyChart series is always XYChartSeriesIdentifier and geometry is always type of GeometryValue @@ -591,16 +622,13 @@ export function XYChart({ tickLabel: { visible: tickLabelsVisibilitySettings?.x, rotation: labelsOrientation?.x, - padding: - referenceLinePaddings.bottom != null - ? { inner: referenceLinePaddings.bottom } - : undefined, + padding: linesPaddings.bottom != null ? { inner: linesPaddings.bottom } : undefined, }, axisTitle: { visible: axisTitlesVisibilitySettings.x, padding: - !tickLabelsVisibilitySettings?.x && referenceLinePaddings.bottom != null - ? { inner: referenceLinePaddings.bottom } + !tickLabelsVisibilitySettings?.x && linesPaddings.bottom != null + ? { inner: linesPaddings.bottom } : undefined, }, }; @@ -633,7 +661,7 @@ export function XYChart({ chartMargins: { ...chartTheme.chartPaddings, ...computeChartMargins( - referenceLinePaddings, + linesPaddings, tickLabelsVisibilitySettings, axisTitlesVisibilitySettings, yAxesMap, @@ -1005,29 +1033,37 @@ export function XYChart({ right: Boolean(yAxesMap.right), }} isHorizontal={shouldRotate} - paddingMap={referenceLinePaddings} + paddingMap={linesPaddings} + /> + ) : null} + {groupedAnnotations.length ? ( + 0} + minInterval={minInterval} /> ) : null}
); } -function getFilteredLayers(layers: DataLayerArgs[], data: LensMultiTable) { - return layers.filter((layer) => { +function getFilteredLayers(layers: XYLayerArgs[], data: LensMultiTable) { + return getDataLayersArgs(layers).filter((layer) => { const { layerId, xAccessor, accessors, splitAccessor } = layer; - return ( - isDataLayer(layer) && - !( - !accessors.length || - !data.tables[layerId] || - data.tables[layerId].rows.length === 0 || - (xAccessor && - data.tables[layerId].rows.every((row) => typeof row[xAccessor] === 'undefined')) || - // stacked percentage bars have no xAccessors but splitAccessor with undefined values in them when empty - (!xAccessor && - splitAccessor && - data.tables[layerId].rows.every((row) => typeof row[splitAccessor] === 'undefined')) - ) + return !( + !accessors.length || + !data.tables[layerId] || + data.tables[layerId].rows.length === 0 || + (xAccessor && + data.tables[layerId].rows.every((row) => typeof row[xAccessor] === 'undefined')) || + // stacked percentage bars have no xAccessors but splitAccessor with undefined values in them when empty + (!xAccessor && + splitAccessor && + data.tables[layerId].rows.every((row) => typeof row[splitAccessor] === 'undefined')) ); }); } diff --git a/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx b/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx index 2d22f6a6ed76ea..7817db573e419a 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx @@ -8,183 +8,19 @@ import './expression_reference_lines.scss'; import React from 'react'; import { groupBy } from 'lodash'; -import { EuiIcon } from '@elastic/eui'; import { RectAnnotation, AnnotationDomainType, LineAnnotation, Position } from '@elastic/charts'; import type { PaletteRegistry } from 'src/plugins/charts/public'; import type { FieldFormat } from 'src/plugins/field_formats/common'; -import { euiLightVars } from '@kbn/ui-theme'; -import type { IconPosition, ReferenceLineLayerArgs, YAxisMode } from '../../common/expressions'; +import type { ReferenceLineLayerArgs } from '../../common/expressions'; import type { LensMultiTable } from '../../common/types'; -import { hasIcon } from './xy_config_panel/shared/icon_select'; - -export const REFERENCE_LINE_MARKER_SIZE = 20; - -export const computeChartMargins = ( - referenceLinePaddings: Partial>, - labelVisibility: Partial>, - titleVisibility: Partial>, - axesMap: Record<'left' | 'right', unknown>, - isHorizontal: boolean -) => { - const result: Partial> = {}; - if (!labelVisibility?.x && !titleVisibility?.x && referenceLinePaddings.bottom) { - const placement = isHorizontal ? mapVerticalToHorizontalPlacement('bottom') : 'bottom'; - result[placement] = referenceLinePaddings.bottom; - } - if ( - referenceLinePaddings.left && - (isHorizontal || (!labelVisibility?.yLeft && !titleVisibility?.yLeft)) - ) { - const placement = isHorizontal ? mapVerticalToHorizontalPlacement('left') : 'left'; - result[placement] = referenceLinePaddings.left; - } - if ( - referenceLinePaddings.right && - (isHorizontal || !axesMap.right || (!labelVisibility?.yRight && !titleVisibility?.yRight)) - ) { - const placement = isHorizontal ? mapVerticalToHorizontalPlacement('right') : 'right'; - result[placement] = referenceLinePaddings.right; - } - // there's no top axis, so just check if a margin has been computed - if (referenceLinePaddings.top) { - const placement = isHorizontal ? mapVerticalToHorizontalPlacement('top') : 'top'; - result[placement] = referenceLinePaddings.top; - } - return result; -}; - -// Note: it does not take into consideration whether the reference line is in view or not -export const getReferenceLineRequiredPaddings = ( - referenceLineLayers: ReferenceLineLayerArgs[], - axesMap: Record<'left' | 'right', unknown> -) => { - // collect all paddings for the 4 axis: if any text is detected double it. - const paddings: Partial> = {}; - const icons: Partial> = {}; - referenceLineLayers.forEach((layer) => { - layer.yConfig?.forEach(({ axisMode, icon, iconPosition, textVisibility }) => { - if (axisMode && (hasIcon(icon) || textVisibility)) { - const placement = getBaseIconPlacement(iconPosition, axisMode, axesMap); - paddings[placement] = Math.max( - paddings[placement] || 0, - REFERENCE_LINE_MARKER_SIZE * (textVisibility ? 2 : 1) // double the padding size if there's text - ); - icons[placement] = (icons[placement] || 0) + (hasIcon(icon) ? 1 : 0); - } - }); - }); - // post-process the padding based on the icon presence: - // if no icon is present for the placement, just reduce the padding - (Object.keys(paddings) as Position[]).forEach((placement) => { - if (!icons[placement]) { - paddings[placement] = REFERENCE_LINE_MARKER_SIZE; - } - }); - - return paddings; -}; - -function mapVerticalToHorizontalPlacement(placement: Position) { - switch (placement) { - case Position.Top: - return Position.Right; - case Position.Bottom: - return Position.Left; - case Position.Left: - return Position.Bottom; - case Position.Right: - return Position.Top; - } -} - -// if there's just one axis, put it on the other one -// otherwise use the same axis -// this function assume the chart is vertical -function getBaseIconPlacement( - iconPosition: IconPosition | undefined, - axisMode: YAxisMode | undefined, - axesMap: Record -) { - if (iconPosition === 'auto') { - if (axisMode === 'bottom') { - return Position.Top; - } - if (axisMode === 'left') { - return axesMap.right ? Position.Left : Position.Right; - } - return axesMap.left ? Position.Right : Position.Left; - } - - if (iconPosition === 'left') { - return Position.Left; - } - if (iconPosition === 'right') { - return Position.Right; - } - if (iconPosition === 'below') { - return Position.Bottom; - } - return Position.Top; -} - -function getMarkerBody(label: string | undefined, isHorizontal: boolean) { - if (!label) { - return; - } - if (isHorizontal) { - return ( -
- {label} -
- ); - } - return ( -
-
- {label} -
-
- ); -} - -interface MarkerConfig { - axisMode?: YAxisMode; - icon?: string; - textVisibility?: boolean; -} - -function getMarkerToShow( - markerConfig: MarkerConfig, - label: string | undefined, - isHorizontal: boolean, - hasReducedPadding: boolean -) { - // show an icon if present - if (hasIcon(markerConfig.icon)) { - return ; - } - // if there's some text, check whether to show it as marker, or just show some padding for the icon - if (markerConfig.textVisibility) { - if (hasReducedPadding) { - return getMarkerBody( - label, - (!isHorizontal && markerConfig.axisMode === 'bottom') || - (isHorizontal && markerConfig.axisMode !== 'bottom') - ); - } - return ; - } -} +import { defaultReferenceLineColor } from './color_assignment'; +import { + MarkerBody, + Marker, + LINES_MARKER_SIZE, + mapVerticalToHorizontalPlacement, + getBaseIconPlacement, +} from './annotations_helpers'; export interface ReferenceLineAnnotationsProps { layers: ReferenceLineLayerArgs[]; @@ -241,32 +77,40 @@ export const ReferenceLineAnnotations = ({ const formatter = formatters[groupId || 'bottom']; - const defaultColor = euiLightVars.euiColorDarkShade; - // get the position for vertical chart const markerPositionVertical = getBaseIconPlacement( yConfig.iconPosition, - yConfig.axisMode, - axesMap + axesMap, + yConfig.axisMode ); // the padding map is built for vertical chart - const hasReducedPadding = - paddingMap[markerPositionVertical] === REFERENCE_LINE_MARKER_SIZE; + const hasReducedPadding = paddingMap[markerPositionVertical] === LINES_MARKER_SIZE; const props = { groupId, - marker: getMarkerToShow( - yConfig, - columnToLabelMap[yConfig.forAccessor], - isHorizontal, - hasReducedPadding + marker: ( + ), - markerBody: getMarkerBody( - yConfig.textVisibility && !hasReducedPadding - ? columnToLabelMap[yConfig.forAccessor] - : undefined, - (!isHorizontal && yConfig.axisMode === 'bottom') || - (isHorizontal && yConfig.axisMode !== 'bottom') + markerBody: ( + ), // rotate the position if required markerPosition: isHorizontal @@ -284,7 +128,7 @@ export const ReferenceLineAnnotations = ({ const sharedStyle = { strokeWidth: yConfig.lineWidth || 1, - stroke: yConfig.color || defaultColor, + stroke: yConfig.color || defaultReferenceLineColor, dash: dashStyle, }; @@ -355,7 +199,7 @@ export const ReferenceLineAnnotations = ({ })} style={{ ...sharedStyle, - fill: yConfig.color || defaultColor, + fill: yConfig.color || defaultReferenceLineColor, opacity: 0.1, }} /> diff --git a/x-pack/plugins/lens/public/xy_visualization/index.ts b/x-pack/plugins/lens/public/xy_visualization/index.ts index 9697ba149e16e0..cfeb1387f689c4 100644 --- a/x-pack/plugins/lens/public/xy_visualization/index.ts +++ b/x-pack/plugins/lens/public/xy_visualization/index.ts @@ -6,6 +6,7 @@ */ import type { CoreSetup } from 'kibana/public'; +import { EventAnnotationPluginSetup } from '../../../../../src/plugins/event_annotation/public'; import type { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; import type { EditorFrameSetup } from '../types'; import type { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; @@ -19,6 +20,7 @@ export interface XyVisualizationPluginSetupPlugins { formatFactory: FormatFactory; editorFrame: EditorFrameSetup; charts: ChartsPluginSetup; + eventAnnotation: EventAnnotationPluginSetup; } export class XyVisualization { @@ -28,8 +30,9 @@ export class XyVisualization { ) { editorFrame.registerVisualization(async () => { const { getXyChartRenderer, getXyVisualization } = await import('../async_services'); - const [, { charts, fieldFormats }] = await core.getStartServices(); + const [, { charts, fieldFormats, eventAnnotation }] = await core.getStartServices(); const palettes = await charts.palettes.getPalettes(); + const eventAnnotationService = await eventAnnotation.getService(); const useLegacyTimeAxis = core.uiSettings.get(LEGACY_TIME_AXIS); expressions.registerRenderer( getXyChartRenderer({ @@ -37,6 +40,7 @@ export class XyVisualization { chartsThemeService: charts.theme, chartsActiveCursorService: charts.activeCursor, paletteService: palettes, + eventAnnotationService, timeZone: getTimeZone(core.uiSettings), useLegacyTimeAxis, kibanaTheme: core.theme, @@ -44,6 +48,7 @@ export class XyVisualization { ); return getXyVisualization({ paletteService: palettes, + eventAnnotationService, fieldFormats, useLegacyTimeAxis, kibanaTheme: core.theme, diff --git a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx index ac50a81da5423b..8b6a96ce24d44f 100644 --- a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx @@ -14,7 +14,7 @@ import type { YConfig, } from '../../common/expressions'; import { Datatable } from '../../../../../src/plugins/expressions/public'; -import type { AccessorConfig, DatasourcePublicAPI, FramePublicAPI, Visualization } from '../types'; +import type { DatasourcePublicAPI, FramePublicAPI, Visualization } from '../types'; import { groupAxesByType } from './axes_configuration'; import { isHorizontalChart, isPercentageSeries, isStackedChart } from './state_helpers'; import type { XYState } from './types'; @@ -27,6 +27,7 @@ import { } from './visualization_helpers'; import { generateId } from '../id_generator'; import { LensIconChartBarReferenceLine } from '../assets/chart_bar_reference_line'; +import { defaultReferenceLineColor } from './color_assignment'; export interface ReferenceLineBase { label: 'x' | 'yRight' | 'yLeft'; @@ -360,18 +361,29 @@ export const setReferenceDimension: Visualization['setDimension'] = ({ }; }; +const getSingleColorConfig = (id: string, color = defaultReferenceLineColor) => ({ + columnId: id, + triggerIcon: 'color' as const, + color, +}); + +export const getReferenceLineAccessorColorConfig = (layer: XYReferenceLineLayerConfig) => { + return layer.accessors.map((accessor) => { + const currentYConfig = layer.yConfig?.find((yConfig) => yConfig.forAccessor === accessor); + return getSingleColorConfig(accessor, currentYConfig?.color); + }); +}; + export const getReferenceConfiguration = ({ state, frame, layer, sortedAccessors, - mappedAccessors, }: { state: XYState; frame: FramePublicAPI; layer: XYReferenceLineLayerConfig; sortedAccessors: string[]; - mappedAccessors: AccessorConfig[]; }) => { const idToIndex = sortedAccessors.reduce>((memo, id, index) => { memo[id] = index; @@ -420,11 +432,7 @@ export const getReferenceConfiguration = ({ groups: groupsToShow.map(({ config = [], id, label, dataTestSubj, valid }) => ({ groupId: id, groupLabel: getAxisName(label, { isHorizontal }), - accessors: config.map(({ forAccessor, color }) => ({ - columnId: forAccessor, - color: color || mappedAccessors.find(({ columnId }) => columnId === forAccessor)?.color, - triggerIcon: 'color' as const, - })), + accessors: config.map(({ forAccessor, color }) => getSingleColorConfig(forAccessor, color)), filterOperations: isNumericMetric, supportsMoreColumns: true, required: false, diff --git a/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts b/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts index dee78997401736..e0984e62cb9cc1 100644 --- a/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts +++ b/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts @@ -16,7 +16,7 @@ import type { XYReferenceLineLayerConfig, } from '../../common/expressions'; import { visualizationTypes } from './types'; -import { getDataLayers, isDataLayer } from './visualization_helpers'; +import { getDataLayers, isAnnotationsLayer, isDataLayer } from './visualization_helpers'; export function isHorizontalSeries(seriesType: SeriesType) { return ( @@ -53,6 +53,9 @@ export function getIconForSeries(type: SeriesType): EuiIconType { } export const getSeriesColor = (layer: XYLayerConfig, accessor: string) => { + if (isAnnotationsLayer(layer)) { + return layer?.annotations?.find((ann) => ann.id === accessor)?.color || null; + } if (isDataLayer(layer) && layer.splitAccessor) { return null; } diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts index fa992d8829b202..2e3db8f2f6f939 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts @@ -15,6 +15,7 @@ import { layerTypes } from '../../common'; import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; import { defaultReferenceLineColor } from './color_assignment'; import { themeServiceMock } from '../../../../../src/core/public/mocks'; +import { eventAnnotationServiceMock } from 'src/plugins/event_annotation/public/mocks'; describe('#toExpression', () => { const xyVisualization = getXyVisualization({ @@ -22,6 +23,7 @@ describe('#toExpression', () => { fieldFormats: fieldFormatsServiceMock.createStartContract(), kibanaTheme: themeServiceMock.createStartContract(), useLegacyTimeAxis: false, + eventAnnotationService: eventAnnotationServiceMock, }); let mockDatasource: ReturnType; let frame: ReturnType; diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index a9c166a9c13eb1..ade90ff98e5539 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -8,29 +8,40 @@ import { Ast } from '@kbn/interpreter'; import { ScaleType } from '@elastic/charts'; import { PaletteRegistry } from 'src/plugins/charts/public'; +import { EventAnnotationServiceType } from 'src/plugins/event_annotation/public'; import { State } from './types'; import { OperationMetadata, DatasourcePublicAPI } from '../types'; import { getColumnToLabelMap } from './state_helpers'; import type { ValidLayer, - XYDataLayerConfig, + XYAnnotationLayerConfig, XYReferenceLineLayerConfig, YConfig, + XYDataLayerConfig, } from '../../common/expressions'; import { layerTypes } from '../../common'; import { hasIcon } from './xy_config_panel/shared/icon_select'; import { defaultReferenceLineColor } from './color_assignment'; import { getDefaultVisualValuesForLayer } from '../shared_components/datasource_default_values'; -import { isDataLayer } from './visualization_helpers'; +import { + getLayerTypeOptions, + getDataLayers, + getReferenceLayers, + getAnnotationsLayers, +} from './visualization_helpers'; +import { defaultAnnotationLabel } from './annotations/config_panel'; +import { getUniqueLabels } from './annotations/helpers'; export const getSortedAccessors = ( datasource: DatasourcePublicAPI, layer: XYDataLayerConfig | XYReferenceLineLayerConfig ) => { const originalOrder = datasource - .getTableSpec() - .map(({ columnId }: { columnId: string }) => columnId) - .filter((columnId: string) => layer.accessors.includes(columnId)); + ? datasource + .getTableSpec() + .map(({ columnId }: { columnId: string }) => columnId) + .filter((columnId: string) => layer.accessors.includes(columnId)) + : layer.accessors; // When we add a column it could be empty, and therefore have no order return Array.from(new Set(originalOrder.concat(layer.accessors))); }; @@ -39,7 +50,8 @@ export const toExpression = ( state: State, datasourceLayers: Record, paletteService: PaletteRegistry, - attributes: Partial<{ title: string; description: string }> = {} + attributes: Partial<{ title: string; description: string }> = {}, + eventAnnotationService: EventAnnotationServiceType ): Ast | null => { if (!state || !state.layers.length) { return null; @@ -49,38 +61,58 @@ export const toExpression = ( state.layers.forEach((layer) => { metadata[layer.layerId] = {}; const datasource = datasourceLayers[layer.layerId]; - datasource.getTableSpec().forEach((column) => { - const operation = datasourceLayers[layer.layerId].getOperationForColumnId(column.columnId); - metadata[layer.layerId][column.columnId] = operation; - }); + if (datasource) { + datasource.getTableSpec().forEach((column) => { + const operation = datasourceLayers[layer.layerId].getOperationForColumnId(column.columnId); + metadata[layer.layerId][column.columnId] = operation; + }); + } }); - return buildExpression(state, metadata, datasourceLayers, paletteService, attributes); + return buildExpression( + state, + metadata, + datasourceLayers, + paletteService, + attributes, + eventAnnotationService + ); +}; + +const simplifiedLayerExpression = { + [layerTypes.DATA]: (layer: XYDataLayerConfig) => ({ ...layer, hide: true }), + [layerTypes.REFERENCELINE]: (layer: XYReferenceLineLayerConfig) => ({ + ...layer, + hide: true, + yConfig: layer.yConfig?.map(({ lineWidth, ...rest }) => ({ + ...rest, + lineWidth: 1, + icon: undefined, + textVisibility: false, + })), + }), + [layerTypes.ANNOTATIONS]: (layer: XYAnnotationLayerConfig) => ({ + ...layer, + hide: true, + annotations: layer.annotations?.map(({ lineWidth, ...rest }) => ({ + ...rest, + lineWidth: 1, + icon: undefined, + textVisibility: false, + })), + }), }; export function toPreviewExpression( state: State, datasourceLayers: Record, - paletteService: PaletteRegistry + paletteService: PaletteRegistry, + eventAnnotationService: EventAnnotationServiceType ) { return toExpression( { ...state, - layers: state.layers.map((layer) => - isDataLayer(layer) - ? { ...layer, hide: true } - : // cap the reference line to 1px - { - ...layer, - hide: true, - yConfig: layer.yConfig?.map(({ lineWidth, ...config }) => ({ - ...config, - lineWidth: 1, - icon: undefined, - textVisibility: false, - })), - } - ), + layers: state.layers.map((layer) => getLayerTypeOptions(layer, simplifiedLayerExpression)), // hide legend for preview legend: { ...state.legend, @@ -90,7 +122,8 @@ export function toPreviewExpression( }, datasourceLayers, paletteService, - {} + {}, + eventAnnotationService ); } @@ -125,23 +158,35 @@ export const buildExpression = ( metadata: Record>, datasourceLayers: Record, paletteService: PaletteRegistry, - attributes: Partial<{ title: string; description: string }> = {} + attributes: Partial<{ title: string; description: string }> = {}, + eventAnnotationService: EventAnnotationServiceType ): Ast | null => { - const validLayers = state.layers + const validDataLayers = getDataLayers(state.layers) .filter((layer): layer is ValidLayer => Boolean(layer.accessors.length)) - .map((layer) => { - if (!datasourceLayers) { - return layer; - } - const sortedAccessors = getSortedAccessors(datasourceLayers[layer.layerId], layer); + .map((layer) => ({ + ...layer, + accessors: getSortedAccessors(datasourceLayers[layer.layerId], layer), + })); + + // sorting doesn't change anything so we don't sort reference layers (TODO: should we make it work?) + const validReferenceLayers = getReferenceLayers(state.layers).filter((layer) => + Boolean(layer.accessors.length) + ); + const uniqueLabels = getUniqueLabels(state.layers); + const validAnnotationsLayers = getAnnotationsLayers(state.layers) + .filter((layer) => Boolean(layer.annotations.length)) + .map((layer) => { return { ...layer, - accessors: sortedAccessors, + annotations: layer.annotations.map((c) => ({ + ...c, + label: uniqueLabels[c.id], + })), }; }); - if (!validLayers.length) { + if (!validDataLayers.length) { return null; } @@ -309,20 +354,25 @@ export const buildExpression = ( valueLabels: [state?.valueLabels || 'hide'], hideEndzones: [state?.hideEndzones || false], valuesInLegend: [state?.valuesInLegend || false], - layers: validLayers.map((layer) => { - if (isDataLayer(layer)) { - return dataLayerToExpression( + layers: [ + ...validDataLayers.map((layer) => + dataLayerToExpression( layer, datasourceLayers[layer.layerId], metadata, paletteService - ); - } - return referenceLineLayerToExpression( - layer, - datasourceLayers[(layer as XYReferenceLineLayerConfig).layerId] - ); - }), + ) + ), + ...validReferenceLayers.map((layer) => + referenceLineLayerToExpression( + layer, + datasourceLayers[(layer as XYReferenceLineLayerConfig).layerId] + ) + ), + ...validAnnotationsLayers.map((layer) => + annotationLayerToExpression(layer, eventAnnotationService) + ), + ], }, }, ], @@ -355,6 +405,41 @@ const referenceLineLayerToExpression = ( }; }; +const annotationLayerToExpression = ( + layer: XYAnnotationLayerConfig, + eventAnnotationService: EventAnnotationServiceType +): Ast => { + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_xy_annotation_layer', + arguments: { + hide: [Boolean(layer.hide)], + layerId: [layer.layerId], + layerType: [layerTypes.ANNOTATIONS], + annotations: layer.annotations + ? layer.annotations.map( + (ann): Ast => + eventAnnotationService.toExpression({ + time: ann.key.timestamp, + label: ann.label || defaultAnnotationLabel, + textVisibility: ann.textVisibility, + icon: ann.icon, + lineStyle: ann.lineStyle, + lineWidth: ann.lineWidth, + color: ann.color, + isHidden: Boolean(ann.isHidden), + }) + ) + : [], + }, + }, + ], + }; +}; + const dataLayerToExpression = ( layer: ValidLayer, datasourceLayer: DatasourcePublicAPI, diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index 07e411b1993c95..b93cf317e1b2f0 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -8,7 +8,7 @@ import { getXyVisualization } from './visualization'; import { Position } from '@elastic/charts'; import { Operation, VisualizeEditorContext, Suggestion, OperationDescriptor } from '../types'; -import type { State, XYSuggestion } from './types'; +import type { State, XYState, XYSuggestion } from './types'; import type { SeriesType, XYDataLayerConfig, @@ -23,6 +23,18 @@ import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks' import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; import { Datatable } from 'src/plugins/expressions'; import { themeServiceMock } from '../../../../../src/core/public/mocks'; +import { eventAnnotationServiceMock } from 'src/plugins/event_annotation/public/mocks'; +import { EventAnnotationConfig } from 'src/plugins/event_annotation/common'; + +const exampleAnnotation: EventAnnotationConfig = { + id: 'an1', + label: 'Event 1', + key: { + type: 'point_in_time', + timestamp: '2022-03-18T08:25:17.140Z', + }, + icon: 'circle', +}; function exampleState(): State { return { @@ -49,6 +61,7 @@ const xyVisualization = getXyVisualization({ fieldFormats: fieldFormatsMock, useLegacyTimeAxis: false, kibanaTheme: themeServiceMock.createStartContract(), + eventAnnotationService: eventAnnotationServiceMock, }); describe('xy_visualization', () => { @@ -149,7 +162,7 @@ describe('xy_visualization', () => { expect(initialState.layers).toHaveLength(1); expect((initialState.layers[0] as XYDataLayerConfig).xAccessor).not.toBeDefined(); - expect(initialState.layers[0].accessors).toHaveLength(0); + expect((initialState.layers[0] as XYDataLayerConfig).accessors).toHaveLength(0); expect(initialState).toMatchInlineSnapshot(` Object { @@ -227,12 +240,63 @@ describe('xy_visualization', () => { describe('#getSupportedLayers', () => { it('should return a double layer types', () => { - expect(xyVisualization.getSupportedLayers()).toHaveLength(2); + expect(xyVisualization.getSupportedLayers()).toHaveLength(3); }); it('should return the icon for the visualization type', () => { expect(xyVisualization.getSupportedLayers()[0].icon).not.toBeUndefined(); }); + describe('annotations', () => { + let mockDatasource: ReturnType; + let frame: ReturnType; + beforeEach(() => { + frame = createMockFramePublicAPI(); + mockDatasource = createMockDatasource('testDatasource'); + + frame.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + frame.datasourceLayers.first.getOperationForColumnId = jest.fn((accessor) => { + if (accessor === 'a') { + return { + dataType: 'date', + isBucketed: true, + scale: 'interval', + label: 'date_histogram', + isStaticValue: false, + hasTimeShift: false, + }; + } + return null; + }); + + frame.activeData = { + first: { + type: 'datatable', + rows: [], + columns: [], + }, + }; + }); + it('when there is no date histogram annotation layer is disabled', () => { + const supportedAnnotationLayer = xyVisualization + .getSupportedLayers(exampleState()) + .find((a) => a.type === 'annotations'); + expect(supportedAnnotationLayer?.disabled).toBeTruthy(); + }); + it('for data with date histogram annotation layer is enabled and calculates initial dimensions', () => { + const supportedAnnotationLayer = xyVisualization + .getSupportedLayers(exampleState(), frame) + .find((a) => a.type === 'annotations'); + expect(supportedAnnotationLayer?.disabled).toBeFalsy(); + expect(supportedAnnotationLayer?.noDatasource).toBeTruthy(); + expect(supportedAnnotationLayer?.initialDimensions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ groupId: 'xAnnotations', columnId: expect.any(String) }), + ]) + ); + }); + }); }); describe('#getLayerType', () => { @@ -358,6 +422,45 @@ describe('xy_visualization', () => { ], }); }); + + describe('annotations', () => { + it('should add a dimension to a annotation layer', () => { + jest.spyOn(Date, 'now').mockReturnValue(new Date('2022-04-18T11:01:58.135Z').valueOf()); + expect( + xyVisualization.setDimension({ + frame, + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'annotation', + layerType: layerTypes.ANNOTATIONS, + annotations: [exampleAnnotation], + }, + ], + }, + layerId: 'annotation', + groupId: 'xAnnotation', + columnId: 'newCol', + }).layers[0] + ).toEqual({ + layerId: 'annotation', + layerType: layerTypes.ANNOTATIONS, + annotations: [ + exampleAnnotation, + { + icon: 'triangle', + id: 'newCol', + key: { + timestamp: '2022-04-18T11:01:58.135Z', + type: 'point_in_time', + }, + label: 'Event', + }, + ], + }); + }); + }); }); describe('#updateLayersConfigurationFromContext', () => { @@ -472,9 +575,10 @@ describe('xy_visualization', () => { layerId: 'first', context: newContext, }); - expect(state?.layers[0]).toHaveProperty('seriesType', 'area'); - expect(state?.layers[0]).toHaveProperty('layerType', 'referenceLine'); - expect(state?.layers[0].yConfig).toStrictEqual([ + const firstLayer = state?.layers[0] as XYDataLayerConfig; + expect(firstLayer).toHaveProperty('seriesType', 'area'); + expect(firstLayer).toHaveProperty('layerType', 'referenceLine'); + expect(firstLayer.yConfig).toStrictEqual([ { axisMode: 'right', color: '#68BC00', @@ -695,6 +799,45 @@ describe('xy_visualization', () => { accessors: [], }); }); + it('removes annotation dimension', () => { + expect( + xyVisualization.removeDimension({ + frame, + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + layerType: layerTypes.DATA, + seriesType: 'area', + xAccessor: 'a', + accessors: [], + }, + { + layerId: 'ann', + layerType: layerTypes.ANNOTATIONS, + annotations: [exampleAnnotation, { ...exampleAnnotation, id: 'an2' }], + }, + ], + }, + layerId: 'ann', + columnId: 'an2', + }).layers + ).toEqual([ + { + layerId: 'first', + layerType: layerTypes.DATA, + seriesType: 'area', + xAccessor: 'a', + accessors: [], + }, + { + layerId: 'ann', + layerType: layerTypes.ANNOTATIONS, + annotations: [exampleAnnotation], + }, + ]); + }); }); describe('#getConfiguration', () => { @@ -1069,7 +1212,7 @@ describe('xy_visualization', () => { it('should support static value', () => { const state = getStateWithBaseReferenceLine(); - state.layers[0].accessors = []; + (state.layers[1] as XYReferenceLineLayerConfig).accessors = []; (state.layers[1] as XYReferenceLineLayerConfig).yConfig = undefined; expect( xyVisualization.getConfiguration({ @@ -1082,7 +1225,7 @@ describe('xy_visualization', () => { it('should return no referenceLine groups for a empty data layer', () => { const state = getStateWithBaseReferenceLine(); - state.layers[0].accessors = []; + (state.layers[0] as XYDataLayerConfig).accessors = []; (state.layers[1] as XYReferenceLineLayerConfig).yConfig = undefined; const options = xyVisualization.getConfiguration({ @@ -1358,6 +1501,83 @@ describe('xy_visualization', () => { }); }); + describe('annotations', () => { + beforeEach(() => { + frame = createMockFramePublicAPI(); + mockDatasource = createMockDatasource('testDatasource'); + + frame.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + frame.datasourceLayers.first.getOperationForColumnId = jest.fn((accessor) => { + if (accessor === 'a') { + return { + dataType: 'date', + isBucketed: true, + scale: 'interval', + label: 'date_histogram', + isStaticValue: false, + hasTimeShift: false, + }; + } + return null; + }); + + frame.activeData = { + first: { + type: 'datatable', + rows: [], + columns: [], + }, + }; + }); + + function getStateWithAnnotationLayer(): State { + return { + ...exampleState(), + layers: [ + { + layerId: 'first', + layerType: layerTypes.DATA, + seriesType: 'area', + splitAccessor: undefined, + xAccessor: 'a', + accessors: ['b'], + }, + { + layerId: 'annotations', + layerType: layerTypes.ANNOTATIONS, + annotations: [exampleAnnotation], + }, + ], + }; + } + + it('returns configuration correctly', () => { + const state = getStateWithAnnotationLayer(); + const config = xyVisualization.getConfiguration({ + state, + frame, + layerId: 'annotations', + }); + expect(config.groups[0].accessors).toEqual([ + { color: '#f04e98', columnId: 'an1', triggerIcon: 'color' }, + ]); + expect(config.groups[0].invalid).toEqual(false); + }); + + it('When data layer is empty, should return invalid state', () => { + const state = getStateWithAnnotationLayer(); + (state.layers[0] as XYDataLayerConfig).xAccessor = undefined; + const config = xyVisualization.getConfiguration({ + state, + frame, + layerId: 'annotations', + }); + expect(config.groups[0].invalid).toEqual(true); + }); + }); + describe('color assignment', () => { function callConfig(layerConfigOverride: Partial) { const baseState = exampleState(); @@ -1954,4 +2174,87 @@ describe('xy_visualization', () => { `); }); }); + describe('#getUniqueLabels', () => { + it('creates unique labels for single annotations layer with repeating labels', async () => { + const xyState = { + layers: [ + { + layerId: 'layerId', + layerType: 'annotations', + annotations: [ + { + label: 'Event', + id: '1', + }, + { + label: 'Event', + id: '2', + }, + { + label: 'Custom', + id: '3', + }, + ], + }, + ], + } as XYState; + + expect(xyVisualization.getUniqueLabels!(xyState)).toEqual({ + '1': 'Event', + '2': 'Event [1]', + '3': 'Custom', + }); + }); + it('creates unique labels for multiple annotations layers with repeating labels', async () => { + const xyState = { + layers: [ + { + layerId: 'layer1', + layerType: 'annotations', + annotations: [ + { + label: 'Event', + id: '1', + }, + { + label: 'Event', + id: '2', + }, + { + label: 'Custom', + id: '3', + }, + ], + }, + { + layerId: 'layer2', + layerType: 'annotations', + annotations: [ + { + label: 'Event', + id: '4', + }, + { + label: 'Event [1]', + id: '5', + }, + { + label: 'Custom', + id: '6', + }, + ], + }, + ], + } as XYState; + + expect(xyVisualization.getUniqueLabels!(xyState)).toEqual({ + '1': 'Event', + '2': 'Event [1]', + '3': 'Custom', + '4': 'Event [2]', + '5': 'Event [1] [1]', + '6': 'Custom [1]', + }); + }); + }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index c9951c24f8a47e..78fd50f7cfece9 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -13,16 +13,17 @@ import { i18n } from '@kbn/i18n'; import { PaletteRegistry } from 'src/plugins/charts/public'; import { FieldFormatsStart } from 'src/plugins/field_formats/public'; import { ThemeServiceStart } from 'kibana/public'; +import { EventAnnotationServiceType } from '../../../../../src/plugins/event_annotation/public'; import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public'; -import type { FillStyle } from '../../common/expressions/xy_chart'; +import type { FillStyle, XYLayerConfig } from '../../common/expressions/xy_chart'; import { getSuggestions } from './xy_suggestions'; import { XyToolbar } from './xy_config_panel'; import { DimensionEditor } from './xy_config_panel/dimension_editor'; import { LayerHeader } from './xy_config_panel/layer_header'; import type { Visualization, AccessorConfig, FramePublicAPI } from '../types'; import { State, visualizationTypes, XYSuggestion } from './types'; -import { SeriesType, XYDataLayerConfig, XYLayerConfig, YAxisMode } from '../../common/expressions'; +import { SeriesType, XYDataLayerConfig, YAxisMode } from '../../common/expressions'; import { layerTypes } from '../../common'; import { isHorizontalChart } from './state_helpers'; import { toExpression, toPreviewExpression, getSortedAccessors } from './to_expression'; @@ -34,6 +35,12 @@ import { getReferenceSupportedLayer, setReferenceDimension, } from './reference_line_helpers'; +import { + getAnnotationsConfiguration, + getAnnotationsSupportedLayer, + setAnnotationsDimension, + getUniqueLabels, +} from './annotations/helpers'; import { checkXAccessorCompatibility, defaultSeriesType, @@ -42,7 +49,9 @@ import { getDescription, getFirstDataLayer, getLayersByType, + getReferenceLayers, getVisualizationType, + isAnnotationsLayer, isBucketed, isDataLayer, isNumericDynamicMetric, @@ -54,14 +63,18 @@ import { import { groupAxesByType } from './axes_configuration'; import { XYState } from '..'; import { ReferenceLinePanel } from './xy_config_panel/reference_line_panel'; +import { DimensionTrigger } from '../shared_components/dimension_trigger'; +import { AnnotationsPanel, defaultAnnotationLabel } from './annotations/config_panel'; export const getXyVisualization = ({ paletteService, fieldFormats, useLegacyTimeAxis, kibanaTheme, + eventAnnotationService, }: { paletteService: PaletteRegistry; + eventAnnotationService: EventAnnotationServiceType; fieldFormats: FieldFormatsStart; useLegacyTimeAxis: boolean; kibanaTheme: ThemeServiceStart; @@ -155,7 +168,11 @@ export const getXyVisualization = ({ }, getSupportedLayers(state, frame) { - return [supportedDataLayer, getReferenceSupportedLayer(state, frame)]; + return [ + supportedDataLayer, + getAnnotationsSupportedLayer(state, frame), + getReferenceSupportedLayer(state, frame), + ]; }, getConfiguration({ state, frame, layerId }) { @@ -164,10 +181,18 @@ export const getXyVisualization = ({ return { groups: [] }; } + if (isAnnotationsLayer(layer)) { + return getAnnotationsConfiguration({ state, frame, layer }); + } + const sortedAccessors: string[] = getSortedAccessors( frame.datasourceLayers[layer.layerId], layer ); + if (isReferenceLayer(layer)) { + return getReferenceConfiguration({ state, frame, layer, sortedAccessors }); + } + const mappedAccessors = getMappedAccessors({ state, frame, @@ -177,11 +202,7 @@ export const getXyVisualization = ({ accessors: sortedAccessors, }); - if (isReferenceLayer(layer)) { - return getReferenceConfiguration({ state, frame, layer, sortedAccessors, mappedAccessors }); - } const dataLayers = getDataLayers(state.layers); - const isHorizontal = isHorizontalChart(state.layers); const { left, right } = groupAxesByType([layer], frame.activeData); // Check locally if it has one accessor OR one accessor per axis @@ -275,6 +296,9 @@ export const getXyVisualization = ({ if (isReferenceLayer(foundLayer)) { return setReferenceDimension(props); } + if (isAnnotationsLayer(foundLayer)) { + return setAnnotationsDimension(props); + } const newLayer = { ...foundLayer }; if (groupId === 'x') { @@ -295,7 +319,7 @@ export const getXyVisualization = ({ updateLayersConfigurationFromContext({ prevState, layerId, context }) { const { chartType, axisPosition, palette, metrics } = context; const foundLayer = prevState?.layers.find((l) => l.layerId === layerId); - if (!foundLayer) { + if (!foundLayer || !isDataLayer(foundLayer)) { return prevState; } const isReferenceLine = metrics.some((metric) => metric.agg === 'static_value'); @@ -377,7 +401,16 @@ export const getXyVisualization = ({ if (!foundLayer) { return prevState; } - const dataLayers = getDataLayers(prevState.layers); + if (isAnnotationsLayer(foundLayer)) { + const newLayer = { ...foundLayer }; + newLayer.annotations = newLayer.annotations.filter(({ id }) => id !== columnId); + + const newLayers = prevState.layers.map((l) => (l.layerId === layerId ? newLayer : l)); + return { + ...prevState, + layers: newLayers, + }; + } const newLayer = { ...foundLayer }; if (isDataLayer(newLayer)) { if (newLayer.xAccessor === columnId) { @@ -392,15 +425,15 @@ export const getXyVisualization = ({ newLayer.accessors = newLayer.accessors.filter((a) => a !== columnId); } - if (newLayer.yConfig) { - newLayer.yConfig = newLayer.yConfig.filter(({ forAccessor }) => forAccessor !== columnId); + if ('yConfig' in newLayer) { + newLayer.yConfig = newLayer.yConfig?.filter(({ forAccessor }) => forAccessor !== columnId); } let newLayers = prevState.layers.map((l) => (l.layerId === layerId ? newLayer : l)); // check if there's any reference layer and pull it off if all data layers have no dimensions set // check for data layers if they all still have xAccessors const groupsAvailable = getGroupsAvailableInData( - dataLayers, + getDataLayers(prevState.layers), frame.datasourceLayers, frame?.activeData ); @@ -410,7 +443,9 @@ export const getXyVisualization = ({ (id) => !groupsAvailable[id] ) ) { - newLayers = newLayers.filter((layer) => isDataLayer(layer) || layer.accessors.length); + newLayers = newLayers.filter( + (layer) => isDataLayer(layer) || ('accessors' in layer && layer.accessors.length) + ); } return { @@ -450,9 +485,12 @@ export const getXyVisualization = ({ const layer = props.state.layers.find((l) => l.layerId === props.layerId)!; const dimensionEditor = isReferenceLayer(layer) ? ( + ) : isAnnotationsLayer(layer) ? ( + ) : ( ); + render( {dimensionEditor} @@ -462,8 +500,9 @@ export const getXyVisualization = ({ }, toExpression: (state, layers, attributes) => - toExpression(state, layers, paletteService, attributes), - toPreviewExpression: (state, layers) => toPreviewExpression(state, layers, paletteService), + toExpression(state, layers, paletteService, attributes, eventAnnotationService), + toPreviewExpression: (state, layers) => + toPreviewExpression(state, layers, paletteService, eventAnnotationService), getErrorMessages(state, datasourceLayers) { // Data error handling below here @@ -504,7 +543,7 @@ export const getXyVisualization = ({ // temporary fix for #87068 errors.push(...checkXAccessorCompatibility(state, datasourceLayers)); - for (const layer of state.layers) { + for (const layer of getDataLayers(state.layers)) { const datasourceAPI = datasourceLayers[layer.layerId]; if (datasourceAPI) { for (const accessor of layer.accessors) { @@ -540,9 +579,10 @@ export const getXyVisualization = ({ return; } - const layers = state.layers; - - const filteredLayers = layers.filter(({ accessors }: XYLayerConfig) => accessors.length > 0); + const filteredLayers = [ + ...getDataLayers(state.layers), + ...getReferenceLayers(state.layers), + ].filter(({ accessors }) => accessors.length > 0); const accessorsWithArrayValues = []; for (const layer of filteredLayers) { const { layerId, accessors } = layer; @@ -569,6 +609,35 @@ export const getXyVisualization = ({ /> )); }, + getUniqueLabels(state) { + return getUniqueLabels(state.layers); + }, + renderDimensionTrigger({ + columnId, + label, + hideTooltip, + invalid, + invalidMessage, + }: { + columnId: string; + label?: string; + hideTooltip?: boolean; + invalid?: boolean; + invalidMessage?: string; + }) { + if (label) { + return ( + + ); + } + return null; + }, }); const getMappedAccessors = ({ @@ -584,7 +653,7 @@ const getMappedAccessors = ({ paletteService: PaletteRegistry; fieldFormats: FieldFormatsStart; state: XYState; - layer: XYLayerConfig; + layer: XYDataLayerConfig; }) => { let mappedAccessors: AccessorConfig[] = accessors.map((accessor) => ({ columnId: accessor, @@ -592,7 +661,7 @@ const getMappedAccessors = ({ if (frame.activeData) { const colorAssignments = getColorAssignments( - state.layers, + getDataLayers(state.layers), { tables: frame.activeData }, fieldFormats.deserialize ); diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx index 7446c2a06119c4..23c2446ca23631 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx @@ -11,8 +11,12 @@ import { DatasourcePublicAPI, OperationMetadata, VisualizationType } from '../ty import { State, visualizationTypes, XYState } from './types'; import { isHorizontalChart } from './state_helpers'; import { + AnnotationLayerArgs, + DataLayerArgs, SeriesType, + XYAnnotationLayerConfig, XYDataLayerConfig, + XYLayerArgs, XYLayerConfig, XYReferenceLineLayerConfig, } from '../../common/expressions'; @@ -130,9 +134,12 @@ export function checkScaleOperation( export const isDataLayer = (layer: Pick): layer is XYDataLayerConfig => layer.layerType === layerTypes.DATA || !layer.layerType; -export const getDataLayers = (layers: XYLayerConfig[]) => +export const getDataLayers = (layers: Array>) => (layers || []).filter((layer): layer is XYDataLayerConfig => isDataLayer(layer)); +export const getDataLayersArgs = (layers: XYLayerArgs[]) => + (layers || []).filter((layer): layer is DataLayerArgs => isDataLayer(layer)); + export const getFirstDataLayer = (layers: XYLayerConfig[]) => (layers || []).find((layer): layer is XYDataLayerConfig => isDataLayer(layer)); @@ -140,9 +147,34 @@ export const isReferenceLayer = ( layer: Pick ): layer is XYReferenceLineLayerConfig => layer.layerType === layerTypes.REFERENCELINE; -export const getReferenceLayers = (layers: XYLayerConfig[]) => +export const getReferenceLayers = (layers: Array>) => (layers || []).filter((layer): layer is XYReferenceLineLayerConfig => isReferenceLayer(layer)); +export const isAnnotationsLayer = ( + layer: Pick +): layer is XYAnnotationLayerConfig => layer.layerType === layerTypes.ANNOTATIONS; + +export const getAnnotationsLayers = (layers: Array>) => + (layers || []).filter((layer): layer is XYAnnotationLayerConfig => isAnnotationsLayer(layer)); + +export const getAnnotationsLayersArgs = (layers: XYLayerArgs[]) => + (layers || []).filter((layer): layer is AnnotationLayerArgs => isAnnotationsLayer(layer)); + +export interface LayerTypeToLayer { + [layerTypes.DATA]: (layer: XYDataLayerConfig) => XYDataLayerConfig; + [layerTypes.REFERENCELINE]: (layer: XYReferenceLineLayerConfig) => XYReferenceLineLayerConfig; + [layerTypes.ANNOTATIONS]: (layer: XYAnnotationLayerConfig) => XYAnnotationLayerConfig; +} + +export const getLayerTypeOptions = (layer: XYLayerConfig, options: LayerTypeToLayer) => { + if (isDataLayer(layer)) { + return options[layerTypes.DATA](layer); + } else if (isReferenceLayer(layer)) { + return options[layerTypes.REFERENCELINE](layer); + } + return options[layerTypes.ANNOTATIONS](layer); +}; + export function getVisualizationType(state: State): VisualizationType | 'mixed' { if (!state.layers.length) { return ( @@ -255,6 +287,11 @@ const newLayerFn = { layerType: layerTypes.REFERENCELINE, accessors: [], }), + [layerTypes.ANNOTATIONS]: ({ layerId }: { layerId: string }): XYAnnotationLayerConfig => ({ + layerId, + layerType: layerTypes.ANNOTATIONS, + annotations: [], + }), }; export function newLayerState({ diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx index 8aa2aaf16ae5fa..b448ebfbd455ef 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx @@ -9,6 +9,7 @@ import React, { useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiColorPicker, EuiColorPickerProps, EuiToolTip, EuiIcon } from '@elastic/eui'; import type { PaletteRegistry } from 'src/plugins/charts/public'; +import { defaultAnnotationColor } from '../../../../../../src/plugins/event_annotation/public'; import type { VisualizationDimensionEditorProps } from '../../types'; import { State } from '../types'; import { FormatFactory } from '../../../common'; @@ -20,7 +21,7 @@ import { } from '../color_assignment'; import { getSortedAccessors } from '../to_expression'; import { TooltipWrapper } from '../../shared_components'; -import { isReferenceLayer } from '../visualization_helpers'; +import { isReferenceLayer, isAnnotationsLayer, getDataLayers } from '../visualization_helpers'; const tooltipContent = { auto: i18n.translate('xpack.lens.configPanel.color.tooltip.auto', { @@ -62,15 +63,17 @@ export const ColorPicker = ({ if (overwriteColor || !frame.activeData) return overwriteColor; if (isReferenceLayer(layer)) { return defaultReferenceLineColor; + } else if (isAnnotationsLayer(layer)) { + return defaultAnnotationColor; } const sortedAccessors: string[] = getSortedAccessors( - frame.datasourceLayers[layer.layerId], + frame.datasourceLayers[layer.layerId] ?? layer.accessors, layer ); const colorAssignments = getColorAssignments( - state.layers, + getDataLayers(state.layers), { tables: frame.activeData }, formatFactory ); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx index 465a627fa33b21..c4e5268cfb8af7 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx @@ -16,8 +16,9 @@ import { trackUiEvent } from '../../lens_ui_telemetry'; import { StaticHeader } from '../../shared_components'; import { ToolbarButton } from '../../../../../../src/plugins/kibana_react/public'; import { LensIconChartBarReferenceLine } from '../../assets/chart_bar_reference_line'; +import { LensIconChartBarAnnotations } from '../../assets/chart_bar_annotations'; import { updateLayer } from '.'; -import { isReferenceLayer } from '../visualization_helpers'; +import { isAnnotationsLayer, isReferenceLayer } from '../visualization_helpers'; export function LayerHeader(props: VisualizationLayerWidgetProps) { const layer = props.state.layers.find((l) => l.layerId === props.layerId); @@ -26,6 +27,8 @@ export function LayerHeader(props: VisualizationLayerWidgetProps) { } if (isReferenceLayer(layer)) { return ; + } else if (isAnnotationsLayer(layer)) { + return ; } return ; } @@ -41,6 +44,17 @@ function ReferenceLayerHeader() { ); } +function AnnotationsLayerHeader() { + return ( + + ); +} + function DataLayerHeader(props: VisualizationLayerWidgetProps) { const [isPopoverOpen, setPopoverIsOpen] = useState(false); const { state, layerId } = props; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx index f00d60b0dc814c..78020034c3d439 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx @@ -70,6 +70,7 @@ export const ReferenceLinePanel = ( return ( <> + {' '} ; + +export const euiIconsSet = [ { value: 'empty', label: i18n.translate('xpack.lens.xyChart.iconSelect.noIconLabel', { @@ -70,29 +72,35 @@ const icons = [ }, ]; -const IconView = (props: { value?: string; label: string }) => { +const IconView = (props: { value?: string; label: string; icon?: IconType }) => { if (!props.value) return null; return ( - - - {` ${props.label}`} - + + + + + {props.label} + ); }; export const IconSelect = ({ value, onChange, + customIconSet = euiIconsSet, }: { value?: string; onChange: (newIcon: string) => void; + customIconSet?: IconSet; }) => { - const selectedIcon = icons.find((option) => value === option.value) || icons[0]; + const selectedIcon = + customIconSet.find((option) => value === option.value) || + customIconSet.find((option) => option.value === 'empty')!; return ( { onChange(selection[0].value!); @@ -100,7 +108,11 @@ export const IconSelect = ({ singleSelection={{ asPlainText: true }} renderOption={IconView} compressed - prepend={hasIcon(selectedIcon.value) ? : undefined} + prepend={ + hasIcon(selectedIcon.value) ? ( + + ) : undefined + } /> ); }; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/line_style_settings.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/line_style_settings.tsx index db01a027d8fec5..766d5462db787a 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/line_style_settings.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/line_style_settings.tsx @@ -40,8 +40,8 @@ export const LineStyleSettings = ({ defaultMessage: 'Line', })} > - - + + { @@ -49,9 +49,8 @@ export const LineStyleSettings = ({ }} /> - + void; isHorizontal: boolean; + customIconSet?: IconSet; }) => { return ( <> @@ -133,13 +136,15 @@ export const MarkerDecorationSettings = ({ })} > { setConfig({ icon: newIcon }); }} /> - {hasIcon(currentConfig?.icon) || currentConfig?.textVisibility ? ( + {currentConfig?.iconPosition && + (hasIcon(currentConfig?.icon) || currentConfig?.textVisibility) ? ( { @@ -533,6 +535,60 @@ describe('xy_suggestions', () => { ); }); + test('passes annotation layer without modifying it', () => { + const annotationLayer: XYAnnotationLayerConfig = { + layerId: 'second', + layerType: layerTypes.ANNOTATIONS, + annotations: [ + { + id: '1', + key: { + type: 'point_in_time', + timestamp: '2020-20-22', + }, + label: 'annotation', + }, + ], + }; + const currentState: XYState = { + legend: { isVisible: true, position: 'bottom' }, + valueLabels: 'hide', + preferredSeriesType: 'bar', + fittingFunction: 'None', + layers: [ + { + accessors: ['price'], + layerId: 'first', + layerType: layerTypes.DATA, + seriesType: 'bar', + splitAccessor: 'date', + xAccessor: 'product', + }, + annotationLayer, + ], + }; + const suggestions = getSuggestions({ + table: { + isMultiRow: true, + columns: [numCol('price'), dateCol('date'), strCol('product')], + layerId: 'first', + changeType: 'unchanged', + }, + state: currentState, + keptLayerIds: [], + }); + + suggestions.every((suggestion) => + expect(suggestion.state.layers).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + layerType: layerTypes.ANNOTATIONS, + }), + ]) + ) + ); + }); + test('includes passed in palette for split charts if specified', () => { const mainPalette: PaletteOutput = { type: 'palette', name: 'mock' }; const [suggestion] = getSuggestions({ diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts index 1578442b528154..bd5a37c206c6cf 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -521,7 +521,10 @@ function buildSuggestion({ const keptLayers = currentState ? currentState.layers // Remove layers that aren't being suggested - .filter((layer) => keptLayerIds.includes(layer.layerId)) + .filter( + (layer) => + keptLayerIds.includes(layer.layerId) || layer.layerType === layerTypes.ANNOTATIONS + ) // Update in place .map((layer) => (layer.layerId === layerId ? newLayer : layer)) // Replace the seriesType on all previous layers diff --git a/x-pack/plugins/lens/server/expressions/expressions.ts b/x-pack/plugins/lens/server/expressions/expressions.ts index 84e238b3eb15ea..c68fed23a7fdb9 100644 --- a/x-pack/plugins/lens/server/expressions/expressions.ts +++ b/x-pack/plugins/lens/server/expressions/expressions.ts @@ -12,6 +12,7 @@ import { yAxisConfig, dataLayerConfig, referenceLineLayerConfig, + annotationLayerConfig, formatColumn, legendConfig, renameColumns, @@ -40,6 +41,7 @@ export const setupExpressions = ( yAxisConfig, dataLayerConfig, referenceLineLayerConfig, + annotationLayerConfig, formatColumn, legendConfig, renameColumns, diff --git a/x-pack/plugins/lens/tsconfig.json b/x-pack/plugins/lens/tsconfig.json index 583e2963a1ca7a..76e25f8b08639b 100644 --- a/x-pack/plugins/lens/tsconfig.json +++ b/x-pack/plugins/lens/tsconfig.json @@ -1,4 +1,3 @@ - { "extends": "../../../tsconfig.base.json", "compilerOptions": { @@ -15,31 +14,86 @@ "../../../typings/**/*" ], "references": [ - { "path": "../spaces/tsconfig.json" }, - { "path": "../../../src/core/tsconfig.json" }, - { "path": "../task_manager/tsconfig.json" }, - { "path": "../global_search/tsconfig.json"}, - { "path": "../saved_objects_tagging/tsconfig.json"}, - { "path": "../../../src/plugins/data/tsconfig.json"}, - { "path": "../../../src/plugins/data_views/tsconfig.json"}, - { "path": "../../../src/plugins/data_view_field_editor/tsconfig.json"}, - { "path": "../../../src/plugins/charts/tsconfig.json"}, - { "path": "../../../src/plugins/expressions/tsconfig.json"}, - { "path": "../../../src/plugins/navigation/tsconfig.json" }, - { "path": "../../../src/plugins/url_forwarding/tsconfig.json" }, - { "path": "../../../src/plugins/visualizations/tsconfig.json" }, - { "path": "../../../src/plugins/dashboard/tsconfig.json" }, - { "path": "../../../src/plugins/ui_actions/tsconfig.json" }, - { "path": "../../../src/plugins/embeddable/tsconfig.json" }, - { "path": "../../../src/plugins/share/tsconfig.json" }, - { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, - { "path": "../../../src/plugins/saved_objects/tsconfig.json"}, - { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, - { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, - { "path": "../../../src/plugins/embeddable/tsconfig.json"}, - { "path": "../../../src/plugins/presentation_util/tsconfig.json"}, - { "path": "../../../src/plugins/field_formats/tsconfig.json"}, - { "path": "../../../src/plugins/chart_expressions/expression_heatmap/tsconfig.json"}, - { "path": "../../../src/plugins/chart_expressions/expression_gauge/tsconfig.json"} + { + "path": "../spaces/tsconfig.json" + }, + { + "path": "../../../src/core/tsconfig.json" + }, + { + "path": "../task_manager/tsconfig.json" + }, + { + "path": "../global_search/tsconfig.json" + }, + { + "path": "../saved_objects_tagging/tsconfig.json" + }, + { + "path": "../../../src/plugins/data/tsconfig.json" + }, + { + "path": "../../../src/plugins/data_views/tsconfig.json" + }, + { + "path": "../../../src/plugins/data_view_field_editor/tsconfig.json" + }, + { + "path": "../../../src/plugins/charts/tsconfig.json" + }, + { + "path": "../../../src/plugins/expressions/tsconfig.json" + }, + { + "path": "../../../src/plugins/navigation/tsconfig.json" + }, + { + "path": "../../../src/plugins/url_forwarding/tsconfig.json" + }, + { + "path": "../../../src/plugins/visualizations/tsconfig.json" + }, + { + "path": "../../../src/plugins/dashboard/tsconfig.json" + }, + { + "path": "../../../src/plugins/ui_actions/tsconfig.json" + }, + { + "path": "../../../src/plugins/embeddable/tsconfig.json" + }, + { + "path": "../../../src/plugins/share/tsconfig.json" + }, + { + "path": "../../../src/plugins/usage_collection/tsconfig.json" + }, + { + "path": "../../../src/plugins/saved_objects/tsconfig.json" + }, + { + "path": "../../../src/plugins/kibana_utils/tsconfig.json" + }, + { + "path": "../../../src/plugins/kibana_react/tsconfig.json" + }, + { + "path": "../../../src/plugins/embeddable/tsconfig.json" + }, + { + "path": "../../../src/plugins/presentation_util/tsconfig.json" + }, + { + "path": "../../../src/plugins/field_formats/tsconfig.json" + }, + { + "path": "../../../src/plugins/chart_expressions/expression_heatmap/tsconfig.json" + }, + { + "path": "../../../src/plugins/chart_expressions/expression_gauge/tsconfig.json" + }, + { + "path": "../../../src/plugins/event_annotation/tsconfig.json" + } ] -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index db10095ce05911..5fbde8959b3641 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -530,7 +530,6 @@ "xpack.lens.indexPattern.ranges.lessThanTooltip": "Inférieur à", "xpack.lens.indexPattern.records": "Enregistrements", "xpack.lens.indexPattern.referenceFunctionPlaceholder": "Sous-fonction", - "xpack.lens.indexPattern.removeColumnAriaLabel": "Ajouter ou glisser-déposer un champ dans {groupLabel}", "xpack.lens.indexPattern.removeColumnLabel": "Retirer la configuration de \"{groupLabel}\"", "xpack.lens.indexPattern.removeFieldLabel": "Retirer le champ du modèle d'indexation", "xpack.lens.indexPattern.sortField.invalid": "Champ non valide. Vérifiez votre modèle d'indexation ou choisissez un autre champ.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2395df6d2d9014..9d1ec062fe1b34 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -611,7 +611,6 @@ "xpack.lens.indexPattern.rareTermsOf": "{name}の希少な値", "xpack.lens.indexPattern.records": "記録", "xpack.lens.indexPattern.referenceFunctionPlaceholder": "サブ関数", - "xpack.lens.indexPattern.removeColumnAriaLabel": "フィールドを追加するか、{groupLabel}までドラッグアンドドロップします", "xpack.lens.indexPattern.removeColumnLabel": "「{groupLabel}」から構成を削除", "xpack.lens.indexPattern.removeFieldLabel": "データビューフィールドを削除", "xpack.lens.indexPattern.sortField.invalid": "無効なフィールドです。データビューを確認するか、別のフィールドを選択してください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 6d4465ae164878..b055d663f9e69b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -617,7 +617,6 @@ "xpack.lens.indexPattern.rareTermsOf": "{name} 的稀有值", "xpack.lens.indexPattern.records": "记录", "xpack.lens.indexPattern.referenceFunctionPlaceholder": "子函数", - "xpack.lens.indexPattern.removeColumnAriaLabel": "将字段添加或拖放到 {groupLabel}", "xpack.lens.indexPattern.removeColumnLabel": "从“{groupLabel}”中删除配置", "xpack.lens.indexPattern.removeFieldLabel": "移除数据视图字段", "xpack.lens.indexPattern.sortField.invalid": "字段无效。检查数据视图或选取其他字段。", From 0b4282e1f5be29f44eab61340d947acaec2326b3 Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Wed, 23 Mar 2022 14:20:26 -0700 Subject: [PATCH 64/64] Fix for process event pagination in session view (#128421) Co-authored-by: mitodrummer --- .../session_view/public/components/session_view/hooks.ts | 4 ++-- .../session_view/server/routes/process_events_route.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/session_view/public/components/session_view/hooks.ts b/x-pack/plugins/session_view/public/components/session_view/hooks.ts index bf8796336602d3..e48b3a335dbd3b 100644 --- a/x-pack/plugins/session_view/public/components/session_view/hooks.ts +++ b/x-pack/plugins/session_view/public/components/session_view/hooks.ts @@ -58,7 +58,7 @@ export const useFetchSessionViewProcessEvents = ( getNextPageParam: (lastPage) => { if (lastPage.events.length === PROCESS_EVENTS_PER_PAGE) { return { - cursor: lastPage.events[lastPage.events.length - 1]['@timestamp'], + cursor: lastPage.events[lastPage.events.length - 1].process.start, forward: true, }; } @@ -66,7 +66,7 @@ export const useFetchSessionViewProcessEvents = ( getPreviousPageParam: (firstPage) => { if (jumpToEvent && firstPage.events.length === PROCESS_EVENTS_PER_PAGE) { return { - cursor: firstPage.events[0]['@timestamp'], + cursor: firstPage.events[0].process.start, forward: false, }; } diff --git a/x-pack/plugins/session_view/server/routes/process_events_route.ts b/x-pack/plugins/session_view/server/routes/process_events_route.ts index 7be1885c70ab12..0dc864c51a07d4 100644 --- a/x-pack/plugins/session_view/server/routes/process_events_route.ts +++ b/x-pack/plugins/session_view/server/routes/process_events_route.ts @@ -57,7 +57,7 @@ export const doSearch = async ( { 'process.start': forward ? 'asc' : 'desc' }, { '@timestamp': forward ? 'asc' : 'desc' }, ], - search_after: cursor ? [cursor] : undefined, + search_after: cursor ? [cursor, cursor] : undefined, }, });