diff --git a/.buildkite/pipelines/pull_request/response_ops.yml b/.buildkite/pipelines/pull_request/response_ops.yml new file mode 100644 index 0000000000000..846477170409b --- /dev/null +++ b/.buildkite/pipelines/pull_request/response_ops.yml @@ -0,0 +1,11 @@ +steps: + - command: .buildkite/scripts/steps/functional/response_ops_cases.sh + label: 'Cases Cypress Tests on Security Solution' + agents: + queue: ci-group-6 + depends_on: build + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: '*' + limit: 1 diff --git a/.buildkite/scripts/pipelines/pull_request/pipeline.js b/.buildkite/scripts/pipelines/pull_request/pipeline.js index 2d3befd00a890..fa167d9f324b4 100644 --- a/.buildkite/scripts/pipelines/pull_request/pipeline.js +++ b/.buildkite/scripts/pipelines/pull_request/pipeline.js @@ -65,7 +65,6 @@ const uploadPipeline = (pipelineContent) => { if ( (await doAnyChangesMatch([ /^x-pack\/plugins\/security_solution/, - /^x-pack\/plugins\/cases/, /^x-pack\/plugins\/lists/, /^x-pack\/plugins\/timelines/, /^x-pack\/test\/security_solution_cypress/, @@ -77,6 +76,13 @@ const uploadPipeline = (pipelineContent) => { pipeline.push(getPipeline('.buildkite/pipelines/pull_request/security_solution.yml')); } + if ( + (await doAnyChangesMatch([/^x-pack\/plugins\/cases/])) || + process.env.GITHUB_PR_LABELS.includes('ci:all-cypress-suites') + ) { + pipeline.push(getPipeline('.buildkite/pipelines/pull_request/response_ops.yml')); + } + if ( (await doAnyChangesMatch([/^x-pack\/plugins\/apm/])) || process.env.GITHUB_PR_LABELS.includes('ci:all-cypress-suites') diff --git a/.buildkite/scripts/steps/functional/response_ops_cases.sh b/.buildkite/scripts/steps/functional/response_ops_cases.sh new file mode 100755 index 0000000000000..13d0ef52130a3 --- /dev/null +++ b/.buildkite/scripts/steps/functional/response_ops_cases.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source .buildkite/scripts/steps/functional/common.sh + +export JOB=kibana-security-solution-chrome + +echo "--- Response Ops Cases Cypress Tests on Security Solution" + +cd "$XPACK_DIR" + +checks-reporter-with-killswitch "Response Ops Cases Cypress Tests on Security Solution" \ + node scripts/functional_tests \ + --debug --bail \ + --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ + --config test/security_solution_cypress/cases_cli_config.ts diff --git a/docs/api/cases.asciidoc b/docs/api/cases.asciidoc index 3b5bfaeceaff4..0033a31fb8905 100644 --- a/docs/api/cases.asciidoc +++ b/docs/api/cases.asciidoc @@ -16,14 +16,15 @@ these APIs: * <> * <> * <> -* {security-guide}/cases-get-connector.html[Get current connector] +* <> * <> * <> * {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] * <> * <> +* <> //ADD include::cases/cases-api-add-comment.asciidoc[leveloffset=+1] @@ -40,8 +41,12 @@ include::cases/cases-api-get-case-activity.asciidoc[leveloffset=+1] include::cases/cases-api-get-case.asciidoc[leveloffset=+1] include::cases/cases-api-get-status.asciidoc[leveloffset=+1] include::cases/cases-api-get-comments.asciidoc[leveloffset=+1] +include::cases/cases-api-get-configuration.asciidoc[leveloffset=+1] include::cases/cases-api-get-reporters.asciidoc[leveloffset=+1] include::cases/cases-api-get-tags.asciidoc[leveloffset=+1] +//SET +include::cases/cases-api-set-configuration.asciidoc[leveloffset=+1] //UPDATE include::cases/cases-api-update.asciidoc[leveloffset=+1] include::cases/cases-api-update-comment.asciidoc[leveloffset=+1] +include::cases/cases-api-update-configuration.asciidoc[leveloffset=+1] diff --git a/docs/api/cases/cases-api-get-configuration.asciidoc b/docs/api/cases/cases-api-get-configuration.asciidoc new file mode 100644 index 0000000000000..778e95949e3f5 --- /dev/null +++ b/docs/api/cases/cases-api-get-configuration.asciidoc @@ -0,0 +1,95 @@ +[[cases-get-configuration]] +== Get case configuration API +++++ +Get configuration +++++ + +Retrieves external connection details, such as the closure type and +default connector for cases. + +=== {api-request-title} + +`GET :/api/cases/configure` + +`GET :/s//api/cases/configure` + +=== {api-prereq-title} + +You must have `read` privileges for the *Cases* feature in the *Management*, +*{observability}*, or *Security* section of the +<>, depending on the +`owner` of the case configuration. + +=== {api-path-parms-title} + +``:: +(Optional, string) An identifier for the space. If it is not specified, the +default space is used. + +=== {api-query-parms-title} + +`owner`:: +(Optional, string or array of strings) A filter to limit the retrieved +details to a specific set of applications. Valid values are: `cases`, +`observability`, and `securitySolution`. If this parameter is omitted, the +response contains information for all applications that the user has access to +read. + +=== Response code + +`200`:: + Indicates a successful call. + +=== Example + +[source,sh] +-------------------------------------------------- +GET api/cases/configure?owner=securitySolution +-------------------------------------------------- +// KIBANA + +The API returns the following type of information: + +[source,json] +-------------------------------------------------- +[ + { + "owner": "securitySolution", + "closure_type": "close-by-user", + "created_at": "2020-03-30T13:31:38.083Z", + "created_by": { + "email": "admin@hms.gov.uk", + "full_name": "Mr Admin", + "username": "admin" + }, + "updated_at": null, + "updated_by": null, + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "my-jira", + "type": ".jira", + "fields": null + }, + "mappings": [ + { + "source": "title", + "target": "summary", + "action_type": "overwrite" + }, + { + "source": "description", + "target": "description", + "action_type": "overwrite" + }, + { + "source": "comments", + "target": "comments", + "action_type": "append" + } + ], + "version": "WzE3NywxXQ==", + "error": null, + "id": "7349772f-421a-4de3-b8bb-2d9b22ccee30" + } +] +-------------------------------------------------- diff --git a/docs/api/cases/cases-api-get-tags.asciidoc b/docs/api/cases/cases-api-get-tags.asciidoc index 426a7e91a0f47..44d2bf9fffd1f 100644 --- a/docs/api/cases/cases-api-get-tags.asciidoc +++ b/docs/api/cases/cases-api-get-tags.asciidoc @@ -4,7 +4,7 @@ Get tags ++++ -Aggregates and returns all unique tags from all cases. +Aggregates and returns a list of case tags. === Request @@ -21,10 +21,6 @@ You must have `read` privileges for the *Cases* feature in the *Management*, === Path parameters -``:: -(Required, string) An identifier for the case to retrieve. Use -<> to retrieve case IDs. - ``:: (Optional, string) An identifier for the space. If it is not specified, the default space is used. @@ -32,9 +28,9 @@ default space is used. === Query parameters `owner`:: -(Optional, string or array of strings) Specifies the set of applications to -limit the retrieved tags. If not specified, the response contains all tags from -cases that the user has access to read. +(Optional, string or array of strings) A filter to limit the retrieved tags to a specific set of applications. +Valid values are: `cases`, `observability`, and `securitySolution`. If this parameter is omitted, the response +contains tags from all cases that the user has access to read. ==== Response code @@ -43,16 +39,13 @@ cases that the user has access to read. ==== Example -Gets all tags for all cases: - [source,sh] -------------------------------------------------- GET api/cases/tags -------------------------------------------------- // KIBANA -The API returns a JSON object with the names and email addresses of users who -opened cases. For example: +The API returns a JSON object with tags from all the cases that the user has access to read. For example: [source,json] -------------------------------------------------- @@ -62,4 +55,4 @@ opened cases. For example: "social engineering", "bubblegum" ] --------------------------------------------------- \ No newline at end of file +-------------------------------------------------- diff --git a/docs/api/cases/cases-api-set-configuration.asciidoc b/docs/api/cases/cases-api-set-configuration.asciidoc new file mode 100644 index 0000000000000..2b0cbefc008ac --- /dev/null +++ b/docs/api/cases/cases-api-set-configuration.asciidoc @@ -0,0 +1,163 @@ +[[cases-api-set-configuration]] +== Set case configuration API +++++ +Set configuration +++++ + +Sets external connection details, such as the closure type and +default connector for cases. + +=== {api-request-title} + +`POST :/api/cases/configure` + +`POST :/s//api/cases/configure` + +=== {api-prereq-title} + +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 configuration. + +=== {api-description-title} + +Connectors are used to interface with external systems. You must create a +connector before you can use it in your cases. Refer to <>. + +If you set a default connector, it is automatically selected when you create +cases in {kib}. If you use the <>, however, +you must still specify all of the connector details. + +=== {api-path-parms-title} + +``:: +(Optional, string) An identifier for the space. If it is not specified, the +default space is used. + +=== {api-request-body-title} + +`closure_type`:: +(Required, string) Specifies whether a case is automatically closed when it is +pushed to external systems. ++ +-- +Valid values are: + +* `close-by-pushing`: Cases are automatically closed when they are pushed. +* `close-by-user`: Cases are not automatically closed. +-- + +`connector`:: +(Required, object) An object that contains the connector configuration. ++ +.Properties of `connector` +[%collapsible%open] +==== +`fields`:: +(Required, object) An object that contains the connector fields. ++ +-- +TIP: The fields specified in the case configuration are not used and are not +propagated to individual cases, therefore it is recommended to set it to `null`. +-- + +`id`:: +(Required, string) The identifier for the connector. If you do not want a +default connector, use `none`. To retrieve connector IDs, use +<>. + +`name`:: +(Required, string) The name of the connector. If you do not want a default +connector, use `none`. To retrieve connector names, use +<>. + +`type`:: +(Required, string) The type of the connector. Valid values are: `.jira`, `.none`, +`.resilient`,`.servicenow`, `.servicenow-sir`, and `.swimlane`. +==== + +`owner`:: +(Required, string) The application that owns the case configuration. Valid +values are: `cases`, `observability`, or `securitySolution`. This value affects +whether you're setting case configuration details for {stack-manage-app}, +{observability}, or {security-app}. + +`settings`:: +(Optional, object) +An object that contains the case settings. ++ +.Properties of `settings` +[%collapsible%open] +==== +`syncAlerts`:: +(Required, boolean) Turns alert syncing on or off. +==== + +=== {api-response-codes-title} + +`200`:: + Indicates a successful call. + +=== {api-example-title} + +Sets the closure type and default connector for cases in **{stack-manage-app}**: + +[source,sh] +-------------------------------------------------- +POST api/cases/configure +{ + "owner": "cases", + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "my-serviceNow", + "type": ".servicenow", + "fields": null, + }, + "closure_type": "close-by-user" +} +-------------------------------------------------- + +The API returns the following response: + +[source,json] +-------------------------------------------------- +{ + "owner": "cases", + "closure_type": "close-by-user", + "created_at": "2022-04-02T01:09:02.303Z", + "created_by": { + "email": "moneypenny@hms.gov.uk", + "full_name": "Ms Moneypenny", + "username": "moneypenny" + }, + "updated_at": null, + "updated_by": null, + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "my-serviceNow", + "type": ".servicenow", + "fields": null, + }, + "mappings": [ + { + "source": "title", + "target": "short_description", + "action_type": "overwrite" + }, + { + "source":"description", + "target":"description", + "action_type":"overwrite" + }, + { + "source":"comments", + "target":"work_notes", + "action_type":"append" + } + ], + "version": "WzE3NywxXQ==", + "error": null, + "id": "7349772f-421a-4de3-b8bb-2d9b22ccee30", +} +-------------------------------------------------- diff --git a/docs/api/cases/cases-api-update-configuration.asciidoc b/docs/api/cases/cases-api-update-configuration.asciidoc new file mode 100644 index 0000000000000..cf7d2ea7d8cfd --- /dev/null +++ b/docs/api/cases/cases-api-update-configuration.asciidoc @@ -0,0 +1,129 @@ +[[cases-api-update-configuration]] +== Update case configuration API +++++ +Update configuration +++++ + +Updates external connection details, such as the closure type and default +connector for cases. + +=== {api-request-title} + +`PATCH :/api/cases/configure/` + +`PATCH :/s//api/cases/configure/` + +=== {api-prereq-title} + +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 configuration. + +=== {api-description-title} + +Connectors are used to interface with external systems. You must create a +connector before you can it in your cases. Refer to <>. + +=== {api-path-parms-title} + +``:: +The identifier for the configuration. To retrieve the configuration IDs, use +<>. + +``:: +(Optional, string) An identifier for the space. If it is not specified, the +default space is used. + +=== Request body + +`closure_type`:: +(Optional, string) Determines whether a case is automatically closed when it is +pushed to external systems. Valid values are: ++ +-- +* `close-by-pushing`: Cases are automatically closed when they +are pushed. +* `close-by-user`: Cases are not automatically closed. +-- + +`connector`:: +(Optional, object) An object that contains the connector configuration. ++ +.Properties of `connector` +[%collapsible%open] +==== +`fields`:: +(Required, object) An object that contains the connector fields. ++ +-- +TIP: The fields specified in the case configuration are not used and are not +propagated to individual cases, therefore it is recommended to set it to `null`. +-- + +`id`:: +(Required, string) The identifier for the connector. To retrieve connector IDs, +use <>. + +`name`:: +(Required, string) The name of the connector. + +`type`:: +(Required, string) The type of the connector. Valid values are: `.servicenow`, +`.servicenow-sir`, `.jira`, `.resilient`, `.swimlane`, and `.none`. +==== + +`version`:: +(Required, string) The version of the connector. To retrieve the version value, +use <>. + +=== Response code + +`200`:: + Indicates a successful call. + +=== Example + +Change the closure type configuration option: + +[source,sh] +-------------------------------------------------- +PATCH api/cases/configure/3297a0f0-b5ec-11ec-b141-0fdb20a7f9a9 +{ + "closure_type": "close-by-pushing", + "version": "WzIwMiwxXQ==" +} +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,json] +-------------------------------------------------- +{ + "closure_type": "close-by-user", + "owner": "cases", + "created_at": "2022-04-06T20:57:40.746Z", + "created_by": { + "email": "admin@hms.gov.uk", + "full_name": "Ms Admin", + "username": "admin" + }, + "updated_at": "2022-04-12T22:41:09.262Z", + "updated_by": { + "email": "admin@hms.gov.uk", + "full_name": "Ms Admin", + "username": "admin" + }, + "connector": { + "id": "none", + "name": "none", + "type": ".none", + "fields": null + }, + "mappings": [], + "version": "WzkwNiw1XQ==", + "error": null, + "id": "3297a0f0-b5ec-11ec-b141-0fdb20a7f9a9" +} +-------------------------------------------------- diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index 513671dd51238..b062dcc3349ad 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -186,6 +186,8 @@ For a time shift example, refer to <>. [[add-annotations]] ==== Add annotations +preview::[] + Annotations allow you to call out specific points in your visualizations that are important, such as a major change in the data. You can add text and icons to annotations and customize the appearance, such as the line format and color. [role="screenshot"] diff --git a/packages/elastic-analytics/BUILD.bazel b/packages/elastic-analytics/BUILD.bazel index a73c908c7ea52..dcf9d33772542 100644 --- a/packages/elastic-analytics/BUILD.bazel +++ b/packages/elastic-analytics/BUILD.bazel @@ -36,6 +36,7 @@ NPM_MODULE_EXTRA_FILES = [ # "@npm//name-of-package" # eg. "@npm//lodash" RUNTIME_DEPS = [ + "@npm//moment", "@npm//rxjs", ] @@ -51,6 +52,7 @@ RUNTIME_DEPS = [ TYPES_DEPS = [ "@npm//@types/node", "@npm//@types/jest", + "@npm//moment", "@npm//rxjs", "//packages/kbn-logging:npm_module_types", "//packages/kbn-logging-mocks:npm_module_types", diff --git a/packages/elastic-analytics/src/analytics_client/analytics_client.test.ts b/packages/elastic-analytics/src/analytics_client/analytics_client.test.ts index 63e8e91e8a5ff..08515ecae8d1e 100644 --- a/packages/elastic-analytics/src/analytics_client/analytics_client.test.ts +++ b/packages/elastic-analytics/src/analytics_client/analytics_client.test.ts @@ -8,7 +8,7 @@ // eslint-disable-next-line max-classes-per-file import type { Observable } from 'rxjs'; -import { BehaviorSubject, lastValueFrom, Subject } from 'rxjs'; +import { BehaviorSubject, firstValueFrom, lastValueFrom, Subject } from 'rxjs'; import type { MockedLogger } from '@kbn/logging-mocks'; import { loggerMock } from '@kbn/logging-mocks'; import { AnalyticsClient } from './analytics_client'; @@ -528,6 +528,44 @@ describe('AnalyticsClient', () => { ]); }); + test('The undefined values are not forwarded to the global context', async () => { + const context$ = new Subject<{ a_field?: boolean; b_field: number }>(); + analyticsClient.registerContextProvider({ + name: 'contextProviderA', + schema: { + a_field: { + type: 'boolean', + _meta: { + description: 'a_field description', + optional: true, + }, + }, + b_field: { + type: 'long', + _meta: { + description: 'b_field description', + }, + }, + }, + context$, + }); + + const globalContextPromise = firstValueFrom(globalContext$.pipe(take(6), toArray())); + context$.next({ b_field: 1 }); + context$.next({ a_field: false, b_field: 1 }); + context$.next({ a_field: true, b_field: 1 }); + context$.next({ b_field: 1 }); + context$.next({ a_field: undefined, b_field: 2 }); + await expect(globalContextPromise).resolves.toEqual([ + {}, // Original empty state + { b_field: 1 }, + { a_field: false, b_field: 1 }, + { a_field: true, b_field: 1 }, + { b_field: 1 }, // a_field is removed because the context provider removed it. + { b_field: 2 }, // a_field is not forwarded because it is `undefined` + ]); + }); + test('Fails to register 2 context providers with the same name', () => { analyticsClient.registerContextProvider({ name: 'contextProviderA', diff --git a/packages/elastic-analytics/src/analytics_client/context_service.ts b/packages/elastic-analytics/src/analytics_client/context_service.ts index cee3e56b389d1..7c3f3c8327eb7 100644 --- a/packages/elastic-analytics/src/analytics_client/context_service.ts +++ b/packages/elastic-analytics/src/analytics_client/context_service.ts @@ -69,9 +69,21 @@ export class ContextService { [...this.contextProvidersRegistry.values()].reduce((acc, context) => { return { ...acc, - ...context, + ...this.removeEmptyValues(context), }; }, {} as Partial) ); } + + private removeEmptyValues(context?: Partial) { + if (!context) { + return {}; + } + return Object.keys(context).reduce((acc, key) => { + if (context[key] !== undefined) { + acc[key] = context[key]; + } + return acc; + }, {} as Partial); + } } diff --git a/packages/elastic-analytics/src/events/types.ts b/packages/elastic-analytics/src/events/types.ts index 8523b7791150a..5f1f587d3b284 100644 --- a/packages/elastic-analytics/src/events/types.ts +++ b/packages/elastic-analytics/src/events/types.ts @@ -8,7 +8,34 @@ import type { ShipperName } from '../analytics_client'; +/** + * Definition of the context that can be appended to the events through the {@link IAnalyticsClient.registerContextProvider}. + */ export interface EventContext { + /** + * The unique user ID. + */ + userId?: string; + /** + * The user's organization ID. + */ + esOrgId?: string; + /** + * The product's version. + */ + version?: string; + /** + * The name of the current page. + */ + pageName?: string; + /** + * The current application ID. + */ + applicationId?: string; + /** + * The current entity ID (dashboard ID, visualization ID, etc.). + */ + entityId?: string; // TODO: Extend with known keys [key: string]: unknown; } diff --git a/packages/elastic-analytics/src/index.ts b/packages/elastic-analytics/src/index.ts index 382974783aeb1..c22ea702c5be8 100644 --- a/packages/elastic-analytics/src/index.ts +++ b/packages/elastic-analytics/src/index.ts @@ -36,8 +36,10 @@ export type { // Types for the registerEventType API EventTypeOpts, } from './analytics_client'; + export type { Event, EventContext, EventType, TelemetryCounter } from './events'; export { TelemetryCounterType } from './events'; + export type { RootSchema, SchemaObject, @@ -52,4 +54,6 @@ export type { AllowedSchemaStringTypes, AllowedSchemaTypes, } from './schema'; -export type { IShipper } from './shippers'; + +export type { IShipper, FullStorySnippetConfig, FullStoryShipperConfig } from './shippers'; +export { FullStoryShipper } from './shippers'; diff --git a/packages/elastic-analytics/src/shippers/fullstory/format_payload.test.ts b/packages/elastic-analytics/src/shippers/fullstory/format_payload.test.ts new file mode 100644 index 0000000000000..6f9ad05bc9da7 --- /dev/null +++ b/packages/elastic-analytics/src/shippers/fullstory/format_payload.test.ts @@ -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 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 { formatPayload } from './format_payload'; + +describe('formatPayload', () => { + test('appends `_str` to string values', () => { + const payload = { + foo: 'bar', + baz: ['qux'], + }; + + expect(formatPayload(payload)).toEqual({ + foo_str: payload.foo, + baz_strs: payload.baz, + }); + }); + + test('appends `_int` to integer values', () => { + const payload = { + foo: 1, + baz: [100000], + }; + + expect(formatPayload(payload)).toEqual({ + foo_int: payload.foo, + baz_ints: payload.baz, + }); + }); + + test('appends `_real` to integer values', () => { + const payload = { + foo: 1.5, + baz: [100000.5], + }; + + expect(formatPayload(payload)).toEqual({ + foo_real: payload.foo, + baz_reals: payload.baz, + }); + }); + + test('appends `_bool` to booleans values', () => { + const payload = { + foo: true, + baz: [false], + }; + + expect(formatPayload(payload)).toEqual({ + foo_bool: payload.foo, + baz_bools: payload.baz, + }); + }); + + test('appends `_date` to Date values', () => { + const payload = { + foo: new Date(), + baz: [new Date()], + }; + + expect(formatPayload(payload)).toEqual({ + foo_date: payload.foo, + baz_dates: payload.baz, + }); + }); + + test('supports nested values', () => { + const payload = { + nested: { + foo: 'bar', + baz: ['qux'], + }, + }; + + expect(formatPayload(payload)).toEqual({ + nested: { + foo_str: payload.nested.foo, + baz_strs: payload.nested.baz, + }, + }); + }); + + test('does not mutate reserved keys', () => { + const payload = { + uid: 'uid', + displayName: 'displayName', + email: 'email', + acctId: 'acctId', + website: 'website', + pageName: 'pageName', + }; + + expect(formatPayload(payload)).toEqual(payload); + }); + + test('removes undefined values', () => { + const payload = { + foo: undefined, + baz: [undefined], + }; + + expect(formatPayload(payload)).toEqual({}); + }); + + test('throws if null is provided', () => { + const payload = { + foo: null, + baz: [null], + }; + + expect(() => formatPayload(payload)).toThrowErrorMatchingInlineSnapshot( + `"Unsupported type: object"` + ); + }); + + describe('String to Date identification', () => { + test('appends `_date` to ISO string values', () => { + const payload = { + foo: new Date().toISOString(), + baz: [new Date().toISOString()], + }; + + expect(formatPayload(payload)).toEqual({ + foo_date: payload.foo, + baz_dates: payload.baz, + }); + }); + + test('appends `_str` to random string values', () => { + const payload = { + foo: 'test-1', + baz: ['test-1'], + }; + + expect(formatPayload(payload)).toEqual({ + foo_str: payload.foo, + baz_strs: payload.baz, + }); + }); + }); +}); diff --git a/packages/elastic-analytics/src/shippers/fullstory/format_payload.ts b/packages/elastic-analytics/src/shippers/fullstory/format_payload.ts new file mode 100644 index 0000000000000..c55ed2409da50 --- /dev/null +++ b/packages/elastic-analytics/src/shippers/fullstory/format_payload.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 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 moment from 'moment'; + +// https://help.fullstory.com/hc/en-us/articles/360020623234#reserved-properties +const FULLSTORY_RESERVED_PROPERTIES = [ + 'uid', + 'displayName', + 'email', + 'acctId', + 'website', + // https://developer.fullstory.com/page-variables + 'pageName', +]; + +export function formatPayload(context: Record): Record { + // format context keys as required for env vars, see docs: https://help.fullstory.com/hc/en-us/articles/360020623234 + return Object.fromEntries( + Object.entries(context) + // Discard any undefined values + .map<[string, unknown]>(([key, value]) => { + return Array.isArray(value) + ? [key, value.filter((v) => typeof v !== 'undefined')] + : [key, value]; + }) + .filter( + ([, value]) => typeof value !== 'undefined' && (!Array.isArray(value) || value.length > 0) + ) + // Transform key names according to the FullStory needs + .map(([key, value]) => { + if (FULLSTORY_RESERVED_PROPERTIES.includes(key)) { + return [key, value]; + } + if (isRecord(value)) { + return [key, formatPayload(value)]; + } + const valueType = getFullStoryType(value); + const formattedKey = valueType ? `${key}_${valueType}` : key; + return [formattedKey, value]; + }) + ); +} + +function getFullStoryType(value: unknown) { + // For arrays, make the decision based on the first element + const isArray = Array.isArray(value); + const v = isArray ? value[0] : value; + let type: string; + switch (typeof v) { + case 'string': + type = moment(v, moment.ISO_8601, true).isValid() ? 'date' : 'str'; + break; + case 'number': + type = Number.isInteger(v) ? 'int' : 'real'; + break; + case 'boolean': + type = 'bool'; + break; + case 'object': + if (isDate(v)) { + type = 'date'; + break; + } + default: + throw new Error(`Unsupported type: ${typeof v}`); + } + + // convert to plural form for arrays + return isArray ? `${type}s` : type; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) && !isDate(value); +} + +function isDate(value: unknown): value is Date { + return value instanceof Date; +} diff --git a/packages/elastic-analytics/src/shippers/fullstory/fullstory_shipper.test.mocks.ts b/packages/elastic-analytics/src/shippers/fullstory/fullstory_shipper.test.mocks.ts new file mode 100644 index 0000000000000..fadd1ffee2ae0 --- /dev/null +++ b/packages/elastic-analytics/src/shippers/fullstory/fullstory_shipper.test.mocks.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { FullStoryApi } from './types'; + +export const fullStoryApiMock: jest.Mocked = { + identify: jest.fn(), + setUserVars: jest.fn(), + setVars: jest.fn(), + consent: jest.fn(), + restart: jest.fn(), + shutdown: jest.fn(), + event: jest.fn(), +}; +jest.doMock('./load_snippet', () => { + return { + loadSnippet: () => fullStoryApiMock, + }; +}); diff --git a/packages/elastic-analytics/src/shippers/fullstory/fullstory_shipper.test.ts b/packages/elastic-analytics/src/shippers/fullstory/fullstory_shipper.test.ts new file mode 100644 index 0000000000000..67797a629c828 --- /dev/null +++ b/packages/elastic-analytics/src/shippers/fullstory/fullstory_shipper.test.ts @@ -0,0 +1,136 @@ +/* + * 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 { loggerMock } from '@kbn/logging-mocks'; +import { fullStoryApiMock } from './fullstory_shipper.test.mocks'; +import { FullStoryShipper } from './fullstory_shipper'; + +describe('FullStoryShipper', () => { + let fullstoryShipper: FullStoryShipper; + + beforeEach(() => { + jest.resetAllMocks(); + fullstoryShipper = new FullStoryShipper( + { + debug: true, + fullStoryOrgId: 'test-org-id', + }, + { + logger: loggerMock.create(), + sendTo: 'staging', + isDev: true, + } + ); + }); + + describe('extendContext', () => { + describe('FS.identify', () => { + test('calls `identify` when the userId is provided', () => { + const userId = 'test-user-id'; + fullstoryShipper.extendContext({ userId }); + expect(fullStoryApiMock.identify).toHaveBeenCalledWith(userId); + }); + + test('calls `identify` again only if the userId changes', () => { + const userId = 'test-user-id'; + fullstoryShipper.extendContext({ userId }); + expect(fullStoryApiMock.identify).toHaveBeenCalledTimes(1); + expect(fullStoryApiMock.identify).toHaveBeenCalledWith(userId); + + fullstoryShipper.extendContext({ userId }); + expect(fullStoryApiMock.identify).toHaveBeenCalledTimes(1); // still only called once + + fullstoryShipper.extendContext({ userId: `${userId}-1` }); + expect(fullStoryApiMock.identify).toHaveBeenCalledTimes(2); // called again because the user changed + expect(fullStoryApiMock.identify).toHaveBeenCalledWith(`${userId}-1`); + }); + }); + + describe('FS.setUserVars', () => { + test('calls `setUserVars` when version is provided', () => { + fullstoryShipper.extendContext({ version: '1.2.3' }); + expect(fullStoryApiMock.setUserVars).toHaveBeenCalledWith({ + version_str: '1.2.3', + version_major_int: 1, + version_minor_int: 2, + version_patch_int: 3, + }); + }); + + test('calls `setUserVars` when esOrgId is provided', () => { + fullstoryShipper.extendContext({ esOrgId: 'test-es-org-id' }); + expect(fullStoryApiMock.setUserVars).toHaveBeenCalledWith({ org_id_str: 'test-es-org-id' }); + }); + + test('merges both: version and esOrgId if both are provided', () => { + fullstoryShipper.extendContext({ version: '1.2.3', esOrgId: 'test-es-org-id' }); + expect(fullStoryApiMock.setUserVars).toHaveBeenCalledWith({ + org_id_str: 'test-es-org-id', + version_str: '1.2.3', + version_major_int: 1, + version_minor_int: 2, + version_patch_int: 3, + }); + }); + }); + + describe('FS.setVars', () => { + test('adds the rest of the context to `setVars`', () => { + const context = { + userId: 'test-user-id', + version: '1.2.3', + esOrgId: 'test-es-org-id', + foo: 'bar', + }; + fullstoryShipper.extendContext(context); + expect(fullStoryApiMock.setVars).toHaveBeenCalledWith('page', { foo_str: 'bar' }); + }); + }); + }); + + describe('optIn', () => { + test('should call consent true and restart when isOptIn: true', () => { + fullstoryShipper.optIn(true); + expect(fullStoryApiMock.consent).toHaveBeenCalledWith(true); + expect(fullStoryApiMock.restart).toHaveBeenCalled(); + }); + + test('should call consent false and shutdown when isOptIn: false', () => { + fullstoryShipper.optIn(false); + expect(fullStoryApiMock.consent).toHaveBeenCalledWith(false); + expect(fullStoryApiMock.shutdown).toHaveBeenCalled(); + }); + }); + + describe('reportEvents', () => { + test('calls the API once per event in the array with the properties transformed', () => { + fullstoryShipper.reportEvents([ + { + event_type: 'test-event-1', + timestamp: '2020-01-01T00:00:00.000Z', + properties: { test: 'test-1' }, + context: { pageName: 'test-page-1' }, + }, + { + event_type: 'test-event-2', + timestamp: '2020-01-01T00:00:00.000Z', + properties: { test: 'test-2' }, + context: { pageName: 'test-page-1' }, + }, + ]); + + expect(fullStoryApiMock.event).toHaveBeenCalledTimes(2); + expect(fullStoryApiMock.event).toHaveBeenCalledWith('test-event-1', { + test_str: 'test-1', + }); + expect(fullStoryApiMock.event).toHaveBeenCalledWith('test-event-2', { + test_str: 'test-2', + }); + }); + }); +}); diff --git a/packages/elastic-analytics/src/shippers/fullstory/fullstory_shipper.ts b/packages/elastic-analytics/src/shippers/fullstory/fullstory_shipper.ts new file mode 100644 index 0000000000000..ff953393b9ddb --- /dev/null +++ b/packages/elastic-analytics/src/shippers/fullstory/fullstory_shipper.ts @@ -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 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 { IShipper } from '../types'; +import type { AnalyticsClientInitContext } from '../../analytics_client'; +import type { EventContext, Event } from '../../events'; +import type { FullStoryApi } from './types'; +import type { FullStorySnippetConfig } from './load_snippet'; +import { getParsedVersion } from './get_parsed_version'; +import { formatPayload } from './format_payload'; +import { loadSnippet } from './load_snippet'; + +export type FullStoryShipperConfig = FullStorySnippetConfig; + +export class FullStoryShipper implements IShipper { + public static shipperName = 'FullStory'; + private readonly fullStoryApi: FullStoryApi; + private lastUserId: string | undefined; + + constructor( + config: FullStoryShipperConfig, + private readonly initContext: AnalyticsClientInitContext + ) { + this.fullStoryApi = loadSnippet(config); + } + + public extendContext(newContext: EventContext): void { + this.initContext.logger.debug(`Received context ${JSON.stringify(newContext)}`); + + // FullStory requires different APIs for different type of contexts. + const { userId, version, esOrgId, ...nonUserContext } = newContext; + + // Call it only when the userId changes + if (userId && userId !== this.lastUserId) { + this.initContext.logger.debug(`Calling FS.identify with userId ${userId}`); + // We need to call the API for every new userId (restarting the session). + this.fullStoryApi.identify(userId); + this.lastUserId = userId; + } + + // User-level context + if (version || esOrgId) { + this.initContext.logger.debug( + `Calling FS.setUserVars with version ${version} and esOrgId ${esOrgId}` + ); + this.fullStoryApi.setUserVars({ + ...(version ? getParsedVersion(version) : {}), + ...(esOrgId ? { org_id_str: esOrgId } : {}), + }); + } + + // Event-level context. At the moment, only the scope `page` is supported by FullStory for webapps. + if (Object.keys(nonUserContext).length) { + // Keeping these fields for backwards compatibility. + if (nonUserContext.applicationId) nonUserContext.app_id = nonUserContext.applicationId; + if (nonUserContext.entityId) nonUserContext.ent_id = nonUserContext.entityId; + + this.initContext.logger.debug( + `Calling FS.setVars with context ${JSON.stringify(nonUserContext)}` + ); + this.fullStoryApi.setVars('page', formatPayload(nonUserContext)); + } + } + + public optIn(isOptedIn: boolean): void { + this.initContext.logger.debug(`Setting FS to optIn ${isOptedIn}`); + // FullStory uses 2 different opt-in methods: + // - `consent` is needed to allow collecting information about the components + // declared as "Record with user consent" (https://help.fullstory.com/hc/en-us/articles/360020623574). + // We need to explicitly call `consent` if for the "Record with user content" feature to work. + this.fullStoryApi.consent(isOptedIn); + // - `restart` and `shutdown` fully start/stop the collection of data. + if (isOptedIn) { + this.fullStoryApi.restart(); + } else { + this.fullStoryApi.shutdown(); + } + } + + public reportEvents(events: Event[]): void { + this.initContext.logger.debug(`Reporting ${events.length} events to FS`); + events.forEach((event) => { + // We only read event.properties and discard the rest because the context is already sent in the other APIs. + this.fullStoryApi.event(event.event_type, formatPayload(event.properties)); + }); + } +} diff --git a/packages/elastic-analytics/src/shippers/fullstory/get_parsed_version.test.ts b/packages/elastic-analytics/src/shippers/fullstory/get_parsed_version.test.ts new file mode 100644 index 0000000000000..b4938dbca3bc4 --- /dev/null +++ b/packages/elastic-analytics/src/shippers/fullstory/get_parsed_version.test.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { getParsedVersion } from './get_parsed_version'; + +describe('getParsedVersion', () => { + test('parses a version string', () => { + expect(getParsedVersion('1.2.3')).toEqual({ + version_str: '1.2.3', + version_major_int: 1, + version_minor_int: 2, + version_patch_int: 3, + }); + }); + + test('parses a version string with extra label', () => { + expect(getParsedVersion('1.2.3-SNAPSHOT')).toEqual({ + version_str: '1.2.3-SNAPSHOT', + version_major_int: 1, + version_minor_int: 2, + version_patch_int: 3, + }); + }); + + test('does not throw for invalid version', () => { + expect(getParsedVersion('INVALID_VERSION')).toEqual({ + version_str: 'INVALID_VERSION', + version_major_int: NaN, + version_minor_int: NaN, + version_patch_int: NaN, + }); + }); +}); diff --git a/packages/elastic-analytics/src/shippers/fullstory/get_parsed_version.ts b/packages/elastic-analytics/src/shippers/fullstory/get_parsed_version.ts new file mode 100644 index 0000000000000..873b47a0cde8a --- /dev/null +++ b/packages/elastic-analytics/src/shippers/fullstory/get_parsed_version.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 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 function getParsedVersion(version: string): { + version_str: string; + version_major_int: number; + version_minor_int: number; + version_patch_int: number; +} { + const [major, minor, patch] = version.split('.'); + return { + version_str: version, + version_major_int: parseInt(major, 10), + version_minor_int: parseInt(minor, 10), + version_patch_int: parseInt(patch, 10), + }; +} diff --git a/test/functional_ccs/ftr_provider_context.ts b/packages/elastic-analytics/src/shippers/fullstory/index.ts similarity index 58% rename from test/functional_ccs/ftr_provider_context.ts rename to packages/elastic-analytics/src/shippers/fullstory/index.ts index 8fa82b46ac406..a9be91c82e9ec 100644 --- a/test/functional_ccs/ftr_provider_context.ts +++ b/packages/elastic-analytics/src/shippers/fullstory/index.ts @@ -6,8 +6,6 @@ * Side Public License, v 1. */ -import { GenericFtrProviderContext } from '@kbn/test'; -import { services } from './services'; -import { pageObjects } from '../functional/page_objects'; - -export type FtrProviderContext = GenericFtrProviderContext; +export { FullStoryShipper } from './fullstory_shipper'; +export type { FullStoryShipperConfig } from './fullstory_shipper'; +export type { FullStorySnippetConfig } from './load_snippet'; diff --git a/packages/elastic-analytics/src/shippers/fullstory/load_snippet.test.ts b/packages/elastic-analytics/src/shippers/fullstory/load_snippet.test.ts new file mode 100644 index 0000000000000..0b920ef2d22c1 --- /dev/null +++ b/packages/elastic-analytics/src/shippers/fullstory/load_snippet.test.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 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 { loadSnippet } from './load_snippet'; + +describe('loadSnippet', () => { + beforeAll(() => { + // Define necessary window and document global variables for the tests + Object.defineProperty(global, 'window', { + writable: true, + value: {}, + }); + + Object.defineProperty(global, 'document', { + writable: true, + value: { + createElement: jest.fn().mockReturnValue({}), + getElementsByTagName: jest + .fn() + .mockReturnValue([{ parentNode: { insertBefore: jest.fn() } }]), + }, + }); + + Object.defineProperty(global, '_fs_script', { + writable: true, + value: '', + }); + }); + + it('should return the FullStory API', () => { + const fullStoryApi = loadSnippet({ debug: true, fullStoryOrgId: 'foo' }); + expect(fullStoryApi).toBeDefined(); + expect(fullStoryApi.event).toBeDefined(); + expect(fullStoryApi.consent).toBeDefined(); + expect(fullStoryApi.restart).toBeDefined(); + expect(fullStoryApi.shutdown).toBeDefined(); + expect(fullStoryApi.identify).toBeDefined(); + expect(fullStoryApi.setUserVars).toBeDefined(); + expect(fullStoryApi.setVars).toBeDefined(); + }); +}); diff --git a/packages/elastic-analytics/src/shippers/fullstory/load_snippet.ts b/packages/elastic-analytics/src/shippers/fullstory/load_snippet.ts new file mode 100644 index 0000000000000..471152f033b5a --- /dev/null +++ b/packages/elastic-analytics/src/shippers/fullstory/load_snippet.ts @@ -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 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 { FullStoryApi } from './types'; + +export interface FullStorySnippetConfig { + /** + * The FullStory account id. + */ + fullStoryOrgId: string; + /** + * The host to send the data to. Used to overcome AdBlockers by using custom DNSs. + * If not specified, it defaults to `fullstory.com`. + */ + host?: string; + /** + * The URL to load the FullStory client from. Falls back to `edge.fullstory.com/s/fs.js` if not specified. + */ + scriptUrl?: string; + /** + * Whether the debug logs should be printed to the console. + */ + debug?: boolean; + /** + * The name of the variable where the API is stored: `window[namespace]`. Defaults to `FS`. + */ + namespace?: string; +} + +export function loadSnippet({ + scriptUrl = 'edge.fullstory.com/s/fs.js', + fullStoryOrgId, + host = 'fullstory.com', + namespace = 'FS', + debug = false, +}: FullStorySnippetConfig): FullStoryApi { + window._fs_debug = debug; + window._fs_host = host; + window._fs_script = scriptUrl; + window._fs_org = fullStoryOrgId; + window._fs_namespace = namespace; + + /* eslint-disable */ + (function(m,n,e,t,l,o,g,y){ + if (e in m) {if(m.console && m.console.log) { m.console.log('FullStory namespace conflict. Please set window["_fs_namespace"].');} return;} + // @ts-expect-error + g=m[e]=function(a,b,s){g.q?g.q.push([a,b,s]):g._api(a,b,s);};g.q=[]; + // @ts-expect-error + o=n.createElement(t);o.async=1;o.crossOrigin='anonymous';o.src=_fs_script; + // @ts-expect-error + y=n.getElementsByTagName(t)[0];y.parentNode.insertBefore(o,y); + // @ts-expect-error + g.identify=function(i,v,s){g(l,{uid:i},s);if(v)g(l,v,s)};g.setUserVars=function(v,s){g(l,v,s)};g.event=function(i,v,s){g('event',{n:i,p:v},s)}; + // @ts-expect-error + g.anonymize=function(){g.identify(!!0)}; + // @ts-expect-error + g.shutdown=function(){g("rec",!1)};g.restart=function(){g("rec",!0)}; + // @ts-expect-error + g.log = function(a,b){g("log",[a,b])}; + // @ts-expect-error + g.consent=function(a){g("consent",!arguments.length||a)}; + // @ts-expect-error + g.identifyAccount=function(i,v){o='account';v=v||{};v.acctId=i;g(o,v)}; + // @ts-expect-error + g.clearUserCookie=function(){}; + // @ts-expect-error + g.setVars=function(n, p){g('setVars',[n,p]);}; + // @ts-expect-error + g._w={};y='XMLHttpRequest';g._w[y]=m[y];y='fetch';g._w[y]=m[y]; + // @ts-expect-error + if(m[y])m[y]=function(){return g._w[y].apply(this,arguments)}; + // @ts-expect-error + g._v="1.3.0"; + + })(window,document,window['_fs_namespace'],'script','user'); + + const fullStoryApi = window[namespace as 'FS']; + + if (!fullStoryApi) { + throw new Error('FullStory snippet failed to load. Check browser logs for more information.'); + } + + return fullStoryApi; +} diff --git a/packages/elastic-analytics/src/shippers/fullstory/types.ts b/packages/elastic-analytics/src/shippers/fullstory/types.ts new file mode 100644 index 0000000000000..6c448c2c4d2e1 --- /dev/null +++ b/packages/elastic-analytics/src/shippers/fullstory/types.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +/** + * Definition of the FullStory API. + * Docs are available at https://developer.fullstory.com/. + */ +export interface FullStoryApi { + /** + * Identify a User + * https://developer.fullstory.com/identify + * @param userId + * @param userVars + */ + identify(userId: string, userVars?: Record): void; + + /** + * Set User Variables + * https://developer.fullstory.com/user-variables + * @param userVars + */ + setUserVars(userVars: Record): void; + + /** + * Setting page variables + * https://developer.fullstory.com/page-variables + * @param scope + * @param pageProperties + */ + setVars(scope: 'page', pageProperties: Record): void; + + /** + * Sending custom event data into FullStory + * https://developer.fullstory.com/custom-events + * @param eventName + * @param eventProperties + */ + event(eventName: string, eventProperties: Record): void; + + /** + * Selectively record parts of your site based on explicit user consent + * https://developer.fullstory.com/consent + * @param isOptedIn true if the user has opted in to tracking + */ + consent(isOptedIn: boolean): void; + + /** + * Restart session recording after it has been shutdown + * https://developer.fullstory.com/restart-recording + */ + restart(): void; + + /** + * Stop recording a session + * https://developer.fullstory.com/stop-recording + */ + shutdown(): void; +} + +declare global { + interface Window { + _fs_debug: boolean; + _fs_host: string; + _fs_org: string; + _fs_namespace: string; + _fs_script: string; + FS: FullStoryApi; + } +} diff --git a/packages/elastic-analytics/src/shippers/index.ts b/packages/elastic-analytics/src/shippers/index.ts index 7a4ab7d85b9f2..c75b38b63a499 100644 --- a/packages/elastic-analytics/src/shippers/index.ts +++ b/packages/elastic-analytics/src/shippers/index.ts @@ -7,3 +7,6 @@ */ export type { IShipper } from './types'; + +export { FullStoryShipper } from './fullstory'; +export type { FullStorySnippetConfig, FullStoryShipperConfig } from './fullstory'; diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index c52407a864c31..80760520da27f 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -333,6 +333,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { securitySolution: { trustedApps: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/trusted-apps-ov.html`, eventFilters: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/event-filters.html`, + blocklist: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/blocklist.html`, }, query: { eql: `${ELASTICSEARCH_DOCS}eql.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index f017c2ec4be00..57a48219b973c 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -238,6 +238,7 @@ export interface DocLinks { readonly securitySolution: { readonly trustedApps: string; readonly eventFilters: string; + readonly blocklist: string; }; readonly query: { readonly eql: string; diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index b185bcb0ea5d0..1e963660a1e03 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -8,8 +8,8 @@ require('../src/setup_node_env'); require('@kbn/test').runTestsCli([ + require.resolve('../test/functional/config.ccs.ts'), require.resolve('../test/functional/config.js'), - require.resolve('../test/functional_ccs/config.ts'), require.resolve('../test/plugin_functional/config.ts'), require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'), require.resolve('../test/new_visualize_flow/config.ts'), diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx index e8ab85f2877ff..bd358ab005fd5 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx +++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx @@ -127,12 +127,8 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { useEffect(() => { const legendShow = showLegendDefault(); - const showLegendDef = shouldShowLegend(visType, visParams.legendDisplay, bucketColumns); - if (showLegendDef !== legendShow) { - setShowLegend(legendShow); - props.uiState?.set('vis.legendOpen', legendShow); - } - }, [showLegendDefault, props.uiState, visParams.legendDisplay, visType, bucketColumns]); + setShowLegend(legendShow); + }, [showLegendDefault]); const onRenderChange = useCallback( (isRendered) => { diff --git a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap index 828a62c85cce3..1725cea040f56 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap @@ -5,7 +5,7 @@ exports[`XYChart component annotations should render basic annotation 1`] = ` dataValues={ Array [ Object { - "dataValue": 1647591917125, + "dataValue": 1647591917140, "details": "Annotation", "header": "2022-03-18T08:25:17.140Z", }, @@ -169,7 +169,7 @@ exports[`XYChart component annotations should render simplified annotation when dataValues={ Array [ Object { - "dataValue": 1647591917125, + "dataValue": 1647591917140, "details": "Annotation", "header": "2022-03-18T08:25:17.140Z", }, diff --git a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx index e8b99a36664df..04c9e7108ab44 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx @@ -170,7 +170,12 @@ export const Annotations = ({ const header = formatter?.convert(isGrouped ? roundedTimestamp : exactTimestamp) || moment(isGrouped ? roundedTimestamp : exactTimestamp).toISOString(); - const strokeWidth = annotation.lineWidth || 1; + const strokeWidth = hide ? 1 : annotation.lineWidth || 1; + const dataValue = isGrouped + ? moment( + isBarChart && minInterval ? roundedTimestamp + minInterval / 2 : roundedTimestamp + ).valueOf() + : moment(exactTimestamp).valueOf(); return ( +
Ctrl/Cmd + L
+
+ +
Esc
void; refreshAutocompleteSettings: (selectedSettings: DevToolsSettings['autocomplete']) => void; settings: DevToolsSettings; + editorInstance: SenseEditor | null; } export function DevToolsSettingsModal(props: Props) { @@ -74,7 +77,10 @@ export function DevToolsSettingsModal(props: Props) { const [polling, setPolling] = useState(props.settings.polling); const [pollInterval, setPollInterval] = useState(props.settings.pollInterval); const [tripleQuotes, setTripleQuotes] = useState(props.settings.tripleQuotes); - const [historyDisabled, setHistoryDisabled] = useState(props.settings.historyDisabled); + const [isHistoryDisabled, setIsHistoryDisabled] = useState(props.settings.isHistoryDisabled); + const [isKeyboardShortcutsDisabled, setIsKeyboardShortcutsDisabled] = useState( + props.settings.isKeyboardShortcutsDisabled + ); const autoCompleteCheckboxes = [ { @@ -134,7 +140,8 @@ export function DevToolsSettingsModal(props: Props) { polling, pollInterval, tripleQuotes, - historyDisabled, + isHistoryDisabled, + isKeyboardShortcutsDisabled, }); } @@ -145,6 +152,21 @@ export function DevToolsSettingsModal(props: Props) { setPollInterval(sanitizedValue); }, []); + const toggleKeyboardShortcuts = useCallback( + (isDisabled: boolean) => { + if (props.editorInstance) { + unregisterCommands(props.editorInstance); + setIsKeyboardShortcutsDisabled(isDisabled); + } + }, + [props.editorInstance] + ); + + const toggleSavingToHistory = useCallback( + (isDisabled: boolean) => setIsHistoryDisabled(isDisabled), + [] + ); + // It only makes sense to show polling options if the user needs to fetch any data. const pollingFields = fields || indices || templates ? ( @@ -160,7 +182,7 @@ export function DevToolsSettingsModal(props: Props) { } > @@ -267,15 +289,34 @@ export function DevToolsSettingsModal(props: Props) { } > } - onChange={(e) => setHistoryDisabled(e.target.checked)} + onChange={(e) => toggleSavingToHistory(e.target.checked)} + /> + + + + } + > + + } + onChange={(e) => toggleKeyboardShortcuts(e.target.checked)} /> diff --git a/src/plugins/console/public/application/containers/editor/editor.tsx b/src/plugins/console/public/application/containers/editor/editor.tsx index df017250664e4..4931648c07df7 100644 --- a/src/plugins/console/public/application/containers/editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/editor.tsx @@ -15,15 +15,17 @@ import { Panel, PanelsContainer } from '../../containers'; import { Editor as EditorUI, EditorOutput } from './legacy/console_editor'; import { StorageKeys } from '../../../services'; import { useEditorReadContext, useServicesContext, useRequestReadContext } from '../../contexts'; +import type { SenseEditor } from '../../models'; const INITIAL_PANEL_WIDTH = 50; const PANEL_MIN_WIDTH = '100px'; interface Props { loading: boolean; + setEditorInstance: (instance: SenseEditor) => void; } -export const Editor = memo(({ loading }: Props) => { +export const Editor = memo(({ loading, setEditorInstance }: Props) => { const { services: { storage }, } = useServicesContext(); @@ -61,7 +63,10 @@ export const Editor = memo(({ loading }: Props) => { {loading ? ( ) : ( - + )} { - + {}} /> diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx index 9105d86f9e2ec..9a879b4a72916 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -34,11 +34,13 @@ import { autoIndent, getDocumentation } from '../console_menu_actions'; import { subscribeResizeChecker } from '../subscribe_console_resize_checker'; import { applyCurrentSettings } from './apply_editor_settings'; import { registerCommands } from './keyboard_shortcuts'; +import type { SenseEditor } from '../../../../models/sense_editor'; const { useUIAceKeyboardMode } = ace; export interface EditorProps { initialTextValue: string; + setEditorInstance: (instance: SenseEditor) => void; } interface QueryParams { @@ -62,7 +64,7 @@ const DEFAULT_INPUT_VALUE = `GET _search const inputId = 'ConAppInputTextarea'; -function EditorUI({ initialTextValue }: EditorProps) { +function EditorUI({ initialTextValue, setEditorInstance }: EditorProps) { const { services: { history, notifications, settings: settingsService, esHostService, http }, docLinkVersion, @@ -225,12 +227,22 @@ function EditorUI({ initialTextValue }: EditorProps) { }, [settings]); useEffect(() => { - registerCommands({ - senseEditor: editorInstanceRef.current!, - sendCurrentRequestToES, - openDocumentation, - }); - }, [sendCurrentRequestToES, openDocumentation]); + const { isKeyboardShortcutsDisabled } = settings; + if (!isKeyboardShortcutsDisabled) { + registerCommands({ + senseEditor: editorInstanceRef.current!, + sendCurrentRequestToES, + openDocumentation, + }); + } + }, [sendCurrentRequestToES, openDocumentation, settings]); + + useEffect(() => { + const { current: editor } = editorInstanceRef; + if (editor) { + setEditorInstance(editor); + } + }, [setEditorInstance]); return (
diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/keyboard_shortcuts.ts b/src/plugins/console/public/application/containers/editor/legacy/console_editor/keyboard_shortcuts.ts index 4f09a49f3ac96..3d100ef0a5528 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/keyboard_shortcuts.ts +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/keyboard_shortcuts.ts @@ -15,6 +15,15 @@ interface Actions { openDocumentation: () => void; } +const COMMANDS = { + SEND_TO_ELASTICSEARCH: 'send to Elasticsearch', + OPEN_DOCUMENTATION: 'open documentation', + AUTO_INDENT_REQUEST: 'auto indent request', + MOVE_TO_PREVIOUS_REQUEST: 'move to previous request start or end', + MOVE_TO_NEXT_REQUEST: 'move to next request start or end', + GO_TO_LINE: 'gotoline', +}; + export function registerCommands({ senseEditor, sendCurrentRequestToES, @@ -28,12 +37,14 @@ export function registerCommands({ coreEditor.registerKeyboardShortcut({ keys: { win: 'Ctrl-Enter', mac: 'Command-Enter' }, - name: 'send to Elasticsearch', - fn: () => sendCurrentRequestToES(), + name: COMMANDS.SEND_TO_ELASTICSEARCH, + fn: () => { + sendCurrentRequestToES(); + }, }); coreEditor.registerKeyboardShortcut({ - name: 'open documentation', + name: COMMANDS.OPEN_DOCUMENTATION, keys: { win: 'Ctrl-/', mac: 'Command-/' }, fn: () => { openDocumentation(); @@ -41,7 +52,7 @@ export function registerCommands({ }); coreEditor.registerKeyboardShortcut({ - name: 'auto indent request', + name: COMMANDS.AUTO_INDENT_REQUEST, keys: { win: 'Ctrl-I', mac: 'Command-I' }, fn: () => { throttledAutoIndent(); @@ -49,7 +60,7 @@ export function registerCommands({ }); coreEditor.registerKeyboardShortcut({ - name: 'move to previous request start or end', + name: COMMANDS.MOVE_TO_PREVIOUS_REQUEST, keys: { win: 'Ctrl-Up', mac: 'Command-Up' }, fn: () => { senseEditor.moveToPreviousRequestEdge(); @@ -57,10 +68,28 @@ export function registerCommands({ }); coreEditor.registerKeyboardShortcut({ - name: 'move to next request start or end', + name: COMMANDS.MOVE_TO_NEXT_REQUEST, keys: { win: 'Ctrl-Down', mac: 'Command-Down' }, fn: () => { senseEditor.moveToNextRequestEdge(false); }, }); + + coreEditor.registerKeyboardShortcut({ + name: COMMANDS.GO_TO_LINE, + keys: { win: 'Ctrl-L', mac: 'Command-L' }, + fn: (editor) => { + const line = parseInt(prompt('Enter line number') ?? '', 10); + if (!isNaN(line)) { + editor.gotoLine(line); + } + }, + }); +} + +export function unregisterCommands(senseEditor: SenseEditor) { + const coreEditor = senseEditor.getCoreEditor(); + Object.values(COMMANDS).forEach((command) => { + coreEditor.unregisterKeyboardShortcut(command); + }); } diff --git a/src/plugins/console/public/application/containers/main/main.tsx b/src/plugins/console/public/application/containers/main/main.tsx index 30bf23d94a327..5895b919f9842 100644 --- a/src/plugins/console/public/application/containers/main/main.tsx +++ b/src/plugins/console/public/application/containers/main/main.tsx @@ -25,6 +25,7 @@ import { useServicesContext, useEditorReadContext, useRequestReadContext } from import { useDataInit } from '../../hooks'; import { getTopNavConfig } from './get_top_nav'; +import type { SenseEditor } from '../../models/sense_editor'; export function Main() { const { @@ -46,6 +47,8 @@ export function Main() { const [showSettings, setShowSettings] = useState(false); const [showHelp, setShowHelp] = useState(false); + const [editorInstance, setEditorInstance] = useState(null); + const renderConsoleHistory = () => { return editorsReady ? setShowHistory(false)} /> : null; }; @@ -108,7 +111,7 @@ export function Main() { {showingHistory ? {renderConsoleHistory()} : null} - + @@ -121,7 +124,9 @@ export function Main() { /> ) : null} - {showSettings ? setShowSettings(false)} /> : null} + {showSettings ? ( + setShowSettings(false)} editorInstance={editorInstance} /> + ) : null} {showHelp ? setShowHelp(false)} /> : null}
diff --git a/src/plugins/console/public/application/containers/settings.tsx b/src/plugins/console/public/application/containers/settings.tsx index f0ec64f4b13b2..c0bd1b18fff26 100644 --- a/src/plugins/console/public/application/containers/settings.tsx +++ b/src/plugins/console/public/application/containers/settings.tsx @@ -15,6 +15,7 @@ import { AutocompleteOptions, DevToolsSettingsModal } from '../components'; import { retrieveAutoCompleteInfo } from '../../lib/mappings/mappings'; import { useServicesContext, useEditorActionContext } from '../contexts'; import { DevToolsSettings, Settings as SettingsService } from '../../services'; +import type { SenseEditor } from '../models'; const getAutocompleteDiff = ( newSettings: DevToolsSettings, @@ -70,9 +71,10 @@ const fetchAutocompleteSettingsIfNeeded = ( export interface Props { onClose: () => void; + editorInstance: SenseEditor | null; } -export function Settings({ onClose }: Props) { +export function Settings({ onClose, editorInstance }: Props) { const { services: { settings, http }, } = useServicesContext(); @@ -102,6 +104,7 @@ export function Settings({ onClose }: Props) { refreshAutocompleteSettings(http, settings, selectedSettings) } settings={settings.toJSON()} + editorInstance={editorInstance} /> ); } diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts index f53a9dadbe108..e7c436c9806b3 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts @@ -49,9 +49,9 @@ export const useSendCurrentRequestToES = () => { const results = await sendRequestToES({ http, requests }); let saveToHistoryError: undefined | Error; - const { historyDisabled } = settings.toJSON(); + const { isHistoryDisabled } = settings.toJSON(); - if (!historyDisabled) { + if (!isHistoryDisabled) { results.forEach(({ request: { path, method, data } }) => { try { history.addToHistory(path, method, data); @@ -81,7 +81,7 @@ export const useSendCurrentRequestToES = () => { notifications.toasts.remove(toast); }, onDisableSavingToHistory: () => { - settings.setHistoryDisabled(true); + settings.setIsHistoryDisabled(true); notifications.toasts.remove(toast); }, }), diff --git a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts index 7a90dbe138f17..f13597e933bb2 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts +++ b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts @@ -297,6 +297,11 @@ export class LegacyCoreEditor implements CoreEditor { }); } + unregisterKeyboardShortcut(command: string) { + // @ts-ignore + this.editor.commands.removeCommand(command); + } + legacyUpdateUI(range: Range) { if (!this.$actions) { return; diff --git a/src/plugins/console/public/services/settings.ts b/src/plugins/console/public/services/settings.ts index 1a7eff3e7ca54..6757631a7d7c5 100644 --- a/src/plugins/console/public/services/settings.ts +++ b/src/plugins/console/public/services/settings.ts @@ -15,7 +15,8 @@ export const DEFAULT_SETTINGS = Object.freeze({ tripleQuotes: true, wrapMode: true, autocomplete: Object.freeze({ fields: true, indices: true, templates: true, dataStreams: true }), - historyDisabled: false, + isHistoryDisabled: false, + isKeyboardShortcutsDisabled: false, }); export interface DevToolsSettings { @@ -30,72 +31,96 @@ export interface DevToolsSettings { polling: boolean; pollInterval: number; tripleQuotes: boolean; - historyDisabled: boolean; + isHistoryDisabled: boolean; + isKeyboardShortcutsDisabled: boolean; +} + +enum SettingKeys { + FONT_SIZE = 'font_size', + WRAP_MODE = 'wrap_mode', + TRIPLE_QUOTES = 'triple_quotes', + AUTOCOMPLETE_SETTINGS = 'autocomplete_settings', + CONSOLE_POLLING = 'console_polling', + POLL_INTERVAL = 'poll_interval', + IS_HISTORY_DISABLED = 'is_history_disabled', + IS_KEYBOARD_SHORTCUTS_DISABLED = 'is_keyboard_shortcuts_disabled', } export class Settings { constructor(private readonly storage: Storage) {} getFontSize() { - return this.storage.get('font_size', DEFAULT_SETTINGS.fontSize); + return this.storage.get(SettingKeys.FONT_SIZE, DEFAULT_SETTINGS.fontSize); } setFontSize(size: number) { - this.storage.set('font_size', size); + this.storage.set(SettingKeys.FONT_SIZE, size); return true; } getWrapMode() { - return this.storage.get('wrap_mode', DEFAULT_SETTINGS.wrapMode); + return this.storage.get(SettingKeys.WRAP_MODE, DEFAULT_SETTINGS.wrapMode); } setWrapMode(mode: boolean) { - this.storage.set('wrap_mode', mode); + this.storage.set(SettingKeys.WRAP_MODE, mode); return true; } setTripleQuotes(tripleQuotes: boolean) { - this.storage.set('triple_quotes', tripleQuotes); + this.storage.set(SettingKeys.TRIPLE_QUOTES, tripleQuotes); return true; } getTripleQuotes() { - return this.storage.get('triple_quotes', DEFAULT_SETTINGS.tripleQuotes); + return this.storage.get(SettingKeys.TRIPLE_QUOTES, DEFAULT_SETTINGS.tripleQuotes); } getAutocomplete() { - return this.storage.get('autocomplete_settings', DEFAULT_SETTINGS.autocomplete); + return this.storage.get(SettingKeys.AUTOCOMPLETE_SETTINGS, DEFAULT_SETTINGS.autocomplete); } setAutocomplete(settings: object) { - this.storage.set('autocomplete_settings', settings); + this.storage.set(SettingKeys.AUTOCOMPLETE_SETTINGS, settings); return true; } getPolling() { - return this.storage.get('console_polling', DEFAULT_SETTINGS.polling); + return this.storage.get(SettingKeys.CONSOLE_POLLING, DEFAULT_SETTINGS.polling); } setPolling(polling: boolean) { - this.storage.set('console_polling', polling); + this.storage.set(SettingKeys.CONSOLE_POLLING, polling); return true; } - setHistoryDisabled(disable: boolean) { - this.storage.set('disable_history', disable); + setIsHistoryDisabled(isDisabled: boolean) { + this.storage.set(SettingKeys.IS_HISTORY_DISABLED, isDisabled); return true; } - getHistoryDisabled() { - return this.storage.get('disable_history', DEFAULT_SETTINGS.historyDisabled); + getIsHistoryDisabled() { + return this.storage.get(SettingKeys.IS_HISTORY_DISABLED, DEFAULT_SETTINGS.isHistoryDisabled); } setPollInterval(interval: number) { - this.storage.set('poll_interval', interval); + this.storage.set(SettingKeys.POLL_INTERVAL, interval); } getPollInterval() { - return this.storage.get('poll_interval', DEFAULT_SETTINGS.pollInterval); + return this.storage.get(SettingKeys.POLL_INTERVAL, DEFAULT_SETTINGS.pollInterval); + } + + setIsKeyboardShortcutsDisabled(disable: boolean) { + this.storage.set(SettingKeys.IS_KEYBOARD_SHORTCUTS_DISABLED, disable); + return true; + } + + getIsKeyboardShortcutsDisabled() { + return this.storage.get( + SettingKeys.IS_KEYBOARD_SHORTCUTS_DISABLED, + DEFAULT_SETTINGS.isKeyboardShortcutsDisabled + ); } toJSON(): DevToolsSettings { @@ -106,7 +131,8 @@ export class Settings { fontSize: parseFloat(this.getFontSize()), polling: Boolean(this.getPolling()), pollInterval: this.getPollInterval(), - historyDisabled: Boolean(this.getHistoryDisabled()), + isHistoryDisabled: Boolean(this.getIsHistoryDisabled()), + isKeyboardShortcutsDisabled: Boolean(this.getIsKeyboardShortcutsDisabled()), }; } @@ -117,7 +143,8 @@ export class Settings { autocomplete, polling, pollInterval, - historyDisabled, + isHistoryDisabled, + isKeyboardShortcutsDisabled, }: DevToolsSettings) { this.setFontSize(fontSize); this.setWrapMode(wrapMode); @@ -125,7 +152,8 @@ export class Settings { this.setAutocomplete(autocomplete); this.setPolling(polling); this.setPollInterval(pollInterval); - this.setHistoryDisabled(historyDisabled); + this.setIsHistoryDisabled(isHistoryDisabled); + this.setIsKeyboardShortcutsDisabled(isKeyboardShortcutsDisabled); } } diff --git a/src/plugins/console/public/types/core_editor.ts b/src/plugins/console/public/types/core_editor.ts index cc344d6bcc881..db8010afe762b 100644 --- a/src/plugins/console/public/types/core_editor.ts +++ b/src/plugins/console/public/types/core_editor.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { Editor } from 'brace'; import { TokensProvider } from './tokens_provider'; import { Token } from './token'; @@ -252,10 +253,15 @@ export interface CoreEditor { */ registerKeyboardShortcut(opts: { keys: string | { win?: string; mac?: string }; - fn: () => void; + fn: (editor: Editor) => void; name: string; }): void; + /** + * Unregister a keyboard shortcut and provide a command name + */ + unregisterKeyboardShortcut(command: string): void; + /** * Register a completions function that will be called when the editor * detects a change diff --git a/src/plugins/telemetry/public/plugin.ts b/src/plugins/telemetry/public/plugin.ts index b227a0b751e03..44799d7f4ffc5 100644 --- a/src/plugins/telemetry/public/plugin.ts +++ b/src/plugins/telemetry/public/plugin.ts @@ -134,7 +134,7 @@ export class TelemetryPlugin implements Plugin { await this.refreshConfig(); + analytics.optIn({ global: { enabled: this.telemetryService!.isOptedIn } }); }); if (home && !this.config.hidePrivacyStatement) { @@ -179,6 +180,7 @@ export class TelemetryPlugin implements Plugin { ); useEffect(() => { - setLegendVisibility(legendUiStateValue); - }, [legendUiStateValue]); + setLegendVisibility(legendUiStateValue ?? stateParams.legendDisplay === LegendDisplay.SHOW); + }, [legendUiStateValue, stateParams.legendDisplay]); useEffect(() => { const fetchPalettes = async () => { diff --git a/test/analytics/__fixtures__/plugins/analytics_plugin_a/server/plugin.ts b/test/analytics/__fixtures__/plugins/analytics_plugin_a/server/plugin.ts index 35d235b896aae..87963bd0c2d05 100644 --- a/test/analytics/__fixtures__/plugins/analytics_plugin_a/server/plugin.ts +++ b/test/analytics/__fixtures__/plugins/analytics_plugin_a/server/plugin.ts @@ -61,13 +61,18 @@ export class AnalyticsPluginAPlugin implements Plugin { validate: { query: schema.object({ takeNumberOfCounters: schema.number({ min: 1 }), + eventType: schema.string(), }), }, }, async (context, req, res) => { - const { takeNumberOfCounters } = req.query; + const { takeNumberOfCounters, eventType } = req.query; - return res.ok({ body: stats.slice(-takeNumberOfCounters) }); + return res.ok({ + body: stats + .filter((counter) => counter.event_type === eventType) + .slice(-takeNumberOfCounters), + }); } ); diff --git a/test/analytics/config.ts b/test/analytics/config.ts index 46fa5d892997a..1ecac5af0d01a 100644 --- a/test/analytics/config.ts +++ b/test/analytics/config.ts @@ -34,6 +34,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...functionalConfig.get('kbnTestServer'), serverArgs: [ ...functionalConfig.get('kbnTestServer.serverArgs'), + // Disabling telemetry so it doesn't call opt-in before the tests run. + '--telemetry.enabled=false', `--plugin-path=${path.resolve(__dirname, './__fixtures__/plugins/analytics_plugin_a')}`, `--plugin-path=${path.resolve(__dirname, './__fixtures__/plugins/analytics_ftr_helpers')}`, ], diff --git a/test/analytics/tests/analytics_from_the_browser.ts b/test/analytics/tests/analytics_from_the_browser.ts index 9c866204ce18e..88da2ddcb5bc7 100644 --- a/test/analytics/tests/analytics_from_the_browser.ts +++ b/test/analytics/tests/analytics_from_the_browser.ts @@ -23,7 +23,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ): Promise => { return await browser.execute( ({ takeNumberOfCounters }) => - window.__analyticsPluginA__.stats.slice(-takeNumberOfCounters), + window.__analyticsPluginA__.stats + .filter((counter) => counter.event_type === 'test-plugin-lifecycle') + .slice(-takeNumberOfCounters), { takeNumberOfCounters: _takeNumberOfCounters } ); }; @@ -70,6 +72,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(context).to.have.property('user_agent'); expect(context.user_agent).to.be.a('string'); + const reportEventContext = actions[2].meta[1].context; + expect(reportEventContext).to.have.property('user_agent'); + expect(reportEventContext.user_agent).to.be.a('string'); + expect(actions).to.eql([ { action: 'optIn', meta: true }, { action: 'extendContext', meta: context }, @@ -85,7 +91,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { { timestamp: actions[2].meta[1].timestamp, event_type: 'test-plugin-lifecycle', - context, + context: reportEventContext, properties: { plugin: 'analyticsPluginA', step: 'start' }, }, ], @@ -103,7 +109,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { { timestamp: actions[2].meta[1].timestamp, event_type: 'test-plugin-lifecycle', - context, + context: reportEventContext, properties: { plugin: 'analyticsPluginA', step: 'start' }, }, ]); diff --git a/test/analytics/tests/analytics_from_the_server.ts b/test/analytics/tests/analytics_from_the_server.ts index 8555d91031d27..0935e52136e60 100644 --- a/test/analytics/tests/analytics_from_the_server.ts +++ b/test/analytics/tests/analytics_from_the_server.ts @@ -20,7 +20,7 @@ export default function ({ getService }: FtrProviderContext) { ): Promise => { const resp = await supertest .get(`/internal/analytics_plugin_a/stats`) - .query({ takeNumberOfCounters }) + .query({ takeNumberOfCounters, eventType: 'test-plugin-lifecycle' }) .set('kbn-xsrf', 'xxx') .expect(200); diff --git a/test/functional/apps/discover/_data_view_editor.ts b/test/functional/apps/discover/_data_view_editor.ts index c67964fddf93b..8e0b42469dbbc 100644 --- a/test/functional/apps/discover/_data_view_editor.ts +++ b/test/functional/apps/discover/_data_view_editor.ts @@ -14,10 +14,22 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const esArchiver = getService('esArchiver'); const security = getService('security'); + const config = getService('config'); const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); + const defaultIndexPatternString = config.get('esTestCluster.ccs') + ? 'ftr-remote:logstash-*' + : 'logstash-*'; const defaultSettings = { - defaultIndex: 'logstash-*', + defaultIndex: defaultIndexPatternString, }; + const localArchiveDirectory = 'test/functional/fixtures/kbn_archiver/discover'; + const remoteArchiveDirectory = 'test/functional/fixtures/kbn_archiver/ccs/discover'; + const esNode = config.get('esTestCluster.ccs') + ? getService('remoteEsArchiver' as 'esArchiver') + : esArchiver; + const kbnDirectory = config.get('esTestCluster.ccs') + ? remoteArchiveDirectory + : localArchiveDirectory; const createDataView = async (dataViewName: string) => { await PageObjects.discover.clickIndexPatternActions(); @@ -32,9 +44,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('discover integration with data view editor', function describeIndexTests() { before(async function () { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await esNode.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await kibanaServer.savedObjects.clean({ types: ['saved-search', 'index-pattern'] }); - await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); + await kibanaServer.importExport.load(kbnDirectory); await kibanaServer.uiSettings.replace(defaultSettings); await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); @@ -42,12 +54,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async () => { await security.testUser.restoreDefaults(); - await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + await kibanaServer.importExport.unload(kbnDirectory); await kibanaServer.savedObjects.clean({ types: ['saved-search', 'index-pattern'] }); }); it('allows creating a new data view', async function () { - const dataViewToCreate = 'logstash'; + const dataViewToCreate = config.get('esTestCluster.ccs') ? 'ftr-remote:logstash' : 'logstash'; await createDataView(dataViewToCreate); await PageObjects.header.waitUntilLoadingHasFinished(); await retry.waitForWithTimeout( diff --git a/test/functional/apps/discover/_field_data_with_fields_api.ts b/test/functional/apps/discover/_field_data_with_fields_api.ts index 402783694cbd5..d7912d6d0959f 100644 --- a/test/functional/apps/discover/_field_data_with_fields_api.ts +++ b/test/functional/apps/discover/_field_data_with_fields_api.ts @@ -34,8 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); }); - // FLAKY: https://github.com/elastic/kibana/issues/127905 - describe.skip('field data', function () { + describe('field data', function () { it('search php should show the correct hit count', async function () { const expectedHitCount = '445'; await retry.try(async function () { @@ -92,12 +91,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { hash.replace('columns:!()', 'columns:!(relatedContent)'), { useActualUrl: true } ); + + await PageObjects.header.waitUntilLoadingHasFinished(); await retry.try(async function tryingForTime() { expect(await PageObjects.discover.getDocHeader()).to.contain('relatedContent'); - }); - const field = await PageObjects.discover.getDocTableIndex(1); - expect(field).to.contain('relatedContent.url'); + const field = await PageObjects.discover.getDocTableIndex(1); + expect(field).to.contain('relatedContent.url'); + }); const marks = await PageObjects.discover.getMarks(); expect(marks.length).to.be.above(0); diff --git a/test/functional/apps/discover/_saved_queries.ts b/test/functional/apps/discover/_saved_queries.ts index fd49a03413321..79d49131df138 100644 --- a/test/functional/apps/discover/_saved_queries.ts +++ b/test/functional/apps/discover/_saved_queries.ts @@ -13,7 +13,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const log = getService('log'); - const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); const browser = getService('browser'); @@ -21,9 +20,30 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const queryBar = getService('queryBar'); const savedQueryManagementComponent = getService('savedQueryManagementComponent'); const testSubjects = getService('testSubjects'); + const config = getService('config'); + const localArchiveDirectories = { + nested: 'test/functional/fixtures/kbn_archiver/date_nested.json', + discover: 'test/functional/fixtures/kbn_archiver/discover.json', + }; + const remoteArchiveDirectories = { + nested: 'test/functional/fixtures/kbn_archiver/ccs/date_nested.json', + discover: 'test/functional/fixtures/kbn_archiver/ccs/discover.json', + }; + const logstashIndexPatternString = config.get('esTestCluster.ccs') + ? 'ftr-remote:logstash-*' + : 'logstash-*'; + const dateNestedIndexPattern = config.get('esTestCluster.ccs') + ? 'ftr-remote:date-nested' + : 'date-nested'; const defaultSettings = { - defaultIndex: 'logstash-*', + defaultIndex: logstashIndexPatternString, }; + const esNode = config.get('esTestCluster.ccs') + ? getService('remoteEsArchiver' as 'esArchiver') + : getService('esArchiver'); + const kbnArchives = config.get('esTestCluster.ccs') + ? remoteArchiveDirectories + : localArchiveDirectories; const from = 'Sep 20, 2015 @ 08:00:00.000'; const to = 'Sep 21, 2015 @ 08:00:00.000'; @@ -34,7 +54,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { log.debug('set up a query with filters to save'); await PageObjects.common.setTime({ from, to }); await PageObjects.common.navigateToApp('discover'); - await PageObjects.discover.selectIndexPattern('logstash-*'); + await PageObjects.discover.selectIndexPattern(logstashIndexPatternString); await retry.try(async function tryingForTime() { const hitCount = await PageObjects.discover.getHitCount(); expect(hitCount).to.be('4,731'); @@ -59,12 +79,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { log.debug('load kibana index with default index pattern'); await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern', 'query'] }); - await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); - await kibanaServer.importExport.load( - 'test/functional/fixtures/kbn_archiver/date_nested.json' - ); - await esArchiver.load('test/functional/fixtures/es_archiver/date_nested'); - await esArchiver.load('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.importExport.load(kbnArchives.discover); + await kibanaServer.importExport.load(kbnArchives.nested); + await esNode.load('test/functional/fixtures/es_archiver/date_nested'); + await esNode.load('test/functional/fixtures/es_archiver/logstash_functional'); await kibanaServer.uiSettings.replace(defaultSettings); log.debug('discover'); @@ -72,12 +90,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { - await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); - await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/date_nested'); + await kibanaServer.importExport.unload(kbnArchives.discover); + await kibanaServer.importExport.unload(kbnArchives.nested); await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern', 'query'] }); await kibanaServer.savedObjects.clean({ types: ['search', 'query'] }); - await esArchiver.unload('test/functional/fixtures/es_archiver/date_nested'); - await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + await esNode.unload('test/functional/fixtures/es_archiver/date_nested'); + await esNode.unload('test/functional/fixtures/es_archiver/logstash_functional'); await PageObjects.common.unsetTime(); }); @@ -102,14 +120,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(false); expect(await queryBar.getQueryString()).to.eql(''); - await PageObjects.discover.selectIndexPattern('date-nested'); + await PageObjects.discover.selectIndexPattern(dateNestedIndexPattern); expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(false); expect(await queryBar.getQueryString()).to.eql(''); - await PageObjects.discover.selectIndexPattern('logstash-*'); + await PageObjects.discover.selectIndexPattern(logstashIndexPatternString); const currentDataView = await PageObjects.discover.getCurrentlySelectedDataView(); - expect(currentDataView).to.be('logstash-*'); + expect(currentDataView).to.be(logstashIndexPatternString); await retry.try(async function tryingForTime() { const hitCount = await PageObjects.discover.getHitCount(); expect(hitCount).to.be('4,731'); diff --git a/test/functional/apps/discover/_search_on_page_load.ts b/test/functional/apps/discover/_search_on_page_load.ts index 3df6ce1c13c43..91943b28c6db0 100644 --- a/test/functional/apps/discover/_search_on_page_load.ts +++ b/test/functional/apps/discover/_search_on_page_load.ts @@ -18,6 +18,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); const testSubjects = getService('testSubjects'); + const refreshButtonSelector = 'refreshDataButton'; const defaultSettings = { defaultIndex: 'logstash-*', @@ -58,63 +59,57 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); }); - // FLAKY: https://github.com/elastic/kibana/issues/118432 - describe.skip(`when it's false`, () => { + describe(`when it's false`, () => { beforeEach(async () => await initSearchOnPageLoad(false)); it('should not fetch data from ES initially', async function () { - expect(await testSubjects.exists('refreshDataButton')).to.be(true); + expect(await testSubjects.exists(refreshButtonSelector)).to.be(true); await retry.waitFor('number of fetches to be 0', waitForFetches(0)); }); it('should not fetch on indexPattern change', async function () { - expect(await testSubjects.exists('refreshDataButton')).to.be(true); + expect(await testSubjects.exists(refreshButtonSelector)).to.be(true); await retry.waitFor('number of fetches to be 0', waitForFetches(0)); await PageObjects.discover.selectIndexPattern('date-nested'); - expect(await testSubjects.exists('refreshDataButton')).to.be(true); + expect(await testSubjects.exists(refreshButtonSelector)).to.be(true); await retry.waitFor('number of fetches to be 0', waitForFetches(0)); }); it('should fetch data from ES after refreshDataButton click', async function () { - expect(await testSubjects.exists('refreshDataButton')).to.be(true); + expect(await testSubjects.exists(refreshButtonSelector)).to.be(true); await retry.waitFor('number of fetches to be 0', waitForFetches(0)); - /** - * We should wait for debounce timeout expired 100 ms, - * otherwise click event will be skipped. See getFetch$ implementation. - */ - await PageObjects.common.sleep(100); - await testSubjects.click('refreshDataButton'); + await testSubjects.click(refreshButtonSelector); + await testSubjects.missingOrFail(refreshButtonSelector); await retry.waitFor('number of fetches to be 1', waitForFetches(1)); - expect(await testSubjects.exists('refreshDataButton')).to.be(false); }); it('should fetch data from ES after submit query', async function () { - expect(await testSubjects.exists('refreshDataButton')).to.be(true); + expect(await testSubjects.exists(refreshButtonSelector)).to.be(true); await retry.waitFor('number of fetches to be 0', waitForFetches(0)); await queryBar.submitQuery(); + await testSubjects.missingOrFail(refreshButtonSelector); await retry.waitFor('number of fetches to be 1', waitForFetches(1)); - expect(await testSubjects.exists('refreshDataButton')).to.be(false); }); it('should fetch data from ES after choosing commonly used time range', async function () { await PageObjects.discover.selectIndexPattern('logstash-*'); - expect(await testSubjects.exists('refreshDataButton')).to.be(true); + expect(await testSubjects.exists(refreshButtonSelector)).to.be(true); await retry.waitFor('number of fetches to be 0', waitForFetches(0)); await PageObjects.timePicker.setCommonlyUsedTime('This_week'); + await testSubjects.missingOrFail(refreshButtonSelector); await retry.waitFor('number of fetches to be 1', waitForFetches(1)); - expect(await testSubjects.exists('refreshDataButton')).to.be(false); }); }); - it(`when it's false should fetch data from ES initially`, async function () { + it(`when it's true should fetch data from ES initially`, async function () { await initSearchOnPageLoad(true); await retry.waitFor('number of fetches to be 1', waitForFetches(1)); }); diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts index 994cfeb851281..e2895f3ca56b4 100644 --- a/test/functional/apps/discover/index.ts +++ b/test/functional/apps/discover/index.ts @@ -5,12 +5,12 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); + const config = getService('config'); describe('discover app', function () { this.tags('ciGroup6'); @@ -23,38 +23,43 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); }); - loadTestFile(require.resolve('./_saved_queries')); - loadTestFile(require.resolve('./_discover')); - loadTestFile(require.resolve('./_discover_histogram')); - loadTestFile(require.resolve('./_doc_table')); - loadTestFile(require.resolve('./_doc_table_newline')); - loadTestFile(require.resolve('./_filter_editor')); - loadTestFile(require.resolve('./_errors')); - loadTestFile(require.resolve('./_field_data')); - loadTestFile(require.resolve('./_field_data_with_fields_api')); - loadTestFile(require.resolve('./_shared_links')); - loadTestFile(require.resolve('./_sidebar')); - loadTestFile(require.resolve('./_source_filters')); - loadTestFile(require.resolve('./_large_string')); - loadTestFile(require.resolve('./_inspector')); - loadTestFile(require.resolve('./_classic_table_doc_navigation')); - loadTestFile(require.resolve('./_date_nanos')); - loadTestFile(require.resolve('./_date_nanos_mixed')); - loadTestFile(require.resolve('./_indexpattern_without_timefield')); - loadTestFile(require.resolve('./_discover_fields_api')); - loadTestFile(require.resolve('./_data_grid')); - loadTestFile(require.resolve('./_data_grid_context')); - loadTestFile(require.resolve('./_data_grid_field_data')); - loadTestFile(require.resolve('./_data_grid_doc_navigation')); - loadTestFile(require.resolve('./_data_grid_doc_table')); - loadTestFile(require.resolve('./_indexpattern_with_unmapped_fields')); - loadTestFile(require.resolve('./_runtime_fields_editor')); - loadTestFile(require.resolve('./_huge_fields')); - loadTestFile(require.resolve('./_date_nested')); - loadTestFile(require.resolve('./_search_on_page_load')); - loadTestFile(require.resolve('./_chart_hidden')); - loadTestFile(require.resolve('./_context_encoded_url_param')); - loadTestFile(require.resolve('./_data_view_editor')); - loadTestFile(require.resolve('./_empty_state')); + if (config.get('esTestCluster.ccs')) { + loadTestFile(require.resolve('./_data_view_editor')); + loadTestFile(require.resolve('./_saved_queries')); + } else { + loadTestFile(require.resolve('./_saved_queries')); + loadTestFile(require.resolve('./_discover')); + loadTestFile(require.resolve('./_discover_histogram')); + loadTestFile(require.resolve('./_doc_table')); + loadTestFile(require.resolve('./_doc_table_newline')); + loadTestFile(require.resolve('./_filter_editor')); + loadTestFile(require.resolve('./_errors')); + loadTestFile(require.resolve('./_field_data')); + loadTestFile(require.resolve('./_field_data_with_fields_api')); + loadTestFile(require.resolve('./_shared_links')); + loadTestFile(require.resolve('./_sidebar')); + loadTestFile(require.resolve('./_source_filters')); + loadTestFile(require.resolve('./_large_string')); + loadTestFile(require.resolve('./_inspector')); + loadTestFile(require.resolve('./_classic_table_doc_navigation')); + loadTestFile(require.resolve('./_date_nanos')); + loadTestFile(require.resolve('./_date_nanos_mixed')); + loadTestFile(require.resolve('./_indexpattern_without_timefield')); + loadTestFile(require.resolve('./_discover_fields_api')); + loadTestFile(require.resolve('./_data_grid')); + loadTestFile(require.resolve('./_data_grid_context')); + loadTestFile(require.resolve('./_data_grid_field_data')); + loadTestFile(require.resolve('./_data_grid_doc_navigation')); + loadTestFile(require.resolve('./_data_grid_doc_table')); + loadTestFile(require.resolve('./_indexpattern_with_unmapped_fields')); + loadTestFile(require.resolve('./_runtime_fields_editor')); + loadTestFile(require.resolve('./_huge_fields')); + loadTestFile(require.resolve('./_date_nested')); + loadTestFile(require.resolve('./_search_on_page_load')); + loadTestFile(require.resolve('./_chart_hidden')); + loadTestFile(require.resolve('./_context_encoded_url_param')); + loadTestFile(require.resolve('./_data_view_editor')); + loadTestFile(require.resolve('./_empty_state')); + } }); } diff --git a/test/functional/apps/management/_index_pattern_filter.ts b/test/functional/apps/management/_index_pattern_filter.ts index 732065282d546..c0f215e560fb0 100644 --- a/test/functional/apps/management/_index_pattern_filter.ts +++ b/test/functional/apps/management/_index_pattern_filter.ts @@ -16,7 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); // FLAKY: https://github.com/elastic/kibana/issues/128558 - describe.skip('index pattern filter', function describeIndexTests() { + describe('data view filter', function describeIndexTests() { before(async function () { await esArchiver.emptyKibanaIndex(); await kibanaServer.uiSettings.replace({}); diff --git a/test/functional_ccs/config.ts b/test/functional/config.ccs.ts similarity index 80% rename from test/functional_ccs/config.ts rename to test/functional/config.ccs.ts index e99a5310453d9..ecb42758785e5 100644 --- a/test/functional_ccs/config.ts +++ b/test/functional/config.ccs.ts @@ -7,8 +7,10 @@ */ import { FtrConfigProviderContext } from '@kbn/test'; -import { services } from './services'; +import { RemoteEsArchiverProvider } from './services/remote_es/remote_es_archiver'; +import { RemoteEsProvider } from './services/remote_es/remote_es'; +// eslint-disable-next-line import/no-default-export export default async function ({ readConfigFile }: FtrConfigProviderContext) { const functionalConfig = await readConfigFile(require.resolve('../functional/config')); @@ -17,7 +19,11 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { testFiles: [require.resolve('./apps/discover')], - services, + services: { + ...functionalConfig.get('services'), + remoteEs: RemoteEsProvider, + remoteEsArchiver: RemoteEsArchiverProvider, + }, junit: { reportName: 'Kibana CCS Tests', diff --git a/test/functional/fixtures/kbn_archiver/date_nested_ccs.json b/test/functional/fixtures/kbn_archiver/ccs/date_nested.json similarity index 100% rename from test/functional/fixtures/kbn_archiver/date_nested_ccs.json rename to test/functional/fixtures/kbn_archiver/ccs/date_nested.json diff --git a/test/functional/fixtures/kbn_archiver/discover_ccs.json b/test/functional/fixtures/kbn_archiver/ccs/discover.json similarity index 100% rename from test/functional/fixtures/kbn_archiver/discover_ccs.json rename to test/functional/fixtures/kbn_archiver/ccs/discover.json diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index dfaaecff0a0c7..4f51452197532 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -273,8 +273,12 @@ export class SettingsPageObject extends FtrService { async clearFieldTypeFilter(type: string) { await this.testSubjects.clickWhenNotDisabled('indexedFieldTypeFilterDropdown'); - await this.testSubjects.existOrFail('indexedFieldTypeFilterDropdown-popover'); - await this.testSubjects.existOrFail(`indexedFieldTypeFilterDropdown-option-${type}-checked`); + await this.retry.try(async () => { + await this.testSubjects.existOrFail('indexedFieldTypeFilterDropdown-popover'); + }); + await this.retry.try(async () => { + await this.testSubjects.existOrFail(`indexedFieldTypeFilterDropdown-option-${type}-checked`); + }); await this.testSubjects.click(`indexedFieldTypeFilterDropdown-option-${type}-checked`); await this.testSubjects.existOrFail(`indexedFieldTypeFilterDropdown-option-${type}`); await this.browser.pressKeys(this.browser.keys.ESCAPE); diff --git a/test/functional_ccs/services/remote_es.ts b/test/functional/services/remote_es/remote_es.ts similarity index 92% rename from test/functional_ccs/services/remote_es.ts rename to test/functional/services/remote_es/remote_es.ts index 05a10d9e068f0..37ce35cdfefbc 100644 --- a/test/functional_ccs/services/remote_es.ts +++ b/test/functional/services/remote_es/remote_es.ts @@ -9,7 +9,7 @@ import { Client } from '@elastic/elasticsearch'; import { systemIndicesSuperuser, createRemoteEsClientForFtrConfig } from '@kbn/test'; -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; /** * Kibana-specific @elastic/elasticsearch client instance. diff --git a/test/functional_ccs/services/remote_es_archiver.ts b/test/functional/services/remote_es/remote_es_archiver.ts similarity index 85% rename from test/functional_ccs/services/remote_es_archiver.ts rename to test/functional/services/remote_es/remote_es_archiver.ts index 569792d050a4d..7e6f4241591e3 100644 --- a/test/functional_ccs/services/remote_es_archiver.ts +++ b/test/functional/services/remote_es/remote_es_archiver.ts @@ -7,10 +7,10 @@ */ import { EsArchiver } from '@kbn/es-archiver'; -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export function RemoteEsArchiverProvider({ getService }: FtrProviderContext): EsArchiver { - const remoteEs = getService('remoteEs'); + const remoteEs = getService('remoteEs' as 'es'); const log = getService('log'); const kibanaServer = getService('kibanaServer'); diff --git a/test/functional_ccs/apps/discover/data_view_ccs.ts b/test/functional_ccs/apps/discover/data_view_ccs.ts deleted file mode 100644 index 5fc39ff5705df..0000000000000 --- a/test/functional_ccs/apps/discover/data_view_ccs.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const retry = getService('retry'); - const testSubjects = getService('testSubjects'); - const kibanaServer = getService('kibanaServer'); - const remoteEsArchiver = getService('remoteEsArchiver'); - - const security = getService('security'); - const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); - - const createDataView = async (dataViewName: string) => { - await PageObjects.discover.clickIndexPatternActions(); - await PageObjects.discover.clickCreateNewDataView(); - await testSubjects.setValue('createIndexPatternNameInput', dataViewName, { - clearWithKeyboard: true, - typeCharByChar: true, - }); - await testSubjects.click('saveIndexPatternButton'); - }; - - describe('discover integration with data view editor', function describeIndexTests() { - before(async function () { - await security.testUser.setRoles([ - 'kibana_admin', - 'test_logstash_reader', - 'ccs_remote_search', - ]); - await remoteEsArchiver.loadIfNeeded( - 'test/functional/fixtures/es_archiver/logstash_functional' - ); - await kibanaServer.savedObjects.clean({ types: ['saved-search', 'index-pattern'] }); - // The test creates the 'ftr-remote:logstash*" data view but we have to load the discover_ccs - // which contains ftr-remote:logstash-* otherwise, discover will redirect us to another page. - await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover_ccs'); - await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); - await PageObjects.common.navigateToApp('discover'); - }); - - after(async () => { - await security.testUser.restoreDefaults(); - await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover_ccs'); - await kibanaServer.savedObjects.clean({ types: ['saved-search', 'index-pattern'] }); - }); - - it('use ccs to create a new data view', async function () { - const dataViewToCreate = 'ftr-remote:logstash'; - await createDataView(dataViewToCreate); - await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.waitForWithTimeout( - 'data view selector to include a newly created dataview', - 5000, - async () => { - const dataViewTitle = await PageObjects.discover.getCurrentlySelectedDataView(); - // data view editor will add wildcard symbol by default - // so we need to include it in our original title when comparing - return dataViewTitle === `${dataViewToCreate}*`; - } - ); - }); - }); -} diff --git a/test/functional_ccs/apps/discover/index.ts b/test/functional_ccs/apps/discover/index.ts deleted file mode 100644 index 2e9d428f44c60..0000000000000 --- a/test/functional_ccs/apps/discover/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, loadTestFile }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); - const browser = getService('browser'); - - describe('discover app css', function () { - this.tags('ciGroup6'); - - before(async () => { - await browser.setWindowSize(1300, 800); - }); - - loadTestFile(require.resolve('./data_view_ccs')); - loadTestFile(require.resolve('./saved_queries_ccs')); - - after(async () => { - await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); - }); - }); -} diff --git a/test/functional_ccs/apps/discover/saved_queries_ccs.ts b/test/functional_ccs/apps/discover/saved_queries_ccs.ts deleted file mode 100644 index 08b6d61368f5d..0000000000000 --- a/test/functional_ccs/apps/discover/saved_queries_ccs.ts +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import expect from '@kbn/expect'; - -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const retry = getService('retry'); - const log = getService('log'); - const remoteEsArchiver = getService('remoteEsArchiver'); - const kibanaServer = getService('kibanaServer'); - const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); - const browser = getService('browser'); - const filterBar = getService('filterBar'); - const queryBar = getService('queryBar'); - const savedQueryManagementComponent = getService('savedQueryManagementComponent'); - const testSubjects = getService('testSubjects'); - const defaultSettings = { - defaultIndex: 'logstash-*', - }; - - const setUpQueriesWithFilters = async () => { - // set up a query with filters and a time filter - log.debug('set up a query with filters to save'); - const from = 'Sep 20, 2015 @ 08:00:00.000'; - const to = 'Sep 21, 2015 @ 08:00:00.000'; - await PageObjects.common.setTime({ from, to }); - await PageObjects.common.navigateToApp('discover'); - await filterBar.addFilter('extension.raw', 'is one of', 'jpg'); - await queryBar.setQuery('response:200'); - }; - - describe('saved queries saved objects', function describeIndexTests() { - before(async function () { - log.debug('load kibana index with default index pattern'); - await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); - - await kibanaServer.importExport.load( - 'test/functional/fixtures/kbn_archiver/discover_ccs.json' - ); - await kibanaServer.importExport.load( - 'test/functional/fixtures/kbn_archiver/date_nested_ccs.json' - ); - await remoteEsArchiver.load('test/functional/fixtures/es_archiver/date_nested'); - await remoteEsArchiver.load('test/functional/fixtures/es_archiver/logstash_functional'); - - await kibanaServer.uiSettings.replace(defaultSettings); - log.debug('discover'); - await PageObjects.common.navigateToApp('discover'); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - }); - - after(async () => { - await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover_ccs'); - await kibanaServer.importExport.unload( - 'test/functional/fixtures/kbn_archiver/date_nested_ccs' - ); - await remoteEsArchiver.unload('test/functional/fixtures/es_archiver/date_nested'); - await remoteEsArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); - await PageObjects.common.unsetTime(); - }); - - describe('saved query selection', () => { - before(async () => await setUpQueriesWithFilters()); - - it(`should unselect saved query when navigating to a 'new'`, async function () { - await savedQueryManagementComponent.saveNewQuery( - 'test-unselect-saved-query', - 'mock', - true, - true - ); - - await queryBar.submitQuery(); - - expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true); - expect(await queryBar.getQueryString()).to.eql('response:200'); - - await PageObjects.discover.clickNewSearchButton(); - - expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(false); - expect(await queryBar.getQueryString()).to.eql(''); - - await PageObjects.discover.selectIndexPattern('ftr-remote:date-nested'); - - expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(false); - expect(await queryBar.getQueryString()).to.eql(''); - - await PageObjects.discover.selectIndexPattern('ftr-remote:logstash-*'); - - expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(false); - expect(await queryBar.getQueryString()).to.eql(''); - - // reset state - await savedQueryManagementComponent.deleteSavedQuery('test-unselect-saved-query'); - }); - }); - - describe('saved query management component functionality', function () { - before(async () => await setUpQueriesWithFilters()); - - it('should show the saved query management component when there are no saved queries', async () => { - await savedQueryManagementComponent.openSavedQueryManagementComponent(); - const descriptionText = await testSubjects.getVisibleText('saved-query-management-popover'); - expect(descriptionText).to.eql( - 'Saved Queries\nThere are no saved queries. Save query text and filters that you want to use again.\nSave current query' - ); - }); - - it('should allow a query to be saved via the saved objects management component', async () => { - await savedQueryManagementComponent.saveNewQuery( - 'OkResponse', - '200 responses for .jpg over 24 hours', - true, - true - ); - await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse'); - await savedQueryManagementComponent.savedQueryTextExist('response:200'); - }); - - it('reinstates filters and the time filter when a saved query has filters and a time filter included', async () => { - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); - await savedQueryManagementComponent.loadSavedQuery('OkResponse'); - const timePickerValues = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); - expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true); - expect(timePickerValues.start).to.not.eql(PageObjects.timePicker.defaultStartTime); - expect(timePickerValues.end).to.not.eql(PageObjects.timePicker.defaultEndTime); - }); - - it('preserves the currently loaded query when the page is reloaded', async () => { - await browser.refresh(); - const timePickerValues = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); - expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true); - expect(timePickerValues.start).to.not.eql(PageObjects.timePicker.defaultStartTime); - expect(timePickerValues.end).to.not.eql(PageObjects.timePicker.defaultEndTime); - await retry.waitFor( - 'the right hit count', - async () => (await PageObjects.discover.getHitCount()) === '2,792' - ); - expect(await savedQueryManagementComponent.getCurrentlyLoadedQueryID()).to.be('OkResponse'); - }); - - it('allows saving changes to a currently loaded query via the saved query management component', async () => { - await queryBar.setQuery('response:404'); - await savedQueryManagementComponent.updateCurrentlyLoadedQuery('OkResponse', false, false); - await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse'); - await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); - expect(await queryBar.getQueryString()).to.eql(''); - await savedQueryManagementComponent.loadSavedQuery('OkResponse'); - expect(await queryBar.getQueryString()).to.eql('response:404'); - }); - - it('allows saving the currently loaded query as a new query', async () => { - await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( - 'OkResponseCopy', - '200 responses', - false, - false - ); - await savedQueryManagementComponent.savedQueryExistOrFail('OkResponseCopy'); - }); - - it('allows deleting the currently loaded saved query in the saved query management component and clears the query', async () => { - await savedQueryManagementComponent.deleteSavedQuery('OkResponseCopy'); - await savedQueryManagementComponent.savedQueryMissingOrFail('OkResponseCopy'); - expect(await queryBar.getQueryString()).to.eql(''); - }); - - it('does not allow saving a query with a non-unique name', async () => { - // this check allows this test to run stand alone, also should fix occacional flakiness - const savedQueryExists = await savedQueryManagementComponent.savedQueryExist('OkResponse'); - if (!savedQueryExists) { - await savedQueryManagementComponent.saveNewQuery( - 'OkResponse', - '200 responses for .jpg over 24 hours', - true, - true - ); - await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); - } - await savedQueryManagementComponent.saveNewQueryWithNameError('OkResponse'); - }); - - it('resets any changes to a loaded query on reloading the same saved query', async () => { - await savedQueryManagementComponent.loadSavedQuery('OkResponse'); - await queryBar.setQuery('response:503'); - await savedQueryManagementComponent.loadSavedQuery('OkResponse'); - expect(await queryBar.getQueryString()).to.eql('response:404'); - }); - - it('allows clearing the currently loaded saved query', async () => { - await savedQueryManagementComponent.loadSavedQuery('OkResponse'); - await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); - expect(await queryBar.getQueryString()).to.eql(''); - }); - - it('allows clearing if non default language was remembered in localstorage', async () => { - await queryBar.switchQueryLanguage('lucene'); - await PageObjects.common.navigateToApp('discover'); // makes sure discovered is reloaded without any state in url - await queryBar.expectQueryLanguageOrFail('lucene'); // make sure lucene is remembered after refresh (comes from localstorage) - await savedQueryManagementComponent.loadSavedQuery('OkResponse'); - await queryBar.expectQueryLanguageOrFail('kql'); - await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); - await queryBar.expectQueryLanguageOrFail('lucene'); - }); - - it('changing language removes saved query', async () => { - await savedQueryManagementComponent.loadSavedQuery('OkResponse'); - await queryBar.switchQueryLanguage('lucene'); - expect(await queryBar.getQueryString()).to.eql(''); - }); - }); - }); -} diff --git a/test/functional_ccs/services/index.ts b/test/functional_ccs/services/index.ts deleted file mode 100644 index dcdffa077fe08..0000000000000 --- a/test/functional_ccs/services/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { services as functionalServices } from '../../functional/services'; -import { RemoteEsProvider } from './remote_es'; -import { RemoteEsArchiverProvider } from './remote_es_archiver'; - -export const services = { - ...functionalServices, - remoteEs: RemoteEsProvider, - remoteEsArchiver: RemoteEsArchiverProvider, -}; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts index 67947691f7757..26f1be5fb97bc 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts @@ -6,7 +6,7 @@ */ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; -import { PaletteOutput } from 'src/plugins/charts/common'; +import type { PaletteOutput } from '@kbn/coloring'; import { Filter as DataFilter } from '@kbn/es-query'; import { TimeRange } from 'src/plugins/data/common'; import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts index 80830eac24021..e57f14d3884eb 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { PaletteRegistry } from 'src/plugins/charts/public'; +import type { PaletteRegistry } from '@kbn/coloring'; import { EmbeddableTypes, EmbeddableInput } from '../../expression_types'; import { toExpression as mapToExpression } from './input_type_to_expression/map'; import { toExpression as visualizationToExpression } from './input_type_to_expression/visualization'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts index 1d3a5eb9164b7..8f791668d75f9 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts @@ -6,7 +6,7 @@ */ import { toExpression as toExpressionString } from '@kbn/interpreter'; -import { PaletteRegistry } from 'src/plugins/charts/public'; +import type { PaletteRegistry } from '@kbn/coloring'; import { SavedLensInput } from '../../../functions/external/saved_lens'; export function toExpression(input: SavedLensInput, palettes: PaletteRegistry): string { diff --git a/x-pack/plugins/canvas/public/functions/index.ts b/x-pack/plugins/canvas/public/functions/index.ts index 7a3ca9a2619bb..ad91d6b98fa7f 100644 --- a/x-pack/plugins/canvas/public/functions/index.ts +++ b/x-pack/plugins/canvas/public/functions/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { PaletteRegistry } from 'src/plugins/charts/public'; +import type { PaletteRegistry } from '@kbn/coloring'; import { asset } from './asset'; import { filtersFunctionFactory } from './filters'; import { timelionFunctionFactory } from './timelion'; diff --git a/x-pack/plugins/canvas/public/functions/pie.ts b/x-pack/plugins/canvas/public/functions/pie.ts index 11e487273b31c..67df9d896350c 100644 --- a/x-pack/plugins/canvas/public/functions/pie.ts +++ b/x-pack/plugins/canvas/public/functions/pie.ts @@ -6,7 +6,7 @@ */ import { get, keyBy, map, groupBy } from 'lodash'; -import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; +import type { PaletteRegistry, PaletteOutput } from '@kbn/coloring'; import { getLegendConfig } from '../../common/lib/get_legend_config'; import { getFunctionHelp } from '../../i18n'; import { diff --git a/x-pack/plugins/canvas/public/functions/plot/index.ts b/x-pack/plugins/canvas/public/functions/plot/index.ts index 477c704190146..6f46ed0930400 100644 --- a/x-pack/plugins/canvas/public/functions/plot/index.ts +++ b/x-pack/plugins/canvas/public/functions/plot/index.ts @@ -8,7 +8,7 @@ import { set } from '@elastic/safer-lodash-set'; import { groupBy, get, keyBy, map, sortBy } from 'lodash'; import { ExpressionFunctionDefinition, Style } from 'src/plugins/expressions'; -import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; +import type { PaletteRegistry, PaletteOutput } from '@kbn/coloring'; import { getLegendConfig } from '../../../common/lib/get_legend_config'; import { getFlotAxisConfig } from './get_flot_axis_config'; import { getFontSpec } from './get_font_spec'; diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx index c8e656b8117eb..b1f5af86cf8ab 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx @@ -811,15 +811,16 @@ describe('AllCasesListGeneric', () => { }); it('should hide the alerts column if the alert feature is disabled', async () => { - expect.assertions(1); - - const { findAllByTestId } = render( + const result = render( ); - await expect(findAllByTestId('case-table-column-alertsCount')).rejects.toThrow(); + await waitFor(() => { + expect(result.getByTestId('cases-table')).toBeTruthy(); + expect(result.queryAllByTestId('case-table-column-alertsCount').length).toBe(0); + }); }); it('should show the alerts column if the alert feature is enabled', async () => { diff --git a/x-pack/plugins/cloud/public/fullstory.ts b/x-pack/plugins/cloud/public/fullstory.ts deleted file mode 100644 index 602b1c4cc63d3..0000000000000 --- a/x-pack/plugins/cloud/public/fullstory.ts +++ /dev/null @@ -1,90 +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 { sha256 } from 'js-sha256'; // loaded here to reduce page load bundle size when FullStory is disabled -import type { IBasePath, PackageInfo } from '../../../../src/core/public'; - -export interface FullStoryDeps { - basePath: IBasePath; - orgId: string; - packageInfo: PackageInfo; -} - -export type FullstoryUserVars = Record; -export type FullstoryVars = Record; - -export interface FullStoryApi { - identify(userId: string, userVars?: FullstoryUserVars): void; - setVars(pageName: string, vars?: FullstoryVars): void; - setUserVars(userVars?: FullstoryUserVars): void; - event(eventName: string, eventProperties: Record): void; -} - -export interface FullStoryService { - fullStory: FullStoryApi; - sha256: typeof sha256; -} - -export const initializeFullStory = ({ - basePath, - orgId, - packageInfo, -}: FullStoryDeps): FullStoryService => { - // @ts-expect-error - window._fs_debug = false; - // @ts-expect-error - window._fs_host = 'fullstory.com'; - // @ts-expect-error - window._fs_script = basePath.prepend(`/internal/cloud/${packageInfo.buildNum}/fullstory.js`); - // @ts-expect-error - window._fs_org = orgId; - // @ts-expect-error - window._fs_namespace = 'FSKibana'; - - /* eslint-disable */ - (function(m,n,e,t,l,o,g,y){ - if (e in m) {if(m.console && m.console.log) { m.console.log('FullStory namespace conflict. Please set window["_fs_namespace"].');} return;} - // @ts-expect-error - g=m[e]=function(a,b,s){g.q?g.q.push([a,b,s]):g._api(a,b,s);};g.q=[]; - // @ts-expect-error - o=n.createElement(t);o.async=1;o.crossOrigin='anonymous';o.src=_fs_script; - // @ts-expect-error - y=n.getElementsByTagName(t)[0];y.parentNode.insertBefore(o,y); - // @ts-expect-error - g.identify=function(i,v,s){g(l,{uid:i},s);if(v)g(l,v,s)};g.setUserVars=function(v,s){g(l,v,s)};g.event=function(i,v,s){g('event',{n:i,p:v},s)}; - // @ts-expect-error - g.anonymize=function(){g.identify(!!0)}; - // @ts-expect-error - g.shutdown=function(){g("rec",!1)};g.restart=function(){g("rec",!0)}; - // @ts-expect-error - g.log = function(a,b){g("log",[a,b])}; - // @ts-expect-error - g.consent=function(a){g("consent",!arguments.length||a)}; - // @ts-expect-error - g.identifyAccount=function(i,v){o='account';v=v||{};v.acctId=i;g(o,v)}; - // @ts-expect-error - g.clearUserCookie=function(){}; - // @ts-expect-error - g.setVars=function(n, p){g('setVars',[n,p]);}; - // @ts-expect-error - g._w={};y='XMLHttpRequest';g._w[y]=m[y];y='fetch';g._w[y]=m[y]; - // @ts-expect-error - if(m[y])m[y]=function(){return g._w[y].apply(this,arguments)}; - // @ts-expect-error - g._v="1.3.0"; - // @ts-expect-error - })(window,document,window['_fs_namespace'],'script','user'); - /* eslint-enable */ - - // @ts-expect-error - const fullStory: FullStoryApi = window.FSKibana; - - return { - fullStory, - sha256, - }; -}; diff --git a/x-pack/plugins/cloud/public/plugin.test.mocks.ts b/x-pack/plugins/cloud/public/plugin.test.mocks.ts deleted file mode 100644 index 1c185d0194912..0000000000000 --- a/x-pack/plugins/cloud/public/plugin.test.mocks.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { sha256 } from 'js-sha256'; -import type { FullStoryDeps, FullStoryApi, FullStoryService } from './fullstory'; - -export const fullStoryApiMock: jest.Mocked = { - event: jest.fn(), - setUserVars: jest.fn(), - setVars: jest.fn(), - identify: jest.fn(), -}; -export const initializeFullStoryMock = jest.fn(() => ({ - fullStory: fullStoryApiMock, - sha256, -})); -jest.doMock('./fullstory', () => { - return { initializeFullStory: initializeFullStoryMock }; -}); diff --git a/x-pack/plugins/cloud/public/plugin.test.ts b/x-pack/plugins/cloud/public/plugin.test.ts index edbf724e25390..fd269245fd775 100644 --- a/x-pack/plugins/cloud/public/plugin.test.ts +++ b/x-pack/plugins/cloud/public/plugin.test.ts @@ -9,14 +9,13 @@ import { nextTick } from '@kbn/test-jest-helpers'; import { coreMock } from 'src/core/public/mocks'; import { homePluginMock } from 'src/plugins/home/public/mocks'; import { securityMock } from '../../security/public/mocks'; -import { fullStoryApiMock, initializeFullStoryMock } from './plugin.test.mocks'; -import { CloudPlugin, CloudConfigType, loadFullStoryUserId } from './plugin'; -import { Observable, Subject } from 'rxjs'; +import { CloudPlugin, CloudConfigType, loadUserId } from './plugin'; +import { firstValueFrom, Observable, Subject } from 'rxjs'; import { KibanaExecutionContext } from 'kibana/public'; describe('Cloud Plugin', () => { describe('#setup', () => { - describe('setupFullstory', () => { + describe('setupFullStory', () => { beforeEach(() => { jest.clearAllMocks(); }); @@ -65,72 +64,82 @@ describe('Cloud Plugin', () => { ); const setup = plugin.setup(coreSetup, securityEnabled ? { security: securitySetup } : {}); - // Wait for fullstory dynamic import to resolve + // Wait for FullStory dynamic import to resolve await new Promise((r) => setImmediate(r)); - return { initContext, plugin, setup }; + return { initContext, plugin, setup, coreSetup }; }; - it('calls initializeFullStory with correct args when enabled and org_id are set', async () => { - const { initContext } = await setupPlugin({ + test('register the shipper FullStory with correct args when enabled and org_id are set', async () => { + const { coreSetup } = await setupPlugin({ config: { full_story: { enabled: true, org_id: 'foo' } }, currentUserProps: { username: '1234', }, }); - expect(initializeFullStoryMock).toHaveBeenCalled(); - const { basePath, orgId, packageInfo } = initializeFullStoryMock.mock.calls[0][0]; - expect(basePath.prepend).toBeDefined(); - expect(orgId).toEqual('foo'); - expect(packageInfo).toEqual(initContext.env.packageInfo); + expect(coreSetup.analytics.registerShipper).toHaveBeenCalled(); + expect(coreSetup.analytics.registerShipper).toHaveBeenCalledWith(expect.anything(), { + fullStoryOrgId: 'foo', + scriptUrl: '/internal/cloud/100/fullstory.js', + namespace: 'FSKibana', + }); }); - it('calls FS.identify with hashed user ID when security is available', async () => { - await setupPlugin({ + test('register the context provider for the cloud user with hashed user ID when security is available', async () => { + const { coreSetup } = await setupPlugin({ config: { full_story: { enabled: true, org_id: 'foo' } }, currentUserProps: { username: '1234', }, }); - expect(fullStoryApiMock.identify).toHaveBeenCalledWith( - '5ef112cfdae3dea57097bc276e275b2816e73ef2a398dc0ffaf5b6b4e3af2041', - { - version_str: 'version', - version_major_int: -1, - version_minor_int: -1, - version_patch_int: -1, - org_id_str: 'cloudId', - } - ); + expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled(); + + const [{ context$ }] = coreSetup.analytics.registerContextProvider.mock.calls.find( + ([{ name }]) => name === 'cloud_user_id' + )!; + + await expect(firstValueFrom(context$)).resolves.toEqual({ + userId: '5ef112cfdae3dea57097bc276e275b2816e73ef2a398dc0ffaf5b6b4e3af2041', + }); }); it('user hash includes org id', async () => { - await setupPlugin({ + const { coreSetup: coreSetup1 } = await setupPlugin({ config: { full_story: { enabled: true, org_id: 'foo' }, id: 'esOrg1' }, currentUserProps: { username: '1234', }, }); - const hashId1 = fullStoryApiMock.identify.mock.calls[0][0]; + const [{ context$: context1$ }] = + coreSetup1.analytics.registerContextProvider.mock.calls.find( + ([{ name }]) => name === 'cloud_user_id' + )!; + + const hashId1 = await firstValueFrom(context1$); - await setupPlugin({ + const { coreSetup: coreSetup2 } = await setupPlugin({ config: { full_story: { enabled: true, org_id: 'foo' }, id: 'esOrg2' }, currentUserProps: { username: '1234', }, }); - const hashId2 = fullStoryApiMock.identify.mock.calls[1][0]; + const [{ context$: context2$ }] = + coreSetup2.analytics.registerContextProvider.mock.calls.find( + ([{ name }]) => name === 'cloud_user_id' + )!; + + const hashId2 = await firstValueFrom(context2$); expect(hashId1).not.toEqual(hashId2); }); - it('calls FS.setVars everytime an app changes', async () => { + it('emits the execution context provider everytime an app changes', async () => { const currentContext$ = new Subject(); - const { plugin } = await setupPlugin({ + const { coreSetup } = await setupPlugin({ config: { full_story: { enabled: true, org_id: 'foo' } }, currentUserProps: { username: '1234', @@ -138,23 +147,34 @@ describe('Cloud Plugin', () => { currentContext$, }); + const [{ context$ }] = coreSetup.analytics.registerContextProvider.mock.calls.find( + ([{ name }]) => name === 'execution_context' + )!; + + let latestContext; + context$.subscribe((context) => { + latestContext = context; + }); + // takes the app name - expect(fullStoryApiMock.setVars).not.toHaveBeenCalled(); + expect(latestContext).toBeUndefined(); currentContext$.next({ name: 'App1', description: '123', }); - expect(fullStoryApiMock.setVars).toHaveBeenCalledWith('page', { + await new Promise((r) => setImmediate(r)); + + expect(latestContext).toEqual({ pageName: 'App1', - app_id_str: 'App1', + applicationId: 'App1', }); // context clear currentContext$.next({}); - expect(fullStoryApiMock.setVars).toHaveBeenCalledWith('page', { - pageName: 'App1', - app_id_str: 'App1', + expect(latestContext).toEqual({ + pageName: '', + applicationId: 'unknown', }); // different app @@ -163,11 +183,11 @@ describe('Cloud Plugin', () => { page: 'page2', id: '123', }); - expect(fullStoryApiMock.setVars).toHaveBeenCalledWith('page', { + expect(latestContext).toEqual({ pageName: 'App2:page2', - app_id_str: 'App2', - page_str: 'page2', - ent_id_str: '123', + applicationId: 'App2', + page: 'page2', + entityId: '123', }); // Back to first app @@ -177,25 +197,25 @@ describe('Cloud Plugin', () => { id: '123', }); - expect(fullStoryApiMock.setVars).toHaveBeenCalledWith('page', { + expect(latestContext).toEqual({ pageName: 'App1:page3', - app_id_str: 'App1', - page_str: 'page3', - ent_id_str: '123', + applicationId: 'App1', + page: 'page3', + entityId: '123', }); - - expect(currentContext$.observers.length).toBe(1); - plugin.stop(); - expect(currentContext$.observers.length).toBe(0); }); - it('does not call FS.identify when security is not available', async () => { - await setupPlugin({ + it('does not register the cloud user id context provider when security is not available', async () => { + const { coreSetup } = await setupPlugin({ config: { full_story: { enabled: true, org_id: 'foo' } }, securityEnabled: false, }); - expect(fullStoryApiMock.identify).not.toHaveBeenCalled(); + expect( + coreSetup.analytics.registerContextProvider.mock.calls.find( + ([{ name }]) => name === 'cloud_user_id' + ) + ).toBeUndefined(); }); describe('with memory', () => { @@ -219,58 +239,44 @@ describe('Cloud Plugin', () => { delete window.performance.memory; }); - it('calls FS.event when security is available', async () => { - const { initContext } = await setupPlugin({ + it('reports an event when security is available', async () => { + const { initContext, coreSetup } = await setupPlugin({ config: { full_story: { enabled: true, org_id: 'foo' } }, currentUserProps: { username: '1234', }, }); - expect(fullStoryApiMock.event).toHaveBeenCalledWith('Loaded Kibana', { - kibana_version_str: initContext.env.packageInfo.version, - memory_js_heap_size_limit_int: 3, - memory_js_heap_size_total_int: 2, - memory_js_heap_size_used_int: 1, + expect(coreSetup.analytics.reportEvent).toHaveBeenCalledWith('Loaded Kibana', { + kibana_version: initContext.env.packageInfo.version, + memory_js_heap_size_limit: 3, + memory_js_heap_size_total: 2, + memory_js_heap_size_used: 1, }); }); }); - it('calls FS.event when security is not available', async () => { - const { initContext } = await setupPlugin({ + it('reports an event when security is not available', async () => { + const { initContext, coreSetup } = await setupPlugin({ config: { full_story: { enabled: true, org_id: 'foo' } }, securityEnabled: false, }); - expect(fullStoryApiMock.event).toHaveBeenCalledWith('Loaded Kibana', { - kibana_version_str: initContext.env.packageInfo.version, - }); - }); - - it('calls FS.event when FS.identify throws an error', async () => { - fullStoryApiMock.identify.mockImplementationOnce(() => { - throw new Error(`identify failed!`); - }); - const { initContext } = await setupPlugin({ - config: { full_story: { enabled: true, org_id: 'foo' } }, - currentUserProps: { - username: '1234', - }, - }); - - expect(fullStoryApiMock.event).toHaveBeenCalledWith('Loaded Kibana', { - kibana_version_str: initContext.env.packageInfo.version, + expect(coreSetup.analytics.reportEvent).toHaveBeenCalledWith('Loaded Kibana', { + kibana_version: initContext.env.packageInfo.version, }); }); it('does not call initializeFullStory when enabled=false', async () => { - await setupPlugin({ config: { full_story: { enabled: false, org_id: 'foo' } } }); - expect(initializeFullStoryMock).not.toHaveBeenCalled(); + const { coreSetup } = await setupPlugin({ + config: { full_story: { enabled: false, org_id: 'foo' } }, + }); + expect(coreSetup.analytics.registerShipper).not.toHaveBeenCalled(); }); it('does not call initializeFullStory when org_id is undefined', async () => { - await setupPlugin({ config: { full_story: { enabled: true } } }); - expect(initializeFullStoryMock).not.toHaveBeenCalled(); + const { coreSetup } = await setupPlugin({ config: { full_story: { enabled: true } } }); + expect(coreSetup.analytics.registerShipper).not.toHaveBeenCalled(); }); }); @@ -659,7 +665,7 @@ describe('Cloud Plugin', () => { it('returns principal ID when username specified', async () => { expect( - await loadFullStoryUserId({ + await loadUserId({ getCurrentUser: jest.fn().mockResolvedValue({ username: '1234', }), @@ -670,7 +676,7 @@ describe('Cloud Plugin', () => { it('returns undefined if getCurrentUser throws', async () => { expect( - await loadFullStoryUserId({ + await loadUserId({ getCurrentUser: jest.fn().mockRejectedValue(new Error(`Oh no!`)), }) ).toBeUndefined(); @@ -678,7 +684,7 @@ describe('Cloud Plugin', () => { it('returns undefined if getCurrentUser returns undefined', async () => { expect( - await loadFullStoryUserId({ + await loadUserId({ getCurrentUser: jest.fn().mockResolvedValue(undefined), }) ).toBeUndefined(); @@ -686,7 +692,7 @@ describe('Cloud Plugin', () => { it('returns undefined and logs if username undefined', async () => { expect( - await loadFullStoryUserId({ + await loadUserId({ getCurrentUser: jest.fn().mockResolvedValue({ username: undefined, metadata: { foo: 'bar' }, @@ -694,7 +700,7 @@ describe('Cloud Plugin', () => { }) ).toBeUndefined(); expect(consoleMock).toHaveBeenLastCalledWith( - `[cloud.full_story] username not specified. User metadata: {"foo":"bar"}` + `[cloud.analytics] username not specified. User metadata: {"foo":"bar"}` ); }); }); diff --git a/x-pack/plugins/cloud/public/plugin.tsx b/x-pack/plugins/cloud/public/plugin.tsx index 89f24971de25c..b524116c25ec9 100644 --- a/x-pack/plugins/cloud/public/plugin.tsx +++ b/x-pack/plugins/cloud/public/plugin.tsx @@ -6,7 +6,7 @@ */ import React, { FC } from 'react'; -import { +import type { CoreSetup, CoreStart, Plugin, @@ -14,11 +14,14 @@ import { HttpStart, IBasePath, ExecutionContextStart, + AnalyticsServiceSetup, } from 'src/core/public'; import { i18n } from '@kbn/i18n'; import useObservable from 'react-use/lib/useObservable'; -import { BehaviorSubject, Subscription } from 'rxjs'; -import { compact, isUndefined, omitBy } from 'lodash'; +import { BehaviorSubject, from, of, Subscription } from 'rxjs'; +import { exhaustMap, filter, map } from 'rxjs/operators'; +import { compact } from 'lodash'; + import type { AuthenticatedUser, SecurityPluginSetup, @@ -83,9 +86,13 @@ export interface CloudSetup { isCloudEnabled: boolean; } -interface SetupFullstoryDeps extends CloudSetupDependencies { - executionContextPromise?: Promise; +interface SetupFullStoryDeps { + analytics: AnalyticsServiceSetup; basePath: IBasePath; +} +interface SetupTelemetryContextDeps extends CloudSetupDependencies { + analytics: AnalyticsServiceSetup; + executionContextPromise: Promise; esOrgId?: string; } @@ -94,7 +101,7 @@ interface SetupChatDeps extends Pick { } export class CloudPlugin implements Plugin { - private config!: CloudConfigType; + private readonly config: CloudConfigType; private isCloudEnabled: boolean; private appSubscription?: Subscription; private chatConfig$ = new BehaviorSubject({ enabled: false }); @@ -109,12 +116,17 @@ export class CloudPlugin implements Plugin { return coreStart.executionContext; }); - this.setupFullstory({ - basePath: core.http.basePath, + this.setupTelemetryContext({ + analytics: core.analytics, security, executionContextPromise, esOrgId: this.config.id, - }).catch((e) => + }).catch((e) => { + // eslint-disable-next-line no-console + console.debug(`Error setting up TelemetryContext: ${e.toString()}`); + }); + + this.setupFullStory({ analytics: core.analytics, basePath: core.http.basePath }).catch((e) => // eslint-disable-next-line no-console console.debug(`Error setting up FullStory: ${e.toString()}`) ); @@ -230,109 +242,158 @@ export class CloudPlugin implements Plugin { return user?.roles.includes('superuser') ?? true; } - private async setupFullstory({ - basePath, + /** + * If the right config is provided, register the FullStory shipper to the analytics client. + * @param analytics Core's Analytics service's setup contract. + * @param basePath Core's http.basePath helper. + * @private + */ + private async setupFullStory({ analytics, basePath }: SetupFullStoryDeps) { + const { enabled, org_id: fullStoryOrgId } = this.config.full_story; + if (!enabled || !fullStoryOrgId) { + return; // do not load any FullStory code in the browser if not enabled + } + + // Keep this import async so that we do not load any FullStory code into the browser when it is disabled. + const { FullStoryShipper } = await import('@elastic/analytics'); + analytics.registerShipper(FullStoryShipper, { + fullStoryOrgId, + // Load an Elastic-internally audited script. Ideally, it should be hosted on a CDN. + scriptUrl: basePath.prepend( + `/internal/cloud/${this.initializerContext.env.packageInfo.buildNum}/fullstory.js` + ), + namespace: 'FSKibana', + }); + } + + /** + * Set up the Analytics context providers. + * @param analytics Core's Analytics service. The Setup contract. + * @param security The security plugin. + * @param executionContextPromise Core's executionContext's start contract. + * @param esOrgId The Cloud Org ID. + * @private + */ + private async setupTelemetryContext({ + analytics, security, executionContextPromise, esOrgId, - }: SetupFullstoryDeps) { - const { enabled, org_id: fsOrgId } = this.config.full_story; - if (!enabled || !fsOrgId) { - return; // do not load any fullstory code in the browser if not enabled - } + }: SetupTelemetryContextDeps) { + // Some context providers can be moved to other places for better domain isolation. + // Let's use https://github.com/elastic/kibana/issues/125690 for that purpose. + analytics.registerContextProvider({ + name: 'kibana_version', + context$: of({ version: this.initializerContext.env.packageInfo.version }), + schema: { version: { type: 'keyword', _meta: { description: 'The version of Kibana' } } }, + }); - // Keep this import async so that we do not load any FullStory code into the browser when it is disabled. - const fullStoryChunkPromise = import('./fullstory'); - const userIdPromise: Promise = security - ? loadFullStoryUserId({ getCurrentUser: security.authc.getCurrentUser }) - : Promise.resolve(undefined); - - // We need to call FS.identify synchronously after FullStory is initialized, so we must load the user upfront - const [{ initializeFullStory }, userId] = await Promise.all([ - fullStoryChunkPromise, - userIdPromise, - ]); - - const { fullStory, sha256 } = initializeFullStory({ - basePath, - orgId: fsOrgId, - packageInfo: this.initializerContext.env.packageInfo, + analytics.registerContextProvider({ + name: 'cloud_org_id', + context$: of({ esOrgId }), + schema: { + esOrgId: { + type: 'keyword', + _meta: { description: 'The Cloud Organization ID', optional: true }, + }, + }, }); - // Very defensive try/catch to avoid any UnhandledPromiseRejections - try { - // This needs to be called syncronously to be sure that we populate the user ID soon enough to make sessions merging - // across domains work - if (userId) { - // Join the cloud org id and the user to create a truly unique user id. - // The hashing here is to keep it at clear as possible in our source code that we do not send literal user IDs - const hashedId = sha256(esOrgId ? `${esOrgId}:${userId}` : `${userId}`); - - executionContextPromise - ?.then(async (executionContext) => { - this.appSubscription = executionContext.context$.subscribe((context) => { - const { name, page, id } = context; - // Update the current context every time it changes - fullStory.setVars( - 'page', - omitBy( - { - // Read about the special pageName property - // https://help.fullstory.com/hc/en-us/articles/1500004101581-FS-setVars-API-Sending-custom-page-data-to-FullStory - pageName: `${compact([name, page]).join(':')}`, - app_id_str: name ?? 'unknown', - page_str: page, - ent_id_str: id, - }, - isUndefined - ) - ); - }); + // This needs to be called synchronously to be sure that we populate the user ID soon enough to make sessions merging + // across domains work + if (security) { + analytics.registerContextProvider({ + name: 'cloud_user_id', + context$: from(loadUserId({ getCurrentUser: security.authc.getCurrentUser })).pipe( + filter((userId): userId is string => Boolean(userId)), + exhaustMap(async (userId) => { + const { sha256 } = await import('js-sha256'); + // Join the cloud org id and the user to create a truly unique user id. + // The hashing here is to keep it at clear as possible in our source code that we do not send literal user IDs + return { userId: sha256(esOrgId ? `${esOrgId}:${userId}` : `${userId}`) }; }) - .catch((e) => { - // eslint-disable-next-line no-console - console.error( - `[cloud.full_story] Could not retrieve application service due to error: ${e.toString()}`, - e - ); - }); - const kibanaVer = this.initializerContext.env.packageInfo.version; - // TODO: use semver instead - const parsedVer = (kibanaVer.indexOf('.') > -1 ? kibanaVer.split('.') : []).map((s) => - parseInt(s, 10) - ); - // `str` suffix is required for evn vars, see docs: https://help.fullstory.com/hc/en-us/articles/360020623234 - fullStory.identify(hashedId, { - version_str: kibanaVer, - version_major_int: parsedVer[0] ?? -1, - version_minor_int: parsedVer[1] ?? -1, - version_patch_int: parsedVer[2] ?? -1, - org_id_str: esOrgId, - }); - } - } catch (e) { - // eslint-disable-next-line no-console - console.error( - `[cloud.full_story] Could not call FS.identify due to error: ${e.toString()}`, - e - ); + ), + schema: { + userId: { + type: 'keyword', + _meta: { description: 'The user id scoped as seen by Cloud (hashed)' }, + }, + }, + }); } + const executionContext = await executionContextPromise; + analytics.registerContextProvider({ + name: 'execution_context', + context$: executionContext.context$.pipe( + // Update the current context every time it changes + map(({ name, page, id }) => ({ + pageName: `${compact([name, page]).join(':')}`, + applicationId: name ?? 'unknown', + page, + entityId: id, + })) + ), + schema: { + pageName: { + type: 'keyword', + _meta: { description: 'The name of the current page' }, + }, + page: { + type: 'keyword', + _meta: { description: 'The current page', optional: true }, + }, + applicationId: { + type: 'keyword', + _meta: { description: 'The id of the current application' }, + }, + entityId: { + type: 'keyword', + _meta: { + description: + 'The id of the current entity (dashboard, visualization, canvas, lens, etc)', + optional: true, + }, + }, + }, + }); + + analytics.registerEventType({ + eventType: 'Loaded Kibana', + schema: { + kibana_version: { + type: 'keyword', + _meta: { description: 'The version of Kibana', optional: true }, + }, + memory_js_heap_size_limit: { + type: 'long', + _meta: { description: 'The maximum size of the heap', optional: true }, + }, + memory_js_heap_size_total: { + type: 'long', + _meta: { description: 'The total size of the heap', optional: true }, + }, + memory_js_heap_size_used: { + type: 'long', + _meta: { description: 'The used size of the heap', optional: true }, + }, + }, + }); + // Get performance information from the browser (non standard property // @ts-expect-error 2339 const memory = window.performance.memory; let memoryInfo = {}; if (memory) { memoryInfo = { - memory_js_heap_size_limit_int: memory.jsHeapSizeLimit, - memory_js_heap_size_total_int: memory.totalJSHeapSize, - memory_js_heap_size_used_int: memory.usedJSHeapSize, + memory_js_heap_size_limit: memory.jsHeapSizeLimit, + memory_js_heap_size_total: memory.totalJSHeapSize, + memory_js_heap_size_used: memory.usedJSHeapSize, }; } - // Record an event that Kibana was opened so we can easily search for sessions that use Kibana - fullStory.event('Loaded Kibana', { - // `str` suffix is required, see docs: https://help.fullstory.com/hc/en-us/articles/360020623234 - kibana_version_str: this.initializerContext.env.packageInfo.version, + + analytics.reportEvent('Loaded Kibana', { + kibana_version: this.initializerContext.env.packageInfo.version, ...memoryInfo, }); } @@ -376,7 +437,7 @@ export class CloudPlugin implements Plugin { } /** @internal exported for testing */ -export const loadFullStoryUserId = async ({ +export const loadUserId = async ({ getCurrentUser, }: { getCurrentUser: () => Promise; @@ -391,7 +452,7 @@ export const loadFullStoryUserId = async ({ if (!currentUser.username) { // eslint-disable-next-line no-console console.debug( - `[cloud.full_story] username not specified. User metadata: ${JSON.stringify( + `[cloud.analytics] username not specified. User metadata: ${JSON.stringify( currentUser.metadata )}` ); @@ -400,7 +461,7 @@ export const loadFullStoryUserId = async ({ return currentUser.username; } catch (e) { // eslint-disable-next-line no-console - console.error(`[cloud.full_story] Error loading the current user: ${e.toString()}`, e); + console.error(`[cloud.analytics] Error loading the current user: ${e.toString()}`, e); return undefined; } }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/cloud_posture_score_chart.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/cloud_posture_score_chart.tsx index dd872b49aec62..540402b986e5b 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/cloud_posture_score_chart.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/cloud_posture_score_chart.tsx @@ -23,8 +23,8 @@ import { FormattedDate, FormattedTime } from '@kbn/i18n-react'; import moment from 'moment'; import { statusColors } from '../../../common/constants'; import type { PostureTrend, Stats } from '../../../../common/types'; -import * as TEXT from '../translations'; import { CompactFormattedNumber } from '../../../components/compact_formatted_number'; +import { RULE_FAILED, RULE_PASSED } from '../../../../common/constants'; interface CloudPostureScoreChartProps { trend: PostureTrend[]; @@ -41,8 +41,8 @@ const ScoreChart = ({ partitionOnElementClick, }: Omit) => { const data = [ - { label: TEXT.PASSED, value: totalPassed }, - { label: TEXT.FAILED, value: totalFailed }, + { label: RULE_PASSED, value: totalPassed }, + { label: RULE_FAILED, value: totalFailed }, ]; return ( @@ -69,7 +69,7 @@ const ScoreChart = ({ groupByRollup: (d: { label: string }) => d.label, shape: { fillColor: (d, index) => - d.dataName === 'Passed' ? statusColors.success : statusColors.danger, + d.dataName === RULE_PASSED ? statusColors.success : statusColors.danger, }, }, ]} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts index d6802c303807b..e78c1805c6ee7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts @@ -113,13 +113,6 @@ describe('RoleMappingsLogic', () => { }); }); - it('setRoleMappings', () => { - RoleMappingsLogic.actions.setRoleMappings({ roleMappings: [asRoleMapping] }); - - expect(RoleMappingsLogic.values.roleMappings).toEqual([asRoleMapping]); - expect(RoleMappingsLogic.values.dataLoading).toEqual(false); - }); - describe('setElasticsearchUser', () => { it('sets user', () => { RoleMappingsLogic.actions.setElasticsearchUser(elasticsearchUsers[0]); @@ -323,7 +316,10 @@ describe('RoleMappingsLogic', () => { describe('listeners', () => { describe('enableRoleBasedAccess', () => { it('calls API and sets values', async () => { - const setRoleMappingsSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappings'); + const initializeRoleMappingsSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'initializeRoleMappings' + ); http.post.mockReturnValue(Promise.resolve(mappingsServerProps)); RoleMappingsLogic.actions.enableRoleBasedAccess(); @@ -333,7 +329,7 @@ describe('RoleMappingsLogic', () => { '/internal/app_search/role_mappings/enable_role_based_access' ); await nextTick(); - expect(setRoleMappingsSpy).toHaveBeenCalledWith(mappingsServerProps); + expect(initializeRoleMappingsSpy).toHaveBeenCalled(); }); itShowsServerErrorAsFlashMessage(http.post, () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts index 39e7df34f2aca..0579eef8ea5fe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts @@ -47,9 +47,6 @@ const emptyUser = { username: '', email: '' } as ElasticsearchUser; interface RoleMappingsActions extends RoleMappingsBaseActions { setRoleMapping(roleMapping: ASRoleMapping): { roleMapping: ASRoleMapping }; setSingleUserRoleMapping(data?: UserMapping): { singleUserRoleMapping: UserMapping }; - setRoleMappings({ roleMappings }: { roleMappings: ASRoleMapping[] }): { - roleMappings: ASRoleMapping[]; - }; setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails; handleAccessAllEnginesChange(selected: boolean): { selected: boolean }; handleEngineSelectionChange(engineNames: string[]): { engineNames: string[] }; @@ -322,8 +319,8 @@ export const RoleMappingsLogic = kea(route); - actions.setRoleMappings(response); + await http.post<{ roleMappings: ASRoleMapping[] }>(route); + actions.initializeRoleMappings(); } catch (e) { flashAPIErrors(e); } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx index f0e81748a6561..349da846ae904 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx @@ -29,7 +29,7 @@ describe('ConfiguredSourcesList', () => { expect(wrapper.find('[data-test-subj="ConfiguredSourcesListItem"]')).toHaveLength(19); }); - it('does not show connect button for a connected external source', () => { + it('does show connect button for a connected external source', () => { const wrapper = shallow( { }} /> ); - expect(wrapper.find(EuiButtonEmptyTo)).toHaveLength(0); + expect(wrapper.find(EuiButtonEmptyTo)).toHaveLength(1); }); it('does show connect button for an unconnected external source', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx index 6e4e4dcec2ba0..bbec096ae07d8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx @@ -124,39 +124,32 @@ export const ConfiguredSourcesList: React.FC = ({ - { - // TODO: Remove this once external connectors are multi-tenant - // This prevents connecting more than one external content source - (serviceType !== 'external' || !connected) && - (((!isOrganization || (isOrganization && !accountContextOnly)) && ( - - {!connected - ? i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.configuredSources.connectButton', - { - defaultMessage: 'Connect', - } - ) - : i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.configuredSources.connectAnotherButton', - { - defaultMessage: 'Connect another', - } - )} - - )) || ( - - {ADD_SOURCE_ORG_SOURCES_TITLE} - - )) - } + {((!isOrganization || (isOrganization && !accountContextOnly)) && ( + + {!connected + ? i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.configuredSources.connectButton', + { + defaultMessage: 'Connect', + } + ) + : i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.configuredSources.connectAnotherButton', + { + defaultMessage: 'Connect another', + } + )} + + )) || ( + + {ADD_SOURCE_ORG_SOURCES_TITLE} + + )} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts index b3246171ee5a7..d14a2adea82b8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts @@ -118,13 +118,6 @@ describe('RoleMappingsLogic', () => { }); }); - it('setRoleMappings', () => { - RoleMappingsLogic.actions.setRoleMappings({ roleMappings: [wsRoleMapping] }); - - expect(RoleMappingsLogic.values.roleMappings).toEqual([wsRoleMapping]); - expect(RoleMappingsLogic.values.dataLoading).toEqual(false); - }); - describe('setElasticsearchUser', () => { it('sets user', () => { RoleMappingsLogic.actions.setElasticsearchUser(elasticsearchUsers[0]); @@ -283,7 +276,10 @@ describe('RoleMappingsLogic', () => { describe('listeners', () => { describe('enableRoleBasedAccess', () => { it('calls API and sets values', async () => { - const setRoleMappingsSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappings'); + const initializeRoleMappingsSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'initializeRoleMappings' + ); http.post.mockReturnValue(Promise.resolve(mappingsServerProps)); RoleMappingsLogic.actions.enableRoleBasedAccess(); @@ -293,7 +289,7 @@ describe('RoleMappingsLogic', () => { '/internal/workplace_search/org/role_mappings/enable_role_based_access' ); await nextTick(); - expect(setRoleMappingsSpy).toHaveBeenCalledWith(mappingsServerProps); + expect(initializeRoleMappingsSpy).toHaveBeenCalled(); }); itShowsServerErrorAsFlashMessage(http.post, () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts index ea01d0049992f..010ae01ab1a71 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts @@ -46,9 +46,6 @@ interface RoleMappingsActions extends RoleMappingsBaseActions { setDefaultGroup(availableGroups: RoleGroup[]): { availableGroups: RoleGroup[] }; setRoleMapping(roleMapping: WSRoleMapping): { roleMapping: WSRoleMapping }; setSingleUserRoleMapping(data?: UserMapping): { singleUserRoleMapping: UserMapping }; - setRoleMappings({ roleMappings }: { roleMappings: WSRoleMapping[] }): { - roleMappings: WSRoleMapping[]; - }; setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails; handleAllGroupsSelectionChange(selected: boolean): { selected: boolean }; handleGroupSelectionChange(groupIds: string[]): { groupIds: string[] }; @@ -322,10 +319,8 @@ export const RoleMappingsLogic = kea(route); - actions.setRoleMappings(response); + await http.post<{ roleMappings: WSRoleMapping[] }>(route); + actions.initializeRoleMappings(); } catch (e) { flashAPIErrors(e); } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.test.tsx index bd486f3c95641..2b1f51747edd1 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.test.tsx @@ -53,7 +53,7 @@ describe('EditOutputFlyout', () => { // Does not show logstash SSL inputs expect(utils.queryByLabelText('Client SSL certificate key')).toBeNull(); expect(utils.queryByLabelText('Client SSL certificate')).toBeNull(); - expect(utils.queryByLabelText('Server SSL certificate authorities')).toBeNull(); + expect(utils.queryByLabelText('Server SSL certificate authorities (optional)')).toBeNull(); }); it('should render the flyout if the output provided is a logstash output', async () => { @@ -68,7 +68,7 @@ describe('EditOutputFlyout', () => { // Show logstash SSL inputs expect(utils.queryByLabelText('Client SSL certificate key')).not.toBeNull(); expect(utils.queryByLabelText('Client SSL certificate')).not.toBeNull(); - expect(utils.queryByLabelText('Server SSL certificate authorities')).not.toBeNull(); + expect(utils.queryByLabelText('Server SSL certificate authorities (optional)')).not.toBeNull(); }); it('should show a callout in the flyout if the selected output is logstash and no encrypted key is set', async () => { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx index c0b8f158f43de..09bd7d2280756 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx @@ -260,7 +260,7 @@ export const EditOutputFlyout: React.FunctionComponent = label={i18n.translate( 'xpack.fleet.settings.editOutputFlyout.sslCertificateAuthoritiesInputLabel', { - defaultMessage: 'Server SSL certificate authorities', + defaultMessage: 'Server SSL certificate authorities (optional)', } )} multiline={true} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form.tsx index afe96713f065d..ac196576ba889 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form.tsx @@ -77,7 +77,14 @@ function validateFleetServerHosts(value: string[]) { const res: Array<{ message: string; index: number }> = []; const hostIndexes: { [key: string]: number[] } = {}; value.forEach((val, idx) => { - if (!val.match(URL_REGEX)) { + if (!val) { + res.push({ + message: i18n.translate('xpack.fleet.settings.fleetServerHostsRequiredError', { + defaultMessage: 'Host URL is required', + }), + index: idx, + }); + } else if (!val.match(URL_REGEX)) { res.push({ message: i18n.translate('xpack.fleet.settings.fleetServerHostsError', { defaultMessage: 'Invalid URL', diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/logstash_instructions/helpers.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/logstash_instructions/helpers.tsx index 1239233b4e12d..9fbeb6eb120c8 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/logstash_instructions/helpers.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/logstash_instructions/helpers.tsx @@ -6,7 +6,7 @@ */ export const LOGSTASH_CONFIG_PIPELINES = `- pipeline.id: elastic-agent-pipeline - path.config: "/etc/path/to/elastic-agent-pipeline.config" + path.config: "/etc/path/to/elastic-agent-pipeline.conf" `; export function getLogstashPipeline(apiKey?: string) { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/logstash_instructions/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/logstash_instructions/index.tsx index 1fdffd1c8a30b..e87e0049bde89 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/logstash_instructions/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/logstash_instructions/index.tsx @@ -12,6 +12,7 @@ import { EuiButton, EuiSpacer, EuiLink, + EuiCode, EuiCodeBlock, EuiCopy, EuiButtonIcon, @@ -150,7 +151,10 @@ const LogstashInstructionSteps = () => { <> pipelines.yml, + }} /> @@ -164,7 +168,10 @@ const LogstashInstructionSteps = () => { <> elastic-agent-pipeline.conf, + }} /> diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts index efeac499bf953..2db6cfa848837 100644 --- a/x-pack/plugins/fleet/server/index.ts +++ b/x-pack/plugins/fleet/server/index.ts @@ -39,6 +39,7 @@ export type { PostPackagePolicyDeleteCallback, PostPackagePolicyCreateCallback, FleetRequestHandlerContext, + PostPackagePolicyPostCreateCallback, } from './types'; export { AgentNotFoundError, FleetUnauthorizedError } from './errors'; diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts index 25d0f5c577667..85f048669b53b 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts @@ -263,6 +263,38 @@ describe('When calling package policy', () => { }); }); }); + + describe('postCreate callback registration', () => { + it('should call to packagePolicyCreate and packagePolicyPostCreate call backs', async () => { + const request = getCreateKibanaRequest(); + await routeHandler(context, request, response); + + expect(response.ok).toHaveBeenCalled(); + expect(packagePolicyService.runExternalCallbacks).toBeCalledTimes(2); + + const firstCB = packagePolicyServiceMock.runExternalCallbacks.mock.calls[0][0]; + const secondCB = packagePolicyServiceMock.runExternalCallbacks.mock.calls[1][0]; + + expect(firstCB).toEqual('packagePolicyCreate'); + expect(secondCB).toEqual('packagePolicyPostCreate'); + }); + + it('should not call packagePolicyPostCreate call back in case of packagePolicy create failed', async () => { + const request = getCreateKibanaRequest(); + + packagePolicyServiceMock.create.mockImplementationOnce( + async (soClient, esClient, newData) => { + throw new Error('foo'); + } + ); + + await routeHandler(context, request, response); + const firstCB = packagePolicyServiceMock.runExternalCallbacks.mock.calls[0][0]; + + expect(firstCB).toEqual('packagePolicyCreate'); + expect(packagePolicyService.runExternalCallbacks).toBeCalledTimes(1); + }); + }); }); describe('update api handler', () => { diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts index de2045ff3e47c..9ee7ac02840a4 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts @@ -110,7 +110,16 @@ export const createPackagePolicyHandler: FleetRequestHandler< force, spaceId, }); - const body: CreatePackagePolicyResponse = { item: packagePolicy }; + + const enrichedPackagePolicy = await packagePolicyService.runExternalCallbacks( + 'packagePolicyPostCreate', + packagePolicy, + context, + request + ); + + const body: CreatePackagePolicyResponse = { item: enrichedPackagePolicy }; + return response.ok({ body, }); diff --git a/x-pack/plugins/fleet/server/services/app_context.ts b/x-pack/plugins/fleet/server/services/app_context.ts index 3adace700f796..20f35af8650ce 100644 --- a/x-pack/plugins/fleet/server/services/app_context.ts +++ b/x-pack/plugins/fleet/server/services/app_context.ts @@ -29,6 +29,7 @@ import type { ExternalCallbacksStorage, PostPackagePolicyCreateCallback, PostPackagePolicyDeleteCallback, + PostPackagePolicyPostCreateCallback, PutPackagePolicyUpdateCallback, } from '../types'; import type { FleetAppContext } from '../plugin'; @@ -183,6 +184,8 @@ class AppContextService { ? PostPackagePolicyCreateCallback : T extends 'postPackagePolicyDelete' ? PostPackagePolicyDeleteCallback + : T extends 'packagePolicyPostCreate' + ? PostPackagePolicyPostCreateCallback : PutPackagePolicyUpdateCallback > | undefined { @@ -192,6 +195,8 @@ class AppContextService { ? PostPackagePolicyCreateCallback : T extends 'postPackagePolicyDelete' ? PostPackagePolicyDeleteCallback + : T extends 'packagePolicyPostCreate' + ? PostPackagePolicyPostCreateCallback : PutPackagePolicyUpdateCallback >; } diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts index 2fde004048a36..fc25f4c0ed23b 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -26,6 +26,7 @@ import type { RegistryDataStream, PackagePolicyInputStream, PackagePolicy, + PostPackagePolicyPostCreateCallback, } from '../types'; import { createPackagePolicyMock } from '../../common/mocks'; @@ -1356,6 +1357,100 @@ describe('Package policy service', () => { }); }); + describe('runPostPackagePolicyPostCreateCallback', () => { + let context: ReturnType; + let request: KibanaRequest; + const packagePolicy = { + id: '93ac25fe-0467-4fcc-a3c5-57a26a8496e2', + version: 'WzYyMzcsMV0=', + name: 'my-cis_kubernetes_benchmark', + namespace: 'default', + description: '', + package: { + name: 'cis_kubernetes_benchmark', + title: 'CIS Kubernetes Benchmark', + version: '0.0.3', + }, + enabled: true, + policy_id: '1e6d0690-b995-11ec-a355-d35391e25881', + output_id: '', + inputs: [ + { + type: 'cloudbeat', + policy_template: 'findings', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { + type: 'logs', + dataset: 'cis_kubernetes_benchmark.findings', + }, + id: 'cloudbeat-cis_kubernetes_benchmark.findings-66b402b3-f24a-4018-b3d0-b88582a836ab', + compiled_stream: { + processors: [ + { + add_cluster_id: null, + }, + ], + }, + }, + ], + }, + ], + vars: { + dataYaml: { + type: 'yaml', + }, + }, + elasticsearch: undefined, + revision: 1, + created_at: '2022-04-11T12:44:43.385Z', + created_by: 'elastic', + updated_at: '2022-04-11T12:44:43.385Z', + updated_by: 'elastic', + }; + const callbackCallingOrder: string[] = []; + + beforeEach(() => { + context = xpackMocks.createRequestHandlerContext(); + request = httpServerMock.createKibanaRequest(); + appContextService.start(createAppContextStartContractMock()); + }); + + afterEach(() => { + appContextService.stop(); + jest.clearAllMocks(); + callbackCallingOrder.length = 0; + }); + + it('should execute PostPackagePolicyPostCreateCallback external callbacks', async () => { + const callbackA: PostPackagePolicyPostCreateCallback = jest.fn(async (ds) => { + callbackCallingOrder.push('a'); + return ds; + }); + + const callbackB: PostPackagePolicyPostCreateCallback = jest.fn(async (ds) => { + callbackCallingOrder.push('b'); + return ds; + }); + + appContextService.addExternalCallback('packagePolicyPostCreate', callbackA); + appContextService.addExternalCallback('packagePolicyPostCreate', callbackB); + + await packagePolicyService.runExternalCallbacks( + 'packagePolicyPostCreate', + packagePolicy, + context, + request + ); + + expect(callbackA).toHaveBeenCalledWith(packagePolicy, context, request); + expect(callbackB).toHaveBeenCalledWith(packagePolicy, context, request); + expect(callbackCallingOrder).toEqual(['a', 'b']); + }); + }); + describe('preconfigurePackageInputs', () => { describe('when variable is already defined', () => { it('override original variable value', () => { diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index a6c67ff529a85..ba3a8027dc746 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -53,13 +53,15 @@ import { PackagePolicyIneligibleForUpgradeError, PackagePolicyValidationError, } from '../errors'; -import { NewPackagePolicySchema, UpdatePackagePolicySchema } from '../types'; +import { NewPackagePolicySchema, PackagePolicySchema, UpdatePackagePolicySchema } from '../types'; import type { NewPackagePolicy, UpdatePackagePolicy, PackagePolicy, PackagePolicySOAttributes, DryRunPackagePolicy, + PostPackagePolicyCreateCallback, + PostPackagePolicyPostCreateCallback, } from '../types'; import type { ExternalCallback } from '..'; @@ -869,16 +871,24 @@ class PackagePolicyService implements PackagePolicyServiceInterface { externalCallbackType: A, packagePolicy: A extends 'postPackagePolicyDelete' ? DeletePackagePoliciesResponse + : A extends 'packagePolicyPostCreate' + ? PackagePolicy : NewPackagePolicy, context: RequestHandlerContext, request: KibanaRequest - ): Promise; + ): Promise< + A extends 'postPackagePolicyDelete' + ? void + : A extends 'packagePolicyPostCreate' + ? PackagePolicy + : NewPackagePolicy + >; public async runExternalCallbacks( externalCallbackType: ExternalCallback[0], - packagePolicy: NewPackagePolicy | DeletePackagePoliciesResponse, + packagePolicy: PackagePolicy | NewPackagePolicy | DeletePackagePoliciesResponse, context: RequestHandlerContext, request: KibanaRequest - ): Promise { + ): Promise { if (externalCallbackType === 'postPackagePolicyDelete') { return await this.runDeleteExternalCallbacks(packagePolicy as DeletePackagePoliciesResponse); } else { @@ -888,7 +898,21 @@ class PackagePolicyService implements PackagePolicyServiceInterface { if (externalCallbacks && externalCallbacks.size > 0) { let updatedNewData = newData; for (const callback of externalCallbacks) { - const result = await callback(updatedNewData, context, request); + let result; + if (externalCallbackType === 'packagePolicyPostCreate') { + result = await (callback as PostPackagePolicyPostCreateCallback)( + updatedNewData as PackagePolicy, + context, + request + ); + updatedNewData = PackagePolicySchema.validate(result); + } else { + result = await (callback as PostPackagePolicyCreateCallback)( + updatedNewData as NewPackagePolicy, + context, + request + ); + } if (externalCallbackType === 'packagePolicyCreate') { updatedNewData = NewPackagePolicySchema.validate(result); } else if (externalCallbackType === 'packagePolicyUpdate') { @@ -1276,10 +1300,18 @@ export interface PackagePolicyServiceInterface { externalCallbackType: A, packagePolicy: A extends 'postPackagePolicyDelete' ? DeletePackagePoliciesResponse + : A extends 'packagePolicyPostCreate' + ? PackagePolicy : NewPackagePolicy, context: RequestHandlerContext, request: KibanaRequest - ): Promise; + ): Promise< + A extends 'postPackagePolicyDelete' + ? void + : A extends 'packagePolicyPostCreate' + ? PackagePolicy + : NewPackagePolicy + >; runDeleteExternalCallbacks(deletedPackagePolicies: DeletePackagePoliciesResponse): Promise; diff --git a/x-pack/plugins/fleet/server/types/extensions.ts b/x-pack/plugins/fleet/server/types/extensions.ts index a7f4a422cc2ae..fefdb5e26d0fb 100644 --- a/x-pack/plugins/fleet/server/types/extensions.ts +++ b/x-pack/plugins/fleet/server/types/extensions.ts @@ -13,6 +13,7 @@ import type { DeletePackagePoliciesResponse, NewPackagePolicy, UpdatePackagePolicy, + PackagePolicy, } from '../../common'; export type PostPackagePolicyDeleteCallback = ( @@ -25,6 +26,12 @@ export type PostPackagePolicyCreateCallback = ( request: KibanaRequest ) => Promise; +export type PostPackagePolicyPostCreateCallback = ( + packagePolicy: PackagePolicy, + context: RequestHandlerContext, + request: KibanaRequest +) => Promise; + export type PutPackagePolicyUpdateCallback = ( updatePackagePolicy: UpdatePackagePolicy, context: RequestHandlerContext, @@ -32,6 +39,10 @@ export type PutPackagePolicyUpdateCallback = ( ) => Promise; export type ExternalCallbackCreate = ['packagePolicyCreate', PostPackagePolicyCreateCallback]; +export type ExternalCallbackPostCreate = [ + 'packagePolicyPostCreate', + PostPackagePolicyPostCreateCallback +]; export type ExternalCallbackDelete = ['postPackagePolicyDelete', PostPackagePolicyDeleteCallback]; export type ExternalCallbackUpdate = ['packagePolicyUpdate', PutPackagePolicyUpdateCallback]; @@ -40,6 +51,7 @@ export type ExternalCallbackUpdate = ['packagePolicyUpdate', PutPackagePolicyUpd */ export type ExternalCallback = | ExternalCallbackCreate + | ExternalCallbackPostCreate | ExternalCallbackDelete | ExternalCallbackUpdate; diff --git a/x-pack/plugins/fleet/server/types/models/package_policy.ts b/x-pack/plugins/fleet/server/types/models/package_policy.ts index 1d4dc1c1b3b65..c4f8ab2c1437e 100644 --- a/x-pack/plugins/fleet/server/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/package_policy.ts @@ -55,6 +55,7 @@ const PackagePolicyStreamsSchema = { }) ) ), + compiled_stream: schema.maybe(schema.any()), }; const PackagePolicyInputsSchema = { @@ -150,4 +151,24 @@ export const PackagePolicySchema = schema.object({ ...PackagePolicyBaseSchema, id: schema.string(), version: schema.maybe(schema.string()), + revision: schema.number(), + updated_at: schema.string(), + updated_by: schema.string(), + created_at: schema.string(), + created_by: schema.string(), + elasticsearch: schema.maybe( + schema.object({ + privileges: schema.maybe( + schema.object({ + cluster: schema.maybe(schema.arrayOf(schema.string())), + }) + ), + }) + ), + inputs: schema.arrayOf( + schema.object({ + ...PackagePolicyInputsSchema, + compiled_input: schema.maybe(schema.any()), + }) + ), }); diff --git a/x-pack/plugins/index_management/__jest__/components/index_table.test.js b/x-pack/plugins/index_management/__jest__/components/index_table.test.js index bf5ab6541bad8..d6d2b1da9670f 100644 --- a/x-pack/plugins/index_management/__jest__/components/index_table.test.js +++ b/x-pack/plugins/index_management/__jest__/components/index_table.test.js @@ -8,10 +8,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; -import axios from 'axios'; -import sinon from 'sinon'; import { findTestSubject } from '@elastic/eui/lib/test'; -import axiosXhrAdapter from 'axios/lib/adapters/xhr'; /** * The below import is required to avoid a console error warn from brace package @@ -20,8 +17,9 @@ import axiosXhrAdapter from 'axios/lib/adapters/xhr'; at createWorker (//node_modules/brace/index.js:17992:5) */ import { mountWithIntl, stubWebWorker } from '@kbn/test-jest-helpers'; // eslint-disable-line no-unused-vars +import { init as initHttpRequests } from '../client_integration/helpers/http_requests'; -import { BASE_PATH, API_BASE_PATH } from '../../common/constants'; +import { BASE_PATH } from '../../common/constants'; import { AppWithoutRouter } from '../../public/application/app'; import { AppContextProvider } from '../../public/application/app_context'; import { loadIndicesSuccess } from '../../public/application/store/actions'; @@ -40,9 +38,6 @@ import { executionContextServiceMock, } from '../../../../../src/core/public/mocks'; -const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); - -let server = null; let store = null; const indices = []; @@ -149,6 +144,8 @@ const getActionMenuButtons = (rendered) => { .map((span) => span.text()); }; describe('index table', () => { + const { httpSetup, httpRequestsMockHelpers } = initHttpRequests(); + beforeEach(() => { // Mock initialization of services const services = { @@ -159,8 +156,7 @@ describe('index table', () => { setExtensionsService(services.extensionsService); setUiMetricService(services.uiMetricService); - // @ts-ignore - httpService.setup(mockHttpClient); + httpService.setup(httpSetup); breadcrumbService.setup(() => undefined); notificationService.setup(notificationServiceMock.createStartContract()); @@ -186,33 +182,9 @@ describe('index table', () => { ); store.dispatch(loadIndicesSuccess({ indices })); - server = sinon.fakeServer.create(); - - server.respondWith(`${API_BASE_PATH}/indices`, [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(indices), - ]); - - server.respondWith([ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({ acknowledged: true }), - ]); - - server.respondWith(`${API_BASE_PATH}/indices/reload`, [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(indices), - ]); - - server.respondImmediately = true; - }); - afterEach(() => { - if (!server) { - return; - } - server.restore(); + + httpRequestsMockHelpers.setLoadIndicesResponse(indices); + httpRequestsMockHelpers.setReloadIndicesResponse(indices); }); test('should change pages when a pagination link is clicked on', async () => { @@ -476,22 +448,17 @@ describe('index table', () => { }); test('close index button works from context menu', async () => { - const rendered = mountWithIntl(component); - await runAllPromises(); - rendered.update(); - const modifiedIndices = indices.map((index) => { return { ...index, status: index.name === 'testy0' ? 'close' : index.status, }; }); + httpRequestsMockHelpers.setReloadIndicesResponse(modifiedIndices); - server.respondWith(`${API_BASE_PATH}/indices/reload`, [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(modifiedIndices), - ]); + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); testAction(rendered, 'closeIndexMenuButton'); }); @@ -503,16 +470,12 @@ describe('index table', () => { status: index.name === 'testy1' ? 'closed' : index.status, }; }); - - server.respondWith(`${API_BASE_PATH}/indices`, [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(modifiedIndices), - ]); + httpRequestsMockHelpers.setLoadIndicesResponse(modifiedIndices); const rendered = mountWithIntl(component); await runAllPromises(); rendered.update(); + testAction(rendered, 'openIndexMenuButton', 'testy1'); }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/append.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/append.test.tsx index af3651525d6a3..f35e05b800f14 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/append.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/append.test.tsx @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { setup, SetupResult, getProcessorValue } from './processor.helpers'; +import { setup, SetupResult, getProcessorValue, setupEnvironment } from './processor.helpers'; const APPEND_TYPE = 'append'; @@ -14,6 +14,8 @@ describe('Processor: Append', () => { let onUpdate: jest.Mock; let testBed: SetupResult; + const { httpSetup } = setupEnvironment(); + beforeAll(() => { jest.useFakeTimers(); }); @@ -26,7 +28,7 @@ describe('Processor: Append', () => { onUpdate = jest.fn(); await act(async () => { - testBed = await setup({ + testBed = await setup(httpSetup, { value: { processors: [], }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/bytes.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/bytes.test.tsx index 51117c3b517f9..d4ac176d6aaf5 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/bytes.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/bytes.test.tsx @@ -6,13 +6,14 @@ */ import { act } from 'react-dom/test-utils'; -import { setup, SetupResult, getProcessorValue } from './processor.helpers'; +import { setup, SetupResult, getProcessorValue, setupEnvironment } from './processor.helpers'; const BYTES_TYPE = 'bytes'; describe('Processor: Bytes', () => { let onUpdate: jest.Mock; let testBed: SetupResult; + const { httpSetup } = setupEnvironment(); beforeAll(() => { jest.useFakeTimers(); @@ -26,7 +27,7 @@ describe('Processor: Bytes', () => { onUpdate = jest.fn(); await act(async () => { - testBed = await setup({ + testBed = await setup(httpSetup, { value: { processors: [], }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx index b8c8f6c58f711..00153471ea65e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx @@ -6,13 +6,14 @@ */ import { act } from 'react-dom/test-utils'; -import { setup, SetupResult, getProcessorValue } from './processor.helpers'; +import { setup, SetupResult, getProcessorValue, setupEnvironment } from './processor.helpers'; const CIRCLE_TYPE = 'circle'; describe('Processor: Circle', () => { let onUpdate: jest.Mock; let testBed: SetupResult; + const { httpSetup } = setupEnvironment(); beforeAll(() => { jest.useFakeTimers(); @@ -26,7 +27,7 @@ describe('Processor: Circle', () => { onUpdate = jest.fn(); await act(async () => { - testBed = await setup({ + testBed = await setup(httpSetup, { value: { processors: [], }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/common_processor_fields.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/common_processor_fields.test.tsx index af5c8c50abf7b..ebffd5adf78c1 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/common_processor_fields.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/common_processor_fields.test.tsx @@ -6,13 +6,14 @@ */ import { act } from 'react-dom/test-utils'; -import { setup, SetupResult, getProcessorValue } from './processor.helpers'; +import { setup, SetupResult, getProcessorValue, setupEnvironment } from './processor.helpers'; const BYTES_TYPE = 'bytes'; describe('Processor: Common Fields For All Processors', () => { let onUpdate: jest.Mock; let testBed: SetupResult; + const { httpSetup } = setupEnvironment(); beforeAll(() => { jest.useFakeTimers(); @@ -26,7 +27,7 @@ describe('Processor: Common Fields For All Processors', () => { onUpdate = jest.fn(); await act(async () => { - testBed = await setup({ + testBed = await setup(httpSetup, { value: { processors: [], }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/community_id.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/community_id.test.tsx index 0338cb8e04348..e571474576ff2 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/community_id.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/community_id.test.tsx @@ -6,13 +6,14 @@ */ import { act } from 'react-dom/test-utils'; -import { setup, SetupResult, getProcessorValue } from './processor.helpers'; +import { setup, SetupResult, getProcessorValue, setupEnvironment } from './processor.helpers'; const COMMUNITY_ID_TYPE = 'community_id'; describe('Processor: Community id', () => { let onUpdate: jest.Mock; let testBed: SetupResult; + const { httpSetup } = setupEnvironment(); beforeAll(() => { jest.useFakeTimers(); @@ -26,7 +27,7 @@ describe('Processor: Community id', () => { onUpdate = jest.fn(); await act(async () => { - testBed = await setup({ + testBed = await setup(httpSetup, { value: { processors: [], }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/convert.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/convert.test.tsx index 5a58a7b595c90..3090e59b32e0b 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/convert.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/convert.test.tsx @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { setup, SetupResult, getProcessorValue } from './processor.helpers'; +import { setup, SetupResult, getProcessorValue, setupEnvironment } from './processor.helpers'; // Default parameter values automatically added to the `convert processor` when saved const defaultConvertParameters = { @@ -23,6 +23,7 @@ const CONVERT_TYPE = 'convert'; describe('Processor: Convert', () => { let onUpdate: jest.Mock; let testBed: SetupResult; + const { httpSetup } = setupEnvironment(); beforeAll(() => { jest.useFakeTimers(); @@ -36,7 +37,7 @@ describe('Processor: Convert', () => { onUpdate = jest.fn(); await act(async () => { - testBed = await setup({ + testBed = await setup(httpSetup, { value: { processors: [], }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/csv.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/csv.test.tsx index b40b46967dae5..6414976b56f9a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/csv.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/csv.test.tsx @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { setup, SetupResult, getProcessorValue } from './processor.helpers'; +import { setup, SetupResult, getProcessorValue, setupEnvironment } from './processor.helpers'; // Default parameter values automatically added to the CSV processor when saved const defaultCSVParameters = { @@ -26,6 +26,7 @@ const CSV_TYPE = 'csv'; describe('Processor: CSV', () => { let onUpdate: jest.Mock; let testBed: SetupResult; + const { httpSetup } = setupEnvironment(); beforeAll(() => { jest.useFakeTimers(); @@ -39,7 +40,7 @@ describe('Processor: CSV', () => { onUpdate = jest.fn(); await act(async () => { - testBed = await setup({ + testBed = await setup(httpSetup, { value: { processors: [], }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/date.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/date.test.tsx index 390f8e0191ce9..22666ebbe2a98 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/date.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/date.test.tsx @@ -6,13 +6,14 @@ */ import { act } from 'react-dom/test-utils'; -import { setup, SetupResult, getProcessorValue } from './processor.helpers'; +import { setup, SetupResult, getProcessorValue, setupEnvironment } from './processor.helpers'; const DATE_TYPE = 'date'; describe('Processor: Date', () => { let onUpdate: jest.Mock; let testBed: SetupResult; + const { httpSetup } = setupEnvironment(); beforeAll(() => { jest.useFakeTimers(); @@ -26,7 +27,7 @@ describe('Processor: Date', () => { onUpdate = jest.fn(); await act(async () => { - testBed = await setup({ + testBed = await setup(httpSetup, { value: { processors: [], }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/date_index.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/date_index.test.tsx index 264db2c5b65c0..b9e990f36c15b 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/date_index.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/date_index.test.tsx @@ -6,13 +6,14 @@ */ import { act } from 'react-dom/test-utils'; -import { setup, SetupResult, getProcessorValue } from './processor.helpers'; +import { setup, SetupResult, getProcessorValue, setupEnvironment } from './processor.helpers'; const DATE_INDEX_TYPE = 'date_index_name'; describe('Processor: Date Index Name', () => { let onUpdate: jest.Mock; let testBed: SetupResult; + const { httpSetup } = setupEnvironment(); beforeAll(() => { jest.useFakeTimers(); @@ -26,7 +27,7 @@ describe('Processor: Date Index Name', () => { onUpdate = jest.fn(); await act(async () => { - testBed = await setup({ + testBed = await setup(httpSetup, { value: { processors: [], }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/dot_expander.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/dot_expander.test.tsx index eb93f4ea86449..7ebad2de01a92 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/dot_expander.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/dot_expander.test.tsx @@ -6,13 +6,14 @@ */ import { act } from 'react-dom/test-utils'; -import { setup, SetupResult, getProcessorValue } from './processor.helpers'; +import { setup, SetupResult, getProcessorValue, setupEnvironment } from './processor.helpers'; const DOT_EXPANDER_TYPE = 'dot_expander'; describe('Processor: Dot Expander', () => { let onUpdate: jest.Mock; let testBed: SetupResult; + const { httpSetup } = setupEnvironment(); beforeAll(() => { jest.useFakeTimers(); @@ -26,7 +27,7 @@ describe('Processor: Dot Expander', () => { onUpdate = jest.fn(); await act(async () => { - testBed = await setup({ + testBed = await setup(httpSetup, { value: { processors: [], }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/fail.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/fail.test.tsx index db5840379536a..9b8148bd0dffd 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/fail.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/fail.test.tsx @@ -6,12 +6,13 @@ */ import { act } from 'react-dom/test-utils'; -import { setup, SetupResult, getProcessorValue } from './processor.helpers'; +import { setup, SetupResult, getProcessorValue, setupEnvironment } from './processor.helpers'; const FAIL_TYPE = 'fail'; describe('Processor: Fail', () => { let onUpdate: jest.Mock; let testBed: SetupResult; + const { httpSetup } = setupEnvironment(); beforeAll(() => { jest.useFakeTimers(); @@ -25,7 +26,7 @@ describe('Processor: Fail', () => { onUpdate = jest.fn(); await act(async () => { - testBed = await setup({ + testBed = await setup(httpSetup, { value: { processors: [], }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/fingerprint.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/fingerprint.test.tsx index 7c2ca012a0460..49d7937fab002 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/fingerprint.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/fingerprint.test.tsx @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { setup, SetupResult, getProcessorValue } from './processor.helpers'; +import { setup, SetupResult, getProcessorValue, setupEnvironment } from './processor.helpers'; // Default parameter values automatically added to the registered domain processor when saved const defaultFingerprintParameters = { @@ -25,6 +25,7 @@ const FINGERPRINT_TYPE = 'fingerprint'; describe('Processor: Fingerprint', () => { let onUpdate: jest.Mock; let testBed: SetupResult; + const { httpSetup } = setupEnvironment(); beforeAll(() => { jest.useFakeTimers(); @@ -38,7 +39,7 @@ describe('Processor: Fingerprint', () => { onUpdate = jest.fn(); await act(async () => { - testBed = await setup({ + testBed = await setup(httpSetup, { value: { processors: [], }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/network_direction.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/network_direction.test.tsx index 7a4c55d6f5e02..4f92aec06efd3 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/network_direction.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/network_direction.test.tsx @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { setup, SetupResult, getProcessorValue } from './processor.helpers'; +import { setup, SetupResult, getProcessorValue, setupEnvironment } from './processor.helpers'; // Default parameter values automatically added to the network direction processor when saved const defaultNetworkDirectionParameters = { @@ -27,6 +27,7 @@ const NETWORK_DIRECTION_TYPE = 'network_direction'; describe('Processor: Network Direction', () => { let onUpdate: jest.Mock; let testBed: SetupResult; + const { httpSetup } = setupEnvironment(); beforeAll(() => { jest.useFakeTimers(); @@ -40,7 +41,7 @@ describe('Processor: Network Direction', () => { onUpdate = jest.fn(); await act(async () => { - testBed = await setup({ + testBed = await setup(httpSetup, { value: { processors: [], }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx index ec68c3b3bfdf6..0e7431964c84b 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx @@ -7,10 +7,9 @@ import { act } from 'react-dom/test-utils'; import React from 'react'; -import axios from 'axios'; -import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import { usageCollectionPluginMock } from 'src/plugins/usage_collection/public/mocks'; +import { HttpSetup } from 'kibana/public'; import { registerTestBed, TestBed } from '@kbn/test-jest-helpers'; import { stubWebWorker } from '@kbn/test-jest-helpers'; @@ -102,7 +101,9 @@ const createActions = (testBed: TestBed) => { }; }; -export const setup = async (props: Props): Promise => { +export const setup = async (httpSetup: HttpSetup, props: Props): Promise => { + apiService.setup(httpSetup, uiMetricService); + const testBed = testBedSetup(props); return { ...testBed, @@ -110,19 +111,11 @@ export const setup = async (props: Props): Promise => { }; }; -const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); - export const setupEnvironment = () => { // Initialize mock services uiMetricService.setup(usageCollectionPluginMock.createSetupContract()); - // @ts-ignore - apiService.setup(mockHttpClient, uiMetricService); - - const { httpRequestsMockHelpers } = initHttpRequests(); - return { - httpRequestsMockHelpers, - }; + return initHttpRequests(); }; export const getProcessorValue = (onUpdate: jest.Mock, type: string) => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor_form.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor_form.test.tsx index de0061dcb0407..b2e2fb81f2c86 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor_form.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor_form.test.tsx @@ -6,11 +6,12 @@ */ import { act } from 'react-dom/test-utils'; -import { setup, SetupResult } from './processor.helpers'; +import { setup, SetupResult, setupEnvironment } from './processor.helpers'; describe('Processor: Bytes', () => { let onUpdate: jest.Mock; let testBed: SetupResult; + const { httpSetup } = setupEnvironment(); beforeAll(() => { jest.useFakeTimers(); @@ -24,7 +25,7 @@ describe('Processor: Bytes', () => { onUpdate = jest.fn(); await act(async () => { - testBed = await setup({ + testBed = await setup(httpSetup, { value: { processors: [], }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/registered_domain.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/registered_domain.test.tsx index 962e099f5b667..dcf332912a94b 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/registered_domain.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/registered_domain.test.tsx @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { setup, SetupResult, getProcessorValue } from './processor.helpers'; +import { setup, SetupResult, getProcessorValue, setupEnvironment } from './processor.helpers'; // Default parameter values automatically added to the registered domain processor when saved const defaultRegisteredDomainParameters = { @@ -21,6 +21,7 @@ const REGISTERED_DOMAIN_TYPE = 'registered_domain'; describe('Processor: Registered Domain', () => { let onUpdate: jest.Mock; let testBed: SetupResult; + const { httpSetup } = setupEnvironment(); beforeAll(() => { jest.useFakeTimers(); @@ -34,7 +35,7 @@ describe('Processor: Registered Domain', () => { onUpdate = jest.fn(); await act(async () => { - testBed = await setup({ + testBed = await setup(httpSetup, { value: { processors: [], }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/set.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/set.test.tsx index 544b8aeb51c05..ebfa678648904 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/set.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/set.test.tsx @@ -6,13 +6,14 @@ */ import { act } from 'react-dom/test-utils'; -import { setup, SetupResult, getProcessorValue } from './processor.helpers'; +import { setup, SetupResult, getProcessorValue, setupEnvironment } from './processor.helpers'; const SET_TYPE = 'set'; describe('Processor: Set', () => { let onUpdate: jest.Mock; let testBed: SetupResult; + const { httpSetup } = setupEnvironment(); beforeAll(() => { jest.useFakeTimers(); @@ -26,7 +27,7 @@ describe('Processor: Set', () => { onUpdate = jest.fn(); await act(async () => { - testBed = await setup({ + testBed = await setup(httpSetup, { value: { processors: [], }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/uri_parts.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/uri_parts.test.tsx index 442788a7f75aa..9062fcc02f7f8 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/uri_parts.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/uri_parts.test.tsx @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { setup, SetupResult, getProcessorValue } from './processor.helpers'; +import { setup, SetupResult, getProcessorValue, setupEnvironment } from './processor.helpers'; // Default parameter values automatically added to the URI parts processor when saved const defaultUriPartsParameters = { @@ -19,6 +19,7 @@ const URI_PARTS_TYPE = 'uri_parts'; describe('Processor: URI parts', () => { let onUpdate: jest.Mock; let testBed: SetupResult; + const { httpSetup } = setupEnvironment(); beforeAll(() => { jest.useFakeTimers(); @@ -32,7 +33,7 @@ describe('Processor: URI parts', () => { onUpdate = jest.fn(); await act(async () => { - testBed = await setup({ + testBed = await setup(httpSetup, { value: { processors: [], }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/user_agent.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/user_agent.test.tsx index fa1c24c9dfb39..ed778aa1cc1f3 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/user_agent.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/user_agent.test.tsx @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { setup, SetupResult, getProcessorValue } from './processor.helpers'; +import { setup, SetupResult, getProcessorValue, setupEnvironment } from './processor.helpers'; // Default parameter values automatically added to the user agent processor when saved const defaultUserAgentParameters = { @@ -24,6 +24,7 @@ const USER_AGENT_TYPE = 'user_agent'; describe('Processor: User Agent', () => { let onUpdate: jest.Mock; let testBed: SetupResult; + const { httpSetup } = setupEnvironment(); beforeAll(() => { jest.useFakeTimers(); @@ -37,7 +38,7 @@ describe('Processor: User Agent', () => { onUpdate = jest.fn(); await act(async () => { - testBed = await setup({ + testBed = await setup(httpSetup, { value: { processors: [], }, diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index a5e2e4e0f2675..d56ade22272a8 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -17,7 +17,7 @@ import type { SuggestionRequest, Visualization, VisualizationSuggestion, - DatasourcePublicAPI, + DatasourceLayers, } from '../types'; import { LensIconChartDatatable } from '../assets/chart_datatable'; import { TableDimensionEditor } from './components/dimension_editor'; @@ -492,7 +492,7 @@ export const getDatatableVisualization = ({ function getDataSourceAndSortedColumns( state: DatatableVisualizationState, - datasourceLayers: Record, + datasourceLayers: DatasourceLayers, layerId: string ) { const datasource = datasourceLayers[state.layerId]; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/add_layer.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/add_layer.tsx index dbbc6932220dc..c38a9a3493708 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/add_layer.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/add_layer.tsx @@ -64,7 +64,6 @@ export function AddLayerButton({ position="bottom" > { return { toolTipContent, @@ -143,7 +142,6 @@ export function AddLayerButton({ {label} ), className: 'lnsLayerAddButton', - width: 300, icon: icon && , ['data-test-subj']: `lnsLayerAddButton-${type}`, onClick: () => { 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 b234b18f5262f..d48c145149aaa 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 @@ -422,6 +422,7 @@ describe('ConfigPanel', () => { datasourceLayers: { a: expect.anything(), }, + dateRange: expect.anything(), }, groupId: 'a', layerId: 'newId', diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss index 343cd746ba2ac..69e2ee99460bc 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss @@ -148,4 +148,4 @@ @include passDownFocusRing('.lnsLayerPanel__triggerTextLabel'); background-color: transparent; } -} +} \ No newline at end of file diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts index 600341931f575..cecf75cd58676 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts @@ -7,7 +7,7 @@ import { Ast, AstFunction, fromExpression } from '@kbn/interpreter'; import { DatasourceStates } from '../../state_management'; -import { Visualization, DatasourcePublicAPI, DatasourceMap } from '../../types'; +import { Visualization, DatasourceMap, DatasourceLayers } from '../../types'; export function prependDatasourceExpression( visualizationExpression: Ast | string | null, @@ -74,7 +74,7 @@ export function buildExpression({ visualizationState: unknown; datasourceMap: DatasourceMap; datasourceStates: DatasourceStates; - datasourceLayers: Record; + datasourceLayers: DatasourceLayers; }): Ast | null { if (visualization === null) { return null; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index 95626b7657e59..4b857a71be961 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -10,8 +10,8 @@ import { Ast } from '@kbn/interpreter'; import memoizeOne from 'memoize-one'; import { Datasource, + DatasourceLayers, DatasourceMap, - DatasourcePublicAPI, FramePublicAPI, InitializationOptions, Visualization, @@ -62,7 +62,7 @@ export const getDatasourceLayers = memoizeOne(function getDatasourceLayers( datasourceStates: DatasourceStates, datasourceMap: DatasourceMap ) { - const datasourceLayers: Record = {}; + const datasourceLayers: DatasourceLayers = {}; Object.keys(datasourceMap) .filter((id) => datasourceStates[id] && !datasourceStates[id].isLoading) .forEach((id) => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts index f24158d2db501..cc1868974bbf2 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts @@ -13,11 +13,11 @@ import { Datasource, TableSuggestion, DatasourceSuggestion, - DatasourcePublicAPI, DatasourceMap, VisualizationMap, VisualizeEditorContext, Suggestion, + DatasourceLayers, } from '../../types'; import { DragDropIdentifier } from '../../drag_drop'; import { LayerType, layerTypes } from '../../../common'; @@ -261,7 +261,7 @@ export function switchToSuggestion( } export function getTopSuggestionForField( - datasourceLayers: Record, + datasourceLayers: DatasourceLayers, visualization: VisualizationState, datasourceStates: DatasourceStates, visualizationMap: Record>, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index 69de606a77ca7..436ab5603a56e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -30,9 +30,9 @@ import { Datasource, Visualization, FramePublicAPI, - DatasourcePublicAPI, DatasourceMap, VisualizationMap, + DatasourceLayers, } from '../../types'; import { getSuggestions, switchToSuggestion } from './suggestion_helpers'; import { @@ -491,7 +491,9 @@ function getPreviewExpression( return null; } - const suggestionFrameApi: FramePublicAPI = { datasourceLayers: { ...frame.datasourceLayers } }; + const suggestionFrameApi: Pick = { + datasourceLayers: { ...frame.datasourceLayers }, + }; // use current frame api and patch apis for changed datasource layers if ( @@ -501,7 +503,7 @@ function getPreviewExpression( ) { const datasource = datasources[visualizableState.datasourceId]; const datasourceState = visualizableState.datasourceState; - const updatedLayerApis: Record = pick( + const updatedLayerApis: DatasourceLayers = pick( frame.datasourceLayers, visualizableState.keptLayerIds ); diff --git a/x-pack/plugins/lens/public/heatmap_visualization/visualization.test.ts b/x-pack/plugins/lens/public/heatmap_visualization/visualization.test.ts index f181167cd8d9c..a16aa69f627f2 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/heatmap_visualization/visualization.test.ts @@ -20,7 +20,7 @@ import { } from './constants'; import { Position } from '@elastic/charts'; import type { HeatmapVisualizationState } from './types'; -import type { DatasourcePublicAPI, OperationDescriptor } from '../types'; +import type { DatasourceLayers, OperationDescriptor } from '../types'; import { chartPluginMock } from 'src/plugins/charts/public/mocks'; import { layerTypes } from '../../common'; import { themeServiceMock } from '../../../../../src/core/public/mocks'; @@ -355,7 +355,7 @@ describe('heatmap', () => { }); describe('#toExpression', () => { - let datasourceLayers: Record; + let datasourceLayers: DatasourceLayers; beforeEach(() => { const mockDatasource = createMockDatasource('testDatasource'); @@ -476,7 +476,7 @@ describe('heatmap', () => { }); describe('#toPreviewExpression', () => { - let datasourceLayers: Record; + let datasourceLayers: DatasourceLayers; beforeEach(() => { const mockDatasource = createMockDatasource('testDatasource'); diff --git a/x-pack/plugins/lens/public/metric_visualization/metric_config_panel/align_options.tsx b/x-pack/plugins/lens/public/metric_visualization/metric_config_panel/align_options.tsx index f2b97454df9df..a400e42ac08f5 100644 --- a/x-pack/plugins/lens/public/metric_visualization/metric_config_panel/align_options.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/metric_config_panel/align_options.tsx @@ -15,6 +15,8 @@ export interface TitlePositionProps { setState: (newState: MetricState) => void; } +export const DEFAULT_TEXT_ALIGNMENT = 'left'; + const alignButtonIcons = [ { id: `left`, @@ -46,7 +48,7 @@ export const AlignOptions: React.FC = ({ state, setState }) defaultMessage: 'Align', })} options={alignButtonIcons} - idSelected={state.textAlign ?? 'center'} + idSelected={state.textAlign ?? DEFAULT_TEXT_ALIGNMENT} onChange={(id) => { setState({ ...state, textAlign: id as MetricState['textAlign'] }); }} diff --git a/x-pack/plugins/lens/public/metric_visualization/metric_config_panel/size_options.tsx b/x-pack/plugins/lens/public/metric_visualization/metric_config_panel/size_options.tsx index 17142ab77ab35..5a4ca7f644956 100644 --- a/x-pack/plugins/lens/public/metric_visualization/metric_config_panel/size_options.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/metric_config_panel/size_options.tsx @@ -15,6 +15,8 @@ export interface TitlePositionProps { setState: (newState: MetricState) => void; } +export const DEFAULT_TITLE_SIZE = 'm'; + const titleSizes = [ { id: 'xs', @@ -55,7 +57,9 @@ const titleSizes = [ ]; export const SizeOptions: React.FC = ({ state, setState }) => { - const currSizeIndex = titleSizes.findIndex((size) => size.id === (state.size || 'xl')); + const currSizeIndex = titleSizes.findIndex( + (size) => size.id === (state.size || DEFAULT_TITLE_SIZE) + ); const changeSize = (change: number) => { setState({ ...state, size: titleSizes[currSizeIndex + change].id }); @@ -86,7 +90,7 @@ export const SizeOptions: React.FC = ({ state, setState }) = inputDisplay: position.label, }; })} - valueOfSelected={state.size ?? 'xl'} + valueOfSelected={state.size ?? DEFAULT_TITLE_SIZE} onChange={(value) => { setState({ ...state, size: value }); }} diff --git a/x-pack/plugins/lens/public/metric_visualization/metric_config_panel/title_position_option.tsx b/x-pack/plugins/lens/public/metric_visualization/metric_config_panel/title_position_option.tsx index acaa11f477226..c58854647c1e7 100644 --- a/x-pack/plugins/lens/public/metric_visualization/metric_config_panel/title_position_option.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/metric_config_panel/title_position_option.tsx @@ -15,9 +15,19 @@ export interface TitlePositionProps { setState: (newState: MetricState) => void; } +export const DEFAULT_TITLE_POSITION = 'top'; + const titlePositions = [ - { id: 'top', label: 'Top' }, - { id: 'bottom', label: 'Bottom' }, + { + id: 'top', + label: i18n.translate('xpack.lens.metricChart.titlePositions.top', { defaultMessage: 'Top' }), + }, + { + id: 'bottom', + label: i18n.translate('xpack.lens.metricChart.titlePositions.bottom', { + defaultMessage: 'Bottom', + }), + }, ]; export const TitlePositionOptions: React.FC = ({ state, setState }) => { @@ -38,7 +48,7 @@ export const TitlePositionOptions: React.FC = ({ state, setS data-test-subj="lnsMissingValuesSelect" legend="This is a basic group" options={titlePositions} - idSelected={state.titlePosition ?? 'bottom'} + idSelected={state.titlePosition ?? DEFAULT_TITLE_POSITION} onChange={(value) => { setState({ ...state, titlePosition: value as MetricState['titlePosition'] }); }} diff --git a/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts b/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts index c7e01b0c6a137..e3a70ef17f8af 100644 --- a/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts @@ -301,13 +301,13 @@ describe('metric_visualization', () => { Object { "arguments": Object { "align": Array [ - "center", + "left", ], "lHeight": Array [ - 127.5, + 82.5, ], "size": Array [ - 85, + 55, ], "sizeUnit": Array [ "px", @@ -329,13 +329,13 @@ describe('metric_visualization', () => { Object { "arguments": Object { "align": Array [ - "center", + "left", ], "lHeight": Array [ - 40.5, + 24, ], "size": Array [ - 27, + 16, ], "sizeUnit": Array [ "px", @@ -349,7 +349,7 @@ describe('metric_visualization', () => { }, ], "labelPosition": Array [ - "bottom", + "top", ], "metric": Array [ Object { diff --git a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx index 87414e20f5abd..bbc8b3448618a 100644 --- a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx @@ -17,11 +17,14 @@ import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/pub import { ColorMode, CustomPaletteState } from '../../../../../src/plugins/charts/common'; import { getSuggestions } from './metric_suggestions'; import { LensIconChartMetric } from '../assets/chart_metric'; -import { Visualization, OperationMetadata, DatasourcePublicAPI } from '../types'; +import { Visualization, OperationMetadata, DatasourceLayers } from '../types'; import type { MetricState } from '../../common/types'; import { layerTypes } from '../../common'; import { MetricDimensionEditor } from './dimension_editor'; import { MetricToolbar } from './metric_config_panel'; +import { DEFAULT_TITLE_POSITION } from './metric_config_panel/title_position_option'; +import { DEFAULT_TITLE_SIZE } from './metric_config_panel/size_options'; +import { DEFAULT_TEXT_ALIGNMENT } from './metric_config_panel/align_options'; interface MetricConfig extends Omit { title: string; @@ -45,7 +48,7 @@ const getFontSizeAndUnit = (fontSize: string) => { const toExpression = ( paletteService: PaletteRegistry, state: MetricState, - datasourceLayers: Record, + datasourceLayers: DatasourceLayers, attributes?: Partial> ): Ast | null => { if (!state.accessor) { @@ -82,7 +85,7 @@ const toExpression = ( xxl: getFontSizeAndUnit(euiThemeVars.euiFontSizeXXL), }; - const labelFont = fontSizes[state?.size || 'xl']; + const labelFont = fontSizes[state?.size || DEFAULT_TITLE_SIZE]; const labelToMetricFontSizeMap: Record = { xs: fontSizes.xs.size * 2, s: fontSizes.m.size * 2.5, @@ -91,7 +94,7 @@ const toExpression = ( xl: fontSizes.xxl.size * 2.5, xxl: fontSizes.xxl.size * 3, }; - const metricFontSize = labelToMetricFontSizeMap[state?.size || 'xl']; + const metricFontSize = labelToMetricFontSizeMap[state?.size || DEFAULT_TITLE_SIZE]; return { type: 'expression', @@ -100,7 +103,7 @@ const toExpression = ( type: 'function', function: 'metricVis', arguments: { - labelPosition: [state?.titlePosition || 'bottom'], + labelPosition: [state?.titlePosition || DEFAULT_TITLE_POSITION], font: [ { type: 'expression', @@ -109,7 +112,7 @@ const toExpression = ( type: 'function', function: 'font', arguments: { - align: [state?.textAlign || 'center'], + align: [state?.textAlign || DEFAULT_TEXT_ALIGNMENT], size: [metricFontSize], weight: ['600'], lHeight: [metricFontSize * 1.5], @@ -127,7 +130,7 @@ const toExpression = ( type: 'function', function: 'font', arguments: { - align: [state?.textAlign || 'center'], + align: [state?.textAlign || DEFAULT_TEXT_ALIGNMENT], size: [labelFont.size], lHeight: [labelFont.size * 1.5], sizeUnit: [labelFont.sizeUnit], diff --git a/x-pack/plugins/lens/public/mocks/index.ts b/x-pack/plugins/lens/public/mocks/index.ts index 4c97fea960eac..ab6f2066e8804 100644 --- a/x-pack/plugins/lens/public/mocks/index.ts +++ b/x-pack/plugins/lens/public/mocks/index.ts @@ -31,6 +31,7 @@ export type FrameMock = jest.Mocked; export const createMockFramePublicAPI = (): FrameMock => ({ datasourceLayers: {}, + dateRange: { fromDate: 'now-7d', toDate: 'now' }, }); export type FrameDatasourceMock = jest.Mocked; diff --git a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts index bacbc8f0720a3..ea5cb560693c4 100644 --- a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts @@ -13,7 +13,7 @@ import { buildExpression, buildExpressionFunction, } from '../../../../../src/plugins/expressions/public'; -import type { Operation, DatasourcePublicAPI } from '../types'; +import type { Operation, DatasourcePublicAPI, DatasourceLayers } from '../types'; import { DEFAULT_PERCENT_DECIMALS } from './constants'; import { shouldShowValuesInLegend } from './render_helpers'; import { @@ -43,7 +43,7 @@ type GenerateExpressionAstFunction = ( attributes: Attributes, operations: OperationColumnId[], layer: PieLayerState, - datasourceLayers: Record, + datasourceLayers: DatasourceLayers, paletteService: PaletteRegistry ) => Ast | null; @@ -52,7 +52,7 @@ type GenerateExpressionAstArguments = ( attributes: Attributes, operations: OperationColumnId[], layer: PieLayerState, - datasourceLayers: Record, + datasourceLayers: DatasourceLayers, paletteService: PaletteRegistry ) => Ast['chain'][number]['arguments']; @@ -246,7 +246,7 @@ const generateExprAst: GenerateExpressionAstFunction = (state, ...restArgs) => function expressionHelper( state: PieVisualizationState, - datasourceLayers: Record, + datasourceLayers: DatasourceLayers, paletteService: PaletteRegistry, attributes: Attributes = { isPreview: false } ): Ast | null { @@ -270,7 +270,7 @@ function expressionHelper( export function toExpression( state: PieVisualizationState, - datasourceLayers: Record, + datasourceLayers: DatasourceLayers, paletteService: PaletteRegistry, attributes: Partial<{ title: string; description: string }> = {} ) { @@ -282,7 +282,7 @@ export function toExpression( export function toPreviewExpression( state: PieVisualizationState, - datasourceLayers: Record, + datasourceLayers: DatasourceLayers, paletteService: PaletteRegistry ) { return expressionHelper(state, datasourceLayers, paletteService, { isPreview: true }); diff --git a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx index dac889fc99df6..766edf6b0dbc0 100644 --- a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx @@ -183,7 +183,7 @@ export function PieToolbar(props: VisualizationToolbarProps ) : null} - {numberOptions.length ? ( + {numberOptions.length && layer.categoryDisplay !== 'hide' ? ( diff --git a/x-pack/plugins/lens/public/shared_components/columns_number_setting.test.tsx b/x-pack/plugins/lens/public/shared_components/columns_number_setting.test.tsx deleted file mode 100644 index 50f2dc2fb93dc..0000000000000 --- a/x-pack/plugins/lens/public/shared_components/columns_number_setting.test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { mountWithIntl as mount } from '@kbn/test-jest-helpers'; -import { ColumnsNumberSetting } from './columns_number_setting'; - -describe('Columns Number Setting', () => { - it('should have default the columns input to 1 when no value is given', () => { - const component = mount(); - expect( - component.find('[data-test-subj="lens-legend-location-columns-input"]').at(0).prop('value') - ).toEqual(1); - }); -}); diff --git a/x-pack/plugins/lens/public/shared_components/columns_number_setting.tsx b/x-pack/plugins/lens/public/shared_components/columns_number_setting.tsx index 6a1d1ec6d0306..3cfd03d53c51d 100644 --- a/x-pack/plugins/lens/public/shared_components/columns_number_setting.tsx +++ b/x-pack/plugins/lens/public/shared_components/columns_number_setting.tsx @@ -9,7 +9,6 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFieldNumber, EuiFormRow } from '@elastic/eui'; import { useDebouncedValue } from './debounced_value'; -import { TooltipWrapper } from './tooltip_wrapper'; export const DEFAULT_FLOATING_COLUMNS = 1; @@ -22,10 +21,6 @@ interface ColumnsNumberSettingProps { * Callback on horizontal alignment option change */ onFloatingColumnsChange?: (value: number) => void; - /** - * Flag to disable the location settings - */ - isDisabled: boolean; /** * Indicates if legend is located outside */ @@ -35,7 +30,6 @@ interface ColumnsNumberSettingProps { export const ColumnsNumberSetting = ({ floatingColumns, onFloatingColumnsChange = () => {}, - isDisabled, isLegendOutside, }: ColumnsNumberSettingProps) => { const { inputValue, handleInputChange } = useDebouncedValue({ @@ -43,6 +37,8 @@ export const ColumnsNumberSetting = ({ onChange: onFloatingColumnsChange, }); + if (isLegendOutside) return null; + return ( - - { - handleInputChange(Number(e.target.value)); - }} - step={1} - /> - + { + handleInputChange(Number(e.target.value)); + }} + step={1} + /> ); }; diff --git a/x-pack/plugins/lens/public/shared_components/datasource_default_values.ts b/x-pack/plugins/lens/public/shared_components/datasource_default_values.ts index 250c5614fe093..a0b6f767b287b 100644 --- a/x-pack/plugins/lens/public/shared_components/datasource_default_values.ts +++ b/x-pack/plugins/lens/public/shared_components/datasource_default_values.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DatasourcePublicAPI } from '../types'; +import { DatasourceLayers } from '../types'; type VisState = { layers: Array<{ layerId: string }> } | { layerId: string }; @@ -28,7 +28,7 @@ function mergeValues(memo: MappedVisualValue, values: Partial export function getDefaultVisualValuesForLayer( state: VisState | undefined, - datasourceLayers: Record + datasourceLayers: DatasourceLayers ): MappedVisualValue { const defaultValues = { truncateText: true }; if (!state) { diff --git a/x-pack/plugins/lens/public/shared_components/legend_location_settings.test.tsx b/x-pack/plugins/lens/public/shared_components/legend_location_settings.test.tsx index f4b5ced490663..d318a19014928 100644 --- a/x-pack/plugins/lens/public/shared_components/legend_location_settings.test.tsx +++ b/x-pack/plugins/lens/public/shared_components/legend_location_settings.test.tsx @@ -32,11 +32,9 @@ describe('Legend Location Settings', () => { expect(props.onPositionChange).toHaveBeenCalled(); }); - it('should disable the position group if isDisabled prop is true', () => { + it('should hide the position group if isDisabled prop is true', () => { const component = shallow(); - expect( - component.find('[data-test-subj="lens-legend-position-btn"]').prop('isDisabled') - ).toEqual(true); + expect(component.exists('[data-test-subj="lens-legend-position-btn"]')).toEqual(false); }); it('should hide the position button group if location inside is given', () => { @@ -104,18 +102,14 @@ describe('Legend Location Settings', () => { expect(newProps.onAlignmentChange).toHaveBeenCalled(); }); - it('should disable the components when is Disabled is true', () => { + it('should hide the components when is Disabled is true', () => { const newProps = { ...props, location: 'inside', isDisabled: true, } as LegendLocationSettingsProps; const component = shallow(); - expect( - component.find('[data-test-subj="lens-legend-location-btn"]').prop('isDisabled') - ).toEqual(true); - expect( - component.find('[data-test-subj="lens-legend-inside-alignment-btn"]').prop('isDisabled') - ).toEqual(true); + expect(component.exists('[data-test-subj="lens-legend-location-btn"]')).toEqual(false); + expect(component.exists('[data-test-subj="lens-legend-inside-alignment-btn"]')).toEqual(false); }); }); diff --git a/x-pack/plugins/lens/public/shared_components/legend_location_settings.tsx b/x-pack/plugins/lens/public/shared_components/legend_location_settings.tsx index f3ac54ab00a05..7372b727268bd 100644 --- a/x-pack/plugins/lens/public/shared_components/legend_location_settings.tsx +++ b/x-pack/plugins/lens/public/shared_components/legend_location_settings.tsx @@ -9,7 +9,6 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiButtonGroup } from '@elastic/eui'; import { VerticalAlignment, HorizontalAlignment, Position } from '@elastic/charts'; -import { TooltipWrapper } from './tooltip_wrapper'; export interface LegendLocationSettingsProps { /** @@ -151,6 +150,7 @@ export const LegendLocationSettings: React.FunctionComponent {location && ( @@ -160,32 +160,22 @@ export const LegendLocationSettings: React.FunctionComponent - - value === location)!.id} - onChange={(optionId) => { - const newLocation = locationOptions.find(({ id }) => id === optionId)!.value; - onLocationChange(newLocation); - }} - /> - + data-test-subj="lens-legend-location-btn" + name="legendLocation" + buttonSize="compressed" + options={locationOptions} + isDisabled={isDisabled} + idSelected={locationOptions.find(({ value }) => value === location)!.id} + onChange={(optionId) => { + const newLocation = locationOptions.find(({ id }) => id === optionId)!.value; + onLocationChange(newLocation); + }} + /> )} <> {(!location || location === 'outside') && ( - - - + isDisabled={isDisabled} + data-test-subj="lens-legend-position-btn" + name="legendPosition" + buttonSize="compressed" + options={toggleButtonsIcons} + idSelected={position || Position.Right} + onChange={onPositionChange} + isIconOnly + /> )} {location === 'inside' && ( - - value === alignment)!.id - } - onChange={(optionId) => { - const newAlignment = locationAlignmentButtonsIcons.find( - ({ id }) => id === optionId - )!.value; - onAlignmentChange(newAlignment); - }} - isIconOnly - /> - + type="single" + data-test-subj="lens-legend-inside-alignment-btn" + name="legendInsideAlignment" + buttonSize="compressed" + isDisabled={isDisabled} + options={locationAlignmentButtonsIcons} + idSelected={ + locationAlignmentButtonsIcons.find(({ value }) => value === alignment)!.id + } + onChange={(optionId) => { + const newAlignment = locationAlignmentButtonsIcons.find( + ({ id }) => id === optionId + )!.value; + onAlignmentChange(newAlignment); + }} + isIconOnly + /> )} diff --git a/x-pack/plugins/lens/public/shared_components/legend_settings_popover.test.tsx b/x-pack/plugins/lens/public/shared_components/legend_settings_popover.test.tsx index e76426515548f..9bf9a1885e6ac 100644 --- a/x-pack/plugins/lens/public/shared_components/legend_settings_popover.test.tsx +++ b/x-pack/plugins/lens/public/shared_components/legend_settings_popover.test.tsx @@ -56,7 +56,7 @@ describe('Legend Settings', () => { }); it('should have default the max lines input to 1 when no value is given', () => { - const component = shallow(); + const component = shallow(); expect(component.find(MaxLinesInput).prop('value')).toEqual(1); }); @@ -74,9 +74,9 @@ describe('Legend Settings', () => { ).toEqual(false); }); - it('should have disabled the max lines input when truncate is set to false', () => { + it('should hide the max lines input when truncate is set to false', () => { const component = shallow(); - expect(component.find(MaxLinesInput).prop('isDisabled')).toEqual(true); + expect(component.exists(MaxLinesInput)).toEqual(false); }); it('should have called the onTruncateLegendChange function on truncate switch change', () => { @@ -115,12 +115,10 @@ describe('Legend Settings', () => { expect(nestedProps.onNestedLegendChange).toHaveBeenCalled(); }); - it('should disable switch group on hide mode', () => { + it('should hide switch group on hide mode', () => { const component = shallow( ); - expect(component.find('[data-test-subj="lens-legend-nested-switch"]').prop('disabled')).toEqual( - true - ); + expect(component.exists('[data-test-subj="lens-legend-nested-switch"]')).toEqual(false); }); }); diff --git a/x-pack/plugins/lens/public/shared_components/legend_settings_popover.tsx b/x-pack/plugins/lens/public/shared_components/legend_settings_popover.tsx index 481c38815d43d..b7bc79c1446e9 100644 --- a/x-pack/plugins/lens/public/shared_components/legend_settings_popover.tsx +++ b/x-pack/plugins/lens/public/shared_components/legend_settings_popover.tsx @@ -20,7 +20,6 @@ import { LegendLocationSettings } from './legend_location_settings'; import { ColumnsNumberSetting } from './columns_number_setting'; import { LegendSizeSettings } from './legend_size_settings'; import { ToolbarButtonProps } from '../../../../../src/plugins/kibana_react/public'; -import { TooltipWrapper } from './tooltip_wrapper'; import { useDebouncedValue } from './debounced_value'; export interface LegendSettingsPopoverProps { @@ -137,11 +136,9 @@ const MIN_TRUNCATE_LINES = 1; export const MaxLinesInput = ({ value, setValue, - isDisabled, }: { value: number; setValue: (value: number) => void; - isDisabled: boolean; }) => { const { inputValue, handleInputChange } = useDebouncedValue({ value, onChange: setValue }); return ( @@ -152,7 +149,6 @@ export const MaxLinesInput = ({ max={MAX_TRUNCATE_LINES} step={1} compressed - disabled={isDisabled} onChange={(e) => { const val = Number(e.target.value); // we want to automatically change the values to the limits @@ -218,146 +214,101 @@ export const LegendSettingsPopover: React.FunctionComponent - - - {location && ( - - )} - - - + - - - - - - - - {renderNestedLegendSwitch && ( - - + )} + - - - )} - {renderValueInLegendSwitch && ( - - - + {shouldTruncate && ( + + + + )} + {renderNestedLegendSwitch && ( + + + + )} + {renderValueInLegendSwitch && ( + - - + > + + + )} + )} ); diff --git a/x-pack/plugins/lens/public/shared_components/legend_size_settings.tsx b/x-pack/plugins/lens/public/shared_components/legend_size_settings.tsx index a596bb7189571..53da283de0b68 100644 --- a/x-pack/plugins/lens/public/shared_components/legend_size_settings.tsx +++ b/x-pack/plugins/lens/public/shared_components/legend_size_settings.tsx @@ -8,7 +8,6 @@ import React, { useEffect, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiSuperSelect } from '@elastic/eui'; -import { TooltipWrapper } from './tooltip_wrapper'; export enum LegendSizes { AUTO = '0', @@ -22,7 +21,6 @@ interface LegendSizeSettingsProps { legendSize: number | undefined; onLegendSizeChange: (size?: number) => void; isVerticalLegend: boolean; - isDisabled: boolean; } const legendSizeOptions: Array<{ value: LegendSizes; inputDisplay: string }> = [ @@ -65,7 +63,6 @@ export const LegendSizeSettings = ({ legendSize, onLegendSizeChange, isVerticalLegend, - isDisabled, }: LegendSizeSettingsProps) => { useEffect(() => { if (legendSize && !isVerticalLegend) { @@ -78,6 +75,8 @@ export const LegendSizeSettings = ({ [onLegendSizeChange] ); + if (!isVerticalLegend) return null; + return ( - - - + ); }; 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 959db8ca006fe..61c99ef732233 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -632,6 +632,7 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { ? (current(state.activeData) as TableInspectorAdapter) : undefined, datasourceLayers: getDatasourceLayers(state.datasourceStates, datasourceMap), + dateRange: current(state.resolvedDateRange), }; const activeDatasource = datasourceMap[state.activeDatasourceId]; @@ -690,6 +691,7 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { ? (current(state.activeData) as TableInspectorAdapter) : undefined, datasourceLayers: getDatasourceLayers(state.datasourceStates, datasourceMap), + dateRange: current(state.resolvedDateRange), }, activeVisualization, activeDatasource, diff --git a/x-pack/plugins/lens/public/state_management/selectors.ts b/x-pack/plugins/lens/public/state_management/selectors.ts index 351496ca3e37b..47ce9a5b7349a 100644 --- a/x-pack/plugins/lens/public/state_management/selectors.ts +++ b/x-pack/plugins/lens/public/state_management/selectors.ts @@ -162,11 +162,13 @@ export const selectFramePublicAPI = createSelector( selectCurrentDatasourceStates, selectActiveData, selectInjectedDependencies as SelectInjectedDependenciesFunction, + selectResolvedDateRange, ], - (datasourceStates, activeData, datasourceMap) => { + (datasourceStates, activeData, datasourceMap, dateRange) => { return { datasourceLayers: getDatasourceLayers(datasourceStates, datasourceMap), activeData, + dateRange, }; } ); diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 2143cb1704bf5..d1a12daee1bfc 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -591,7 +591,7 @@ interface VisualizationDimensionChangeProps { layerId: string; columnId: string; prevState: T; - frame: Pick; + frame: FramePublicAPI; } export interface Suggestion { visualizationId: string; @@ -683,9 +683,11 @@ export interface VisualizationSuggestion { previewIcon: IconType; } +export type DatasourceLayers = Record; + export interface FramePublicAPI { - datasourceLayers: Record; - appliedDatasourceLayers?: Record; // this is only set when auto-apply is turned off + datasourceLayers: DatasourceLayers; + dateRange: DateRange; /** * Data of the chart currently rendered in the preview. * This data might be not available (e.g. if the chart can't be rendered) or outdated and belonging to another chart. @@ -694,7 +696,6 @@ export interface FramePublicAPI { activeData?: Record; } export interface FrameDatasourceAPI extends FramePublicAPI { - dateRange: DateRange; query: Query; filters: Filter[]; } @@ -884,7 +885,7 @@ export interface Visualization { toExpression: ( state: T, - datasourceLayers: Record, + datasourceLayers: DatasourceLayers, attributes?: Partial<{ title: string; description: string }> ) => ExpressionAstExpression | string | null; /** @@ -893,7 +894,7 @@ export interface Visualization { */ toPreviewExpression?: ( state: T, - datasourceLayers: Record + datasourceLayers: DatasourceLayers ) => ExpressionAstExpression | string | null; /** * The frame will call this function on all visualizations at few stages (pre-build/build error) in order @@ -901,7 +902,7 @@ export interface Visualization { */ getErrorMessages: ( state: T, - datasourceLayers?: Record + datasourceLayers?: DatasourceLayers ) => | Array<{ shortMessage: string; diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts index 58637b95fce67..6e658cd2729bb 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts @@ -9,7 +9,7 @@ import type { PaletteOutput, CustomPaletteParams } from '@kbn/coloring'; import { getGaugeVisualization, isNumericDynamicMetric, isNumericMetric } from './visualization'; import { createMockDatasource, createMockFramePublicAPI } from '../../mocks'; import { GROUP_ID } from './constants'; -import type { DatasourcePublicAPI, OperationDescriptor } from '../../types'; +import type { DatasourceLayers, OperationDescriptor } from '../../types'; import { chartPluginMock } from 'src/plugins/charts/public/mocks'; import { layerTypes } from '../../../common'; import type { GaugeVisualizationState } from './constants'; @@ -455,7 +455,7 @@ describe('gauge', () => { }); describe('#toExpression', () => { - let datasourceLayers: Record; + let datasourceLayers: DatasourceLayers; beforeEach(() => { const mockDatasource = createMockDatasource('testDatasource'); mockDatasource.publicAPIMock.getOperationForColumnId.mockReturnValue({ diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx index 72d0db1c851b1..fff068937b470 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx @@ -25,7 +25,7 @@ import { VerticalBulletIcon, HorizontalBulletIcon, } from '../../../../../../src/plugins/chart_expressions/expression_gauge/public'; -import type { DatasourcePublicAPI, OperationMetadata, Visualization } from '../../types'; +import type { DatasourceLayers, OperationMetadata, Visualization } from '../../types'; import { getSuggestions } from './suggestions'; import { GROUP_ID, @@ -115,7 +115,7 @@ const checkInvalidConfiguration = (row?: DatatableRow, state?: GaugeVisualizatio const toExpression = ( paletteService: PaletteRegistry, state: GaugeVisualizationState, - datasourceLayers: Record, + datasourceLayers: DatasourceLayers, attributes?: Partial> ): Ast | null => { const datasource = datasourceLayers[state.layerId]; 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 index fbf13db7fa7a5..1e02f929e90ce 100644 --- a/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.test.ts @@ -8,13 +8,17 @@ import { FramePublicAPI } from '../../types'; import { getStaticDate } from './helpers'; +const frame = { + datasourceLayers: {}, + dateRange: { fromDate: '2022-02-01T00:00:00.000Z', toDate: '2022-04-20T00:00:00.000Z' }, +}; + 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 the middle of the date range on when nothing is configured', () => { + expect(getStaticDate([], frame)).toBe('2022-03-12T00:00:00.000Z'); }); - it('should return `now` value on when there is no active data', () => { + it('should return the middle of the date range value on when there is no active data', () => { expect( getStaticDate( [ @@ -26,9 +30,9 @@ describe('annotations helpers', () => { xAccessor: 'a', }, ], - undefined + frame ) - ).toBe('2022-04-08T11:01:58.135Z'); + ).toBe('2022-03-12T00:00:00.000Z'); }); it('should return timestamp value for single active data point', () => { @@ -66,11 +70,57 @@ describe('annotations helpers', () => { xAccessor: 'a', }, ], - activeData as FramePublicAPI['activeData'] + { + ...frame, + activeData: activeData as FramePublicAPI['activeData'], + } ) ).toBe('2022-02-27T23:00:00.000Z'); }); + it('should return the middle of the date range value on when there the active data lies outside of the timerange (auto apply off case)', () => { + const activeData = { + layerId: { + type: 'datatable', + rows: [ + { + a: 1642673600000, // smaller than dateRange.fromDate + 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', + }, + ], + { + ...frame, + activeData: activeData as FramePublicAPI['activeData'], + } + ) + ).toBe('2022-03-12T00:00:00.000Z'); + }); + it('should correctly calculate middle value for active data', () => { const activeData = { layerId: { @@ -118,7 +168,10 @@ describe('annotations helpers', () => { xAccessor: 'a', }, ], - activeData as FramePublicAPI['activeData'] + { + ...frame, + activeData: activeData as FramePublicAPI['activeData'], + } ) ).toBe('2022-03-26T05:00:00.000Z'); }); @@ -202,7 +255,11 @@ describe('annotations helpers', () => { xAccessor: 'd', }, ], - activeData as FramePublicAPI['activeData'] + { + ...frame, + dateRange: { fromDate: '2020-02-01T00:00:00.000Z', toDate: '2022-09-20T00:00:00.000Z' }, + activeData: 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 index c7370c17c6fec..a4f1ecb540c2f 100644 --- a/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx @@ -29,13 +29,14 @@ export const defaultAnnotationLabel = i18n.translate('xpack.lens.xyChart.default defaultMessage: 'Event', }); -export function getStaticDate( - dataLayers: XYDataLayerConfig[], - activeData: FramePublicAPI['activeData'] -) { - const fallbackValue = moment().toISOString(); - +export function getStaticDate(dataLayers: XYDataLayerConfig[], frame: FramePublicAPI) { const dataLayersId = dataLayers.map(({ layerId }) => layerId); + const { activeData, dateRange } = frame; + + const dateRangeMinValue = moment(dateRange.fromDate).valueOf(); + const dateRangeMaxValue = moment(dateRange.toDate).valueOf(); + const fallbackValue = moment((dateRangeMinValue + dateRangeMaxValue) / 2).toISOString(); + if ( !activeData || Object.entries(activeData) @@ -57,7 +58,11 @@ export function getStaticDate( return lastTimestamp && lastTimestamp > acc ? lastTimestamp : acc; }, MIN_DATE); const middleDate = (minDate + maxDate) / 2; - return moment(middleDate).toISOString(); + + if (dateRangeMinValue < middleDate && dateRangeMaxValue > middleDate) { + return moment(middleDate).toISOString(); + } + return fallbackValue; } export const getAnnotationsSupportedLayer = ( @@ -124,7 +129,7 @@ export const setAnnotationsDimension: Visualization['setDimension'] = ( label: defaultAnnotationLabel, key: { type: 'point_in_time', - timestamp: getStaticDate(getDataLayers(prevState.layers), frame?.activeData), + timestamp: getStaticDate(getDataLayers(prevState.layers), frame), }, icon: 'triangle', ...previousConfig, @@ -167,7 +172,7 @@ export const getAnnotationsConfiguration = ({ layer, }: { state: XYState; - frame: FramePublicAPI; + frame: Pick; layer: XYAnnotationLayerConfig; }) => { const dataLayers = getDataLayers(state.layers); 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 af679d1354792..a53f665f7c577 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 @@ -13,7 +13,7 @@ import type { YConfig, } from '../../../../../src/plugins/chart_expressions/expression_xy/common'; import { Datatable } from '../../../../../src/plugins/expressions/public'; -import type { DatasourcePublicAPI, FramePublicAPI, Visualization } from '../types'; +import type { DatasourceLayers, FramePublicAPI, Visualization } from '../types'; import { groupAxesByType } from './axes_configuration'; import { isHorizontalChart, isPercentageSeries, isStackedChart } from './state_helpers'; import type { XYState, XYDataLayerConfig, XYReferenceLineLayerConfig } from './types'; @@ -40,7 +40,7 @@ export interface ReferenceLineBase { export function getGroupsToShow( referenceLayers: T[], state: XYState | undefined, - datasourceLayers: Record, + datasourceLayers: DatasourceLayers, tables: Record | undefined ): Array { if (!state) { @@ -60,7 +60,7 @@ export function getGroupsToShow( referenceLayers: T[], state: XYState | undefined, - datasourceLayers: Record, + datasourceLayers: DatasourceLayers, tables: Record | undefined ): T[] { if (!state) { @@ -75,7 +75,7 @@ export function getGroupsRelatedToData( */ export function getGroupsAvailableInData( dataLayers: XYDataLayerConfig[], - datasourceLayers: Record, + datasourceLayers: DatasourceLayers, tables: Record | undefined ) { const hasNumberHistogram = dataLayers.some( @@ -384,7 +384,7 @@ export const getReferenceConfiguration = ({ sortedAccessors, }: { state: XYState; - frame: FramePublicAPI; + frame: Pick; layer: XYReferenceLineLayerConfig; sortedAccessors: string[]; }) => { 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 7f1bd96b50044..9b54fa0504e11 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -16,7 +16,7 @@ import { XYReferenceLineLayerConfig, XYAnnotationLayerConfig, } from './types'; -import { OperationMetadata, DatasourcePublicAPI } from '../types'; +import { OperationMetadata, DatasourcePublicAPI, DatasourceLayers } from '../types'; import { getColumnToLabelMap } from './state_helpers'; import type { ValidLayer, @@ -50,7 +50,7 @@ export const getSortedAccessors = ( export const toExpression = ( state: State, - datasourceLayers: Record, + datasourceLayers: DatasourceLayers, paletteService: PaletteRegistry, attributes: Partial<{ title: string; description: string }> = {}, eventAnnotationService: EventAnnotationServiceType @@ -107,7 +107,7 @@ const simplifiedLayerExpression = { export function toPreviewExpression( state: State, - datasourceLayers: Record, + datasourceLayers: DatasourceLayers, paletteService: PaletteRegistry, eventAnnotationService: EventAnnotationServiceType ) { @@ -158,7 +158,7 @@ export function getScaleType(metadata: OperationMetadata | null, defaultScale: S export const buildExpression = ( state: State, metadata: Record>, - datasourceLayers: Record, + datasourceLayers: DatasourceLayers, paletteService: PaletteRegistry, attributes: Partial<{ title: string; description: string }> = {}, eventAnnotationService: EventAnnotationServiceType 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 18cd16c17b365..c2e0f314f8008 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -332,16 +332,18 @@ describe('xy_visualization', () => { { columnId: 'c', fields: [] }, ]); - frame.datasourceLayers = { - first: mockDatasource.publicAPIMock, - }; - - frame.activeData = { - first: { - type: 'datatable', - rows: [], - columns: [], + frame = { + datasourceLayers: { + first: mockDatasource.publicAPIMock, + }, + activeData: { + first: { + type: 'datatable', + rows: [], + columns: [], + }, }, + dateRange: { fromDate: '2022-04-10T00:00:00.000Z', toDate: '2022-04-20T00:00:00.000Z' }, }; }); @@ -436,7 +438,6 @@ 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, @@ -463,7 +464,7 @@ describe('xy_visualization', () => { icon: 'triangle', id: 'newCol', key: { - timestamp: '2022-04-18T11:01:58.135Z', + timestamp: '2022-04-15T00:00:00.000Z', type: 'point_in_time', }, label: 'Event', diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index d8fae2657db23..13825ffc55013 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -663,7 +663,7 @@ const getMappedAccessors = ({ layer, }: { accessors: string[]; - frame: FramePublicAPI; + frame: Pick; paletteService: PaletteRegistry; fieldFormats: FieldFormatsStart; state: XYState; 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 d680494ef8315..84e769b5e2623 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { uniq } from 'lodash'; -import { DatasourcePublicAPI, OperationMetadata, VisualizationType } from '../types'; +import { DatasourceLayers, OperationMetadata, VisualizationType } from '../types'; import { State, visualizationTypes, @@ -63,10 +63,7 @@ export function getAxisName( // * 2 or more layers // * at least one with date histogram // * at least one with interval function -export function checkXAccessorCompatibility( - state: XYState, - datasourceLayers: Record -) { +export function checkXAccessorCompatibility(state: XYState, datasourceLayers: DatasourceLayers) { const dataLayers = getDataLayers(state.layers); const errors = []; const hasDateHistogramSet = dataLayers.some( @@ -116,7 +113,7 @@ export function checkXAccessorCompatibility( export function checkScaleOperation( scaleType: 'ordinal' | 'interval' | 'ratio', dataType: 'date' | 'number' | 'string' | undefined, - datasourceLayers: Record + datasourceLayers: DatasourceLayers ) { return (layer: XYDataLayerConfig) => { const datasourceAPI = datasourceLayers[layer.layerId]; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.test.tsx index 87ccf41e91143..b5c45cd517432 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.test.tsx @@ -89,11 +89,9 @@ describe('Axes Settings', () => { expect(props.setOrientation).toHaveBeenCalled(); }); - it('should disable the orientation group if the tickLabels are set to not visible', () => { + it('should hide the orientation group if the tickLabels are set to not visible', () => { const component = shallow(); - expect( - component.find('[data-test-subj="lnsXY_axisOrientation_groups"]').prop('isDisabled') - ).toEqual(true); + expect(component.exists('[data-test-subj="lnsXY_axisOrientation_groups"]')).toEqual(false); }); it('hides the endzone visibility flag if no setter is passed in', () => { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.tsx index 340a9211fcdee..e9a970cb82987 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.tsx @@ -22,12 +22,7 @@ import { AxisExtentConfig, } from '../../../../../../src/plugins/chart_expressions/expression_xy/common'; import { XYLayerConfig } from '../types'; -import { - ToolbarPopover, - useDebouncedValue, - TooltipWrapper, - AxisTitleSettings, -} from '../../shared_components'; +import { ToolbarPopover, useDebouncedValue, AxisTitleSettings } from '../../shared_components'; import { isHorizontalChart } from '../state_helpers'; import { EuiIconAxisBottom } from '../../assets/axis_bottom'; import { EuiIconAxisLeft } from '../../assets/axis_left'; @@ -312,20 +307,13 @@ export const AxisSettingsPopover: React.FunctionComponent - - value === orientation)!.id} @@ -345,8 +332,8 @@ export const AxisSettingsPopover: React.FunctionComponent - - + + )} {setEndzoneVisibility && ( { + const lensState = state as unknown as { attributes: LensDocShape810 }; + const migratedLensState = commonLockOldMetricVisSettings(lensState.attributes); + return { + ...lensState, + attributes: migratedLensState, + } as unknown as SerializableRecord; + }, }), getLensCustomVisualizationMigrations(customVisualizationMigrations) ), diff --git a/x-pack/plugins/lens/server/migrations/common_migrations.ts b/x-pack/plugins/lens/server/migrations/common_migrations.ts index 0a76b0a5e6b45..039da7662d84c 100644 --- a/x-pack/plugins/lens/server/migrations/common_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/common_migrations.ts @@ -28,7 +28,7 @@ import { CustomVisualizationMigrations, LensDocShape810, } from './types'; -import { DOCUMENT_FIELD_NAME, layerTypes } from '../../common'; +import { DOCUMENT_FIELD_NAME, layerTypes, MetricState } from '../../common'; import { LensDocShape } from './saved_object_migrations'; export const commonRenameOperationsForFormula = ( @@ -239,6 +239,21 @@ export const commonSetIncludeEmptyRowsDateHistogram = ( return newAttributes; }; +export const commonLockOldMetricVisSettings = ( + attributes: LensDocShape810 +): LensDocShape810 => { + const newAttributes = cloneDeep(attributes); + if (newAttributes.visualizationType !== 'lnsMetric') { + return newAttributes; + } + + const visState = newAttributes.state.visualization as MetricState; + visState.textAlign = visState.textAlign ?? 'center'; + visState.titlePosition = visState.titlePosition ?? 'bottom'; + visState.size = visState.size ?? 'xl'; + return newAttributes; +}; + const getApplyCustomVisualizationMigrationToLens = (id: string, migration: MigrateFunction) => { return (savedObject: { attributes: LensDocShape }) => { if (savedObject.attributes.visualizationType !== id) return savedObject; diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts index 5d93e648124b1..7c6e8345b71de 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts @@ -22,7 +22,7 @@ import { VisState810, VisState820, } from './types'; -import { layerTypes } from '../../common'; +import { layerTypes, MetricState } from '../../common'; import { Filter } from '@kbn/es-query'; describe('Lens migrations', () => { @@ -2064,4 +2064,53 @@ describe('Lens migrations', () => { expect(layer2Columns['4'].params).toHaveProperty('includeEmptyRows', true); }); }); + describe('8.3.0 old metric visualization defaults', () => { + const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext; + const example = { + type: 'lens', + id: 'mocked-saved-object-id', + attributes: { + savedObjectId: '1', + title: 'MyRenamedOps', + description: '', + visualizationType: 'lnsMetric', + state: { + visualization: {}, + }, + }, + } as unknown as SavedObjectUnsanitizedDoc; + + it('preserves current config for existing visualizations that are using the DEFAULTS', () => { + const result = migrations['8.3.0'](example, context) as ReturnType< + SavedObjectMigrationFn + >; + const visState = result.attributes.state.visualization as MetricState; + expect(visState.textAlign).toBe('center'); + expect(visState.titlePosition).toBe('bottom'); + expect(visState.size).toBe('xl'); + }); + + it('preserves current config for existing visualizations that are using CUSTOM settings', () => { + const result = migrations['8.3.0']( + { + ...example, + attributes: { + ...example.attributes, + state: { + visualization: { + textAlign: 'right', + titlePosition: 'top', + size: 's', + }, + }, + }, + }, + context + ) as ReturnType>; + const visState = result.attributes.state.visualization as MetricState; + expect(visState.textAlign).toBe('right'); + expect(visState.titlePosition).toBe('top'); + expect(visState.size).toBe('s'); + }); + }); }); diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts index 6fb8044baf8ea..17243ae9c8723 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts @@ -44,6 +44,7 @@ import { commonSetLastValueShowArrayValues, commonEnhanceTableRowHeight, commonSetIncludeEmptyRowsDateHistogram, + commonLockOldMetricVisSettings, } from './common_migrations'; interface LensDocShapePre710 { @@ -485,6 +486,10 @@ const setIncludeEmptyRowsDateHistogram: SavedObjectMigrationFn = ( + doc +) => ({ ...doc, attributes: commonLockOldMetricVisSettings(doc.attributes) }); + const lensMigrations: SavedObjectMigrationMap = { '7.7.0': removeInvalidAccessors, // The order of these migrations matter, since the timefield migration relies on the aggConfigs @@ -504,6 +509,7 @@ const lensMigrations: SavedObjectMigrationMap = { setIncludeEmptyRowsDateHistogram, enhanceTableRowHeight ), + '8.3.0': lockOldMetricVisSettings, }; export const getAllMigrations = ( diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index 8b51fb5fbebc6..5cfd5ee43fc8c 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -6,10 +6,10 @@ */ import type { CoreStart } from 'kibana/public'; +import type { PaletteRegistry } from '@kbn/coloring'; import type { MapsConfigType } from '../config'; import type { MapsPluginStartDependencies } from './plugin'; import type { EMSSettings } from '../../../../src/plugins/maps_ems/common/ems_settings'; -import type { PaletteRegistry } from '../../../../src/plugins/charts/public'; import { MapsEmsPluginPublicStart } from '../../../../src/plugins/maps_ems/public'; let coreStart: CoreStart; diff --git a/x-pack/plugins/maps/public/lens/choropleth_chart/visualization.tsx b/x-pack/plugins/maps/public/lens/choropleth_chart/visualization.tsx index 93c3bec52471e..0f8e4ff705bbe 100644 --- a/x-pack/plugins/maps/public/lens/choropleth_chart/visualization.tsx +++ b/x-pack/plugins/maps/public/lens/choropleth_chart/visualization.tsx @@ -10,8 +10,8 @@ import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n-react'; import { render } from 'react-dom'; import type { FileLayer } from '@elastic/ems-client'; +import type { PaletteRegistry } from '@kbn/coloring'; import { ThemeServiceStart } from 'kibana/public'; -import type { PaletteRegistry } from 'src/plugins/charts/public'; import { KibanaThemeProvider } from '../../../../../../src/plugins/kibana_react/public'; import { getSuggestions } from './suggestions'; import { layerTypes } from '../../../../lens/public'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index 775c989df2aec..3cb2c2e95fa43 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { PaletteOutput } from 'src/plugins/charts/public'; -import { ExistsFilter, PhraseFilter } from '@kbn/es-query'; -import { +import type { PaletteOutput } from '@kbn/coloring'; +import type { ExistsFilter, PhraseFilter } from '@kbn/es-query'; +import type { LastValueIndexPatternColumn, DateHistogramIndexPatternColumn, FieldBasedIndexPatternColumn, @@ -16,7 +16,7 @@ import { YConfig, } from '../../../../../lens/public'; -import { PersistableFilter } from '../../../../../lens/common'; +import type { PersistableFilter } from '../../../../../lens/common'; import type { DataView } from '../../../../../../../src/plugins/data_views/common'; export const ReportViewTypes = { diff --git a/x-pack/plugins/observability/public/pages/rules/index.tsx b/x-pack/plugins/observability/public/pages/rules/index.tsx index 02137f13e3ebd..64b8cf61da3e6 100644 --- a/x-pack/plugins/observability/public/pages/rules/index.tsx +++ b/x-pack/plugins/observability/public/pages/rules/index.tsx @@ -83,7 +83,7 @@ export function RulesPage() { application: { capabilities }, notifications: { toasts }, } = useKibana().services; - const documentationLink = docLinks.links.alerting.guide; + const documentationLink = docLinks.links.observability.createAlerts; const ruleTypeRegistry = triggersActionsUi.ruleTypeRegistry; const canExecuteActions = hasExecuteActionsCapability(capabilities); const [page, setPage] = useState({ index: 0, size: DEFAULT_SEARCH_PAGE_SIZE }); diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 026cfd273905e..63a626055c7cb 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -16,7 +16,7 @@ export const allowedExperimentalValues = Object.freeze({ tGridEnabled: true, tGridEventRenderedViewEnabled: true, excludePoliciesInFilterEnabled: false, - usersEnabled: false, + usersEnabled: true, detectionResponseEnabled: false, disableIsolationUIPendingStatuses: false, riskyHostsEnabled: false, diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/attach_alert_to_case.spec.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/cases/attach_alert_to_case.spec.ts diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index a899f5948c780..fb0a9a7ed1d78 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -13,6 +13,7 @@ "cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/security_solution_cypress/visual_config.ts", "cypress:open:upgrade": "yarn cypress:open --config integrationFolder=./cypress/upgrade_integration", "cypress:run": "yarn cypress:run:reporter --browser chrome --spec './cypress/integration/**/*.spec.ts'; status=$?; yarn junit:merge && exit $status", + "cypress:run:cases": "yarn cypress:run:reporter --browser chrome --spec './cypress/integration/cases/*.spec.ts'; status=$?; yarn junit:merge && exit $status", "cypress:run:firefox": "yarn cypress:run:reporter --browser firefox --spec './cypress/integration/**/*.spec.ts'; status=$?; yarn junit:merge && exit $status", "cypress:run:reporter": "yarn cypress run --config-file ./cypress/cypress.json --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json", "cypress:run:ccs": "yarn cypress:run:reporter --browser chrome --config integrationFolder=./cypress/ccs_integration; status=$?; yarn junit:merge && exit $status", diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts index 0676c516ede64..118d73cbedf1d 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts @@ -142,7 +142,11 @@ describe('deepLinks', () => { describe('experimental flags', () => { it('should return NO users link when enableExperimental.usersEnabled === false', () => { - const deepLinks = getDeepLinks(mockGlobalState.app.enableExperimental); + const deepLinks = getDeepLinks({ + ...mockGlobalState.app.enableExperimental, + usersEnabled: false, + }); + expect(findDeepLink(SecurityPageName.users, deepLinks)).toBeFalsy(); }); diff --git a/x-pack/plugins/security_solution/public/common/components/empty_value/__snapshots__/empty_value.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/empty_value/__snapshots__/empty_value.test.tsx.snap index 142ed7a0d7175..77edd4e1fe991 100644 --- a/x-pack/plugins/security_solution/public/common/components/empty_value/__snapshots__/empty_value.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/empty_value/__snapshots__/empty_value.test.tsx.snap @@ -2,6 +2,6 @@ exports[`EmptyValue it renders against snapshot 1`] = `

- (Empty String) + (Empty string)

`; diff --git a/x-pack/plugins/security_solution/public/common/components/empty_value/empty_value.test.tsx b/x-pack/plugins/security_solution/public/common/components/empty_value/empty_value.test.tsx index c455ed1953c28..c869bb5e6810c 100644 --- a/x-pack/plugins/security_solution/public/common/components/empty_value/empty_value.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/empty_value/empty_value.test.tsx @@ -39,7 +39,7 @@ describe('EmptyValue', () => {

{getEmptyString()}

); - expect(wrapper.text()).toBe('(Empty String)'); + expect(wrapper.text()).toBe('(Empty string)'); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/empty_value/translations.ts b/x-pack/plugins/security_solution/public/common/components/empty_value/translations.ts index d598f4856413e..9eaddfd8650f0 100644 --- a/x-pack/plugins/security_solution/public/common/components/empty_value/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/empty_value/translations.ts @@ -10,6 +10,6 @@ import { i18n } from '@kbn/i18n'; export const EMPTY_STRING = i18n.translate( 'xpack.securitySolution.emptyString.emptyStringDescription', { - defaultMessage: 'Empty String', + defaultMessage: 'Empty string', } ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.tsx index d3cf8c3840497..0d0803f41c34b 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.tsx @@ -6,7 +6,7 @@ */ import { EuiFlexGroup, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; -import React from 'react'; +import React, { useState } from 'react'; import { ActionCell } from '../table/action_cell'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; @@ -19,7 +19,9 @@ const ActionWrapper = euiStyled.div` margin-left: ${({ theme }) => theme.eui.paddingSizes.s}; `; -const OverviewPanel = euiStyled(EuiPanel)` +const OverviewPanel = euiStyled(EuiPanel)<{ + $isPopoverVisible: boolean; +}>` &&& { background-color: ${({ theme }) => theme.eui.euiColorLightestShade}; padding: ${({ theme }) => theme.eui.paddingSizes.s}; @@ -45,15 +47,35 @@ const OverviewPanel = euiStyled(EuiPanel)` transform: translate(0); } } + + ${(props) => + props.$isPopoverVisible && + ` + & ${ActionWrapper} { + width: auto; + transform: translate(0); + } + `} } `; interface OverviewCardProps { + isPopoverVisible?: boolean; // Prevent the hover actions from collapsing on each other when not directly hovered on title: string; } -export const OverviewCard: React.FC = ({ title, children }) => ( - +export const OverviewCard: React.FC = ({ + title, + children, + isPopoverVisible = false, // default to false as this behavior is only really necessary in the situation without an overflow +}) => ( + {title} {children} @@ -85,13 +107,20 @@ export const OverviewCardWithActions: React.FC = ( dataTestSubj, enrichedFieldInfo, }) => { + const [isPopoverVisisble, setIsPopoverVisible] = useState(false); + return ( - + {children} - + diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx index 523b56e9ecf76..a223b49b5558c 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx @@ -18,6 +18,7 @@ interface Props extends EnrichedFieldInfo { disabled?: boolean; getLinkValue?: (field: string) => string | null; onFilterAdded?: () => void; + setIsPopoverVisible?: (isVisible: boolean) => void; toggleColumn?: (column: ColumnHeaderOptions) => void; hideAddToTimeline?: boolean; } @@ -32,6 +33,7 @@ export const ActionCell: React.FC = React.memo( getLinkValue, linkValue, onFilterAdded, + setIsPopoverVisible, timelineId, toggleColumn, values, @@ -55,9 +57,10 @@ export const ActionCell: React.FC = React.memo( const toggleTopN = useCallback(() => { setShowTopN((prevShowTopN) => { const newShowTopN = !prevShowTopN; + if (setIsPopoverVisible) setIsPopoverVisible(newShowTopN); return newShowTopN; }); - }, []); + }, [setIsPopoverVisible]); const closeTopN = useCallback(() => { setShowTopN(false); diff --git a/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx b/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx index d5624421c0006..48b4acbf4d008 100644 --- a/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx @@ -8,7 +8,7 @@ import { set } from '@elastic/safer-lodash-set/fp'; import { getOr } from 'lodash/fp'; import React, { memo, useEffect, useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { connect, ConnectedProps, useDispatch } from 'react-redux'; import { Dispatch } from 'redux'; import { Subscription } from 'rxjs'; import styled from 'styled-components'; @@ -35,10 +35,11 @@ import { startSelector, toStrSelector, } from './selectors'; -import { hostsActions } from '../../../hosts/store'; -import { networkActions } from '../../../network/store'; import { timelineActions } from '../../../timelines/store/timeline'; import { useKibana } from '../../lib/kibana'; +import { usersActions } from '../../../users/store'; +import { hostsActions } from '../../../hosts/store'; +import { networkActions } from '../../../network/store'; const APP_STATE_STORAGE_KEY = 'securitySolution.searchBar.appState'; @@ -91,16 +92,25 @@ export const SearchBarComponent = memo( }, } = useKibana().services; + const dispatch = useDispatch(); + const setTablesActivePageToZero = useCallback(() => { + dispatch(usersActions.setUsersTablesActivePageToZero()); + dispatch(hostsActions.setHostTablesActivePageToZero()); + dispatch(networkActions.setNetworkTablesActivePageToZero()); + }, [dispatch]); + useEffect(() => { if (fromStr != null && toStr != null) { timefilter.setTime({ from: fromStr, to: toStr }); } else if (start != null && end != null) { + setTablesActivePageToZero(); + timefilter.setTime({ from: new Date(start).toISOString(), to: new Date(end).toISOString(), }); } - }, [end, fromStr, start, timefilter, toStr]); + }, [end, fromStr, start, timefilter, toStr, setTablesActivePageToZero]); const onQuerySubmit = useCallback( (payload: { dateRange: TimeRange; query?: Query }) => { @@ -119,6 +129,7 @@ export const SearchBarComponent = memo( isQuickSelection, updateTime: false, filterManager, + setTablesActivePageToZero, }; let isStateUpdated = false; @@ -164,6 +175,7 @@ export const SearchBarComponent = memo( filterQuery, queries, updateSearch, + setTablesActivePageToZero, ] ); @@ -178,12 +190,13 @@ export const SearchBarComponent = memo( isQuickSelection: true, updateTime: true, filterManager, + setTablesActivePageToZero, }); } else { queries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)()); } }, - [updateSearch, id, filterManager, queries] + [updateSearch, id, filterManager, queries, setTablesActivePageToZero] ); const onSaved = useCallback( @@ -209,6 +222,7 @@ export const SearchBarComponent = memo( isQuickSelection, updateTime: false, filterManager, + setTablesActivePageToZero, }; if (savedQueryUpdated.attributes.timefilter) { @@ -226,7 +240,7 @@ export const SearchBarComponent = memo( updateSearch(updateSearchBar); }, - [id, toStr, end, fromStr, start, filterManager, updateSearch] + [id, toStr, end, fromStr, start, filterManager, updateSearch, setTablesActivePageToZero] ); const onClearSavedQuery = useCallback(() => { @@ -246,9 +260,20 @@ export const SearchBarComponent = memo( resetSavedQuery: true, savedQuery: undefined, filterManager, + setTablesActivePageToZero, }); } - }, [savedQuery, updateSearch, id, toStr, end, fromStr, start, filterManager]); + }, [ + savedQuery, + updateSearch, + id, + toStr, + end, + fromStr, + start, + filterManager, + setTablesActivePageToZero, + ]); const saveAppStateToStorage = useCallback( (filters: Filter[]) => storage.set(APP_STATE_STORAGE_KEY, filters), @@ -273,6 +298,8 @@ export const SearchBarComponent = memo( id, filters: filterManager.getFilters(), }); + + setTablesActivePageToZero(); } }, }) @@ -369,6 +396,7 @@ interface UpdateReduxSearchBar extends OnTimeChangeProps { resetSavedQuery?: boolean; timelineId?: string; updateTime: boolean; + setTablesActivePageToZero: () => void; } export const dispatchUpdateSearch = @@ -385,6 +413,7 @@ export const dispatchUpdateSearch = timelineId, filterManager, updateTime = false, + setTablesActivePageToZero, }: UpdateReduxSearchBar): void => { if (updateTime) { const fromDate = formatDate(start); @@ -446,8 +475,7 @@ export const dispatchUpdateSearch = dispatch(inputsActions.setSavedQuery({ id, savedQuery })); } - dispatch(hostsActions.setHostTablesActivePageToZero()); - dispatch(networkActions.setNetworkTablesActivePageToZero()); + setTablesActivePageToZero(); }; const mapDispatchToProps = (dispatch: Dispatch) => ({ diff --git a/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx index bc397292af4ec..36acb0abc219e 100644 --- a/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx @@ -58,7 +58,7 @@ describe('Table Helpers', () => { }); const wrapper = mount({rowItem}); expect(wrapper.find('[data-test-subj="render-content-attrName"]').first().text()).toBe( - '(Empty String)' + '(Empty string)' ); }); @@ -119,7 +119,7 @@ describe('Table Helpers', () => { }); const wrapper = mount({rowItems}); expect(wrapper.find('[data-test-subj="render-content-attrName"]').first().text()).toBe( - '(Empty String)' + '(Empty string)' ); }); 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 27f33409ae1a5..a72746f729ad5 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 @@ -64,6 +64,7 @@ export const StackByComboBox: React.FC = ({ selected, onSe placeholder={i18n.STACK_BY_PLACEHOLDER} prepend={i18n.STACK_BY_LABEL} singleSelection={singleSelection} + isClearable={false} sortMatchesBy="startsWith" options={stackOptions} selectedOptions={selectedOptions} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/translations.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/translations.tsx index d35a89484f264..3908c890b1f89 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/translations.tsx @@ -13,14 +13,14 @@ export const bulkApplyTimelineTemplate = { FORM_TITLE: i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.applyTimelineTemplate.formTitle', { - defaultMessage: 'Apply timeline template', + defaultMessage: 'Apply Timeline template', } ), TEMPLATE_SELECTOR_LABEL: i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.applyTimelineTemplate.templateSelectorLabel', { - defaultMessage: 'Apply timeline template to selected rules', + defaultMessage: 'Apply Timeline template to selected rules', } ), @@ -28,7 +28,7 @@ export const bulkApplyTimelineTemplate = { 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.applyTimelineTemplate.templateSelectorHelpText', { defaultMessage: - 'Select which timeline to apply to selected rules when investigating generated alerts.', + 'Select which Timeline to apply to selected rules when investigating generated alerts.', } ), @@ -42,8 +42,8 @@ export const bulkApplyTimelineTemplate = { warningCalloutMessage: (rulesCount: number): JSX.Element => ( ), 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 09949cc5c1a09..7071ef95c8c7e 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 @@ -196,7 +196,7 @@ export const BULK_ACTION_DELETE_TAGS = i18n.translate( export const BULK_ACTION_APPLY_TIMELINE_TEMPLATE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.applyTimelineTemplateTitle', { - defaultMessage: 'Apply timeline template', + defaultMessage: 'Apply Timeline template', } ); diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.test.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.test.tsx index 383a72031ed76..86e66319891f1 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.test.tsx @@ -18,7 +18,9 @@ import { jest.mock('../../../common/components/user_privileges'); -describe('When using the ArtifactListPage component', () => { +// FLAKY: https://github.com/elastic/kibana/issues/129837 +// FLAKY: https://github.com/elastic/kibana/issues/129836 +describe.skip('When using the ArtifactListPage component', () => { let render: ( props?: Partial ) => ReturnType; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.tsx index 4b126d6e747db..9120042318f5b 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.tsx @@ -7,6 +7,7 @@ import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; +import { DocLinks } from '@kbn/doc-links'; import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { EuiButton, @@ -20,6 +21,7 @@ import { EuiFlyoutHeader, EuiTitle, } from '@elastic/eui'; + import { EuiFlyoutSize } from '@elastic/eui/src/components/flyout/flyout'; import { HttpFetchError } from 'kibana/public'; import { useUrlParams } from '../../hooks/use_url_params'; @@ -33,7 +35,7 @@ import { } from '../types'; import { ManagementPageLoader } from '../../management_page_loader'; import { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client'; -import { useToasts } from '../../../../common/lib/kibana'; +import { useKibana, useToasts } from '../../../../common/lib/kibana'; import { createExceptionListItemForCreate } from '../../../../../common/endpoint/service/artifacts/utils'; import { useWithArtifactSubmitData } from '../hooks/use_with_artifact_submit_data'; import { useIsArtifactAllowedPerPolicyUsage } from '../hooks/use_is_artifact_allowed_per_policy_usage'; @@ -96,7 +98,7 @@ export const ARTIFACT_FLYOUT_LABELS = Object.freeze({ * ); * } */ - flyoutDowngradedLicenseDocsInfo: (): React.ReactNode => + flyoutDowngradedLicenseDocsInfo: (_: DocLinks['securitySolution']): React.ReactNode => i18n.translate('xpack.securitySolution.artifactListPage.flyoutDowngradedLicenseDocsInfo', { defaultMessage: 'For more information, see our documentation.', }), @@ -188,6 +190,11 @@ export const ArtifactFlyout = memo( 'data-test-subj': dataTestSubj, size = 'm', }) => { + const { + docLinks: { + links: { securitySolution }, + }, + } = useKibana().services; const getTestId = useTestIdGenerator(dataTestSubj); const toasts = useToasts(); const isFlyoutOpened = useIsFlyoutOpened(); @@ -364,7 +371,8 @@ export const ArtifactFlyout = memo( iconType="help" data-test-subj={getTestId('expiredLicenseCallout')} > - {`${labels.flyoutDowngradedLicenseInfo} ${labels.flyoutDowngradedLicenseDocsInfo()}`} + {labels.flyoutDowngradedLicenseInfo}{' '} + {labels.flyoutDowngradedLicenseDocsInfo(securitySolution)} )} diff --git a/x-pack/plugins/security_solution/public/management/components/no_permissons/index.ts b/x-pack/plugins/security_solution/public/management/components/no_permissons/index.ts new file mode 100644 index 0000000000000..25421ba1dcd1a --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/no_permissons/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 { NoPermissions } from './no_permissions'; diff --git a/x-pack/plugins/security_solution/public/management/components/no_permissons/no_permissions.tsx b/x-pack/plugins/security_solution/public/management/components/no_permissons/no_permissions.tsx new file mode 100644 index 0000000000000..87a5b5cf7142b --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/no_permissons/no_permissions.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { EuiEmptyPrompt, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +export const NoPermissions = memo(() => { + return ( + <> + + } + body={ + + + + } + /> + + ); +}); +NoPermissions.displayName = 'NoPermissions'; diff --git a/x-pack/plugins/security_solution/public/management/pages/blocklist/translations.ts b/x-pack/plugins/security_solution/public/management/pages/blocklist/translations.ts index 4a0901798841b..1ce96f5267916 100644 --- a/x-pack/plugins/security_solution/public/management/pages/blocklist/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/blocklist/translations.ts @@ -8,95 +8,95 @@ import { i18n } from '@kbn/i18n'; import { BlocklistConditionEntryField } from '@kbn/securitysolution-utils'; -export const DETAILS_HEADER = i18n.translate('xpack.securitySolution.blocklists.details.header', { +export const DETAILS_HEADER = i18n.translate('xpack.securitySolution.blocklist.details.header', { defaultMessage: 'Details', }); export const DETAILS_HEADER_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.blocklists.details.header.description', + 'xpack.securitySolution.blocklist.details.header.description', { defaultMessage: 'The blocklist prevents selected applications from running on your hosts by extending the list of processes the Endpoint considers malicious.', } ); -export const NAME_LABEL = i18n.translate('xpack.securitySolution.blocklists.name.label', { +export const NAME_LABEL = i18n.translate('xpack.securitySolution.blocklist.name.label', { defaultMessage: 'Name', }); export const DESCRIPTION_LABEL = i18n.translate( - 'xpack.securitySolution.blocklists.description.label', + 'xpack.securitySolution.blocklist.description.label', { defaultMessage: 'Description', } ); export const CONDITIONS_HEADER = i18n.translate( - 'xpack.securitySolution.blocklists.conditions.header', + 'xpack.securitySolution.blocklist.conditions.header', { defaultMessage: 'Conditions', } ); export const CONDITIONS_HEADER_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.blocklists.conditions.header.description', + 'xpack.securitySolution.blocklist.conditions.header.description', { defaultMessage: 'Select an operating system and add conditions. Availability of conditions may depend on your chosen OS.', } ); -export const SELECT_OS_LABEL = i18n.translate('xpack.securitySolution.blocklists.os.label', { +export const SELECT_OS_LABEL = i18n.translate('xpack.securitySolution.blocklist.os.label', { defaultMessage: 'Select operating system', }); -export const FIELD_LABEL = i18n.translate('xpack.securitySolution.blocklists.field.label', { +export const FIELD_LABEL = i18n.translate('xpack.securitySolution.blocklist.field.label', { defaultMessage: 'Field', }); -export const OPERATOR_LABEL = i18n.translate('xpack.securitySolution.blocklists.operator.label', { +export const OPERATOR_LABEL = i18n.translate('xpack.securitySolution.blocklist.operator.label', { defaultMessage: 'Operator', }); -export const VALUE_LABEL = i18n.translate('xpack.securitySolution.blocklists.value.label', { +export const VALUE_LABEL = i18n.translate('xpack.securitySolution.blocklist.value.label', { defaultMessage: 'Value', }); export const VALUE_LABEL_HELPER = i18n.translate( - 'xpack.securitySolution.blocklists.value.label.helper', + 'xpack.securitySolution.blocklist.value.label.helper', { defaultMessage: 'Type or copy & paste one or multiple comma delimited values', } ); export const CONDITION_FIELD_TITLE: { [K in BlocklistConditionEntryField]: string } = { - 'file.hash.*': i18n.translate('xpack.securitySolution.blocklists.entry.field.hash', { + 'file.hash.*': i18n.translate('xpack.securitySolution.blocklist.entry.field.hash', { defaultMessage: 'Hash', }), - 'file.path': i18n.translate('xpack.securitySolution.blocklists.entry.field.path', { + 'file.path': i18n.translate('xpack.securitySolution.blocklist.entry.field.path', { defaultMessage: 'Path', }), 'file.Ext.code_signature': i18n.translate( - 'xpack.securitySolution.blocklists.entry.field.signature', + 'xpack.securitySolution.blocklist.entry.field.signature', { defaultMessage: 'Signature' } ), }; export const CONDITION_FIELD_DESCRIPTION: { [K in BlocklistConditionEntryField]: string } = { - 'file.hash.*': i18n.translate('xpack.securitySolution.blocklists.entry.field.description.hash', { + 'file.hash.*': i18n.translate('xpack.securitySolution.blocklist.entry.field.description.hash', { defaultMessage: 'md5, sha1, or sha256', }), - 'file.path': i18n.translate('xpack.securitySolution.blocklists.entry.field.description.path', { + 'file.path': i18n.translate('xpack.securitySolution.blocklist.entry.field.description.path', { defaultMessage: 'The full path of the application', }), 'file.Ext.code_signature': i18n.translate( - 'xpack.securitySolution.blocklists.entry.field.description.signature', + 'xpack.securitySolution.blocklist.entry.field.description.signature', { defaultMessage: 'The signer of the application' } ), }; export const POLICY_SELECT_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.blocklists.policyAssignmentSectionDescription', + 'xpack.securitySolution.blocklist.policyAssignmentSectionDescription', { defaultMessage: 'Assign this blocklist globally across all policies, or assign it to specific policies.', @@ -104,26 +104,32 @@ export const POLICY_SELECT_DESCRIPTION = i18n.translate( ); export const ERRORS = { - NAME_REQUIRED: i18n.translate('xpack.securitySolution.blocklists.errors.name.required', { + NAME_REQUIRED: i18n.translate('xpack.securitySolution.blocklist.errors.name.required', { defaultMessage: 'Name is required', }), - VALUE_REQUIRED: i18n.translate('xpack.securitySolution.blocklists.errors.values.required', { + VALUE_REQUIRED: i18n.translate('xpack.securitySolution.blocklist.errors.values.required', { defaultMessage: 'Field entry must have a value', }), - INVALID_HASH: i18n.translate('xpack.securitySolution.blocklists.errors.values.invalidHash', { + INVALID_HASH: i18n.translate('xpack.securitySolution.blocklist.errors.values.invalidHash', { defaultMessage: 'Invalid hash value', }), - INVALID_PATH: i18n.translate('xpack.securitySolution.blocklists.warnings.values.invalidPath', { + INVALID_PATH: i18n.translate('xpack.securitySolution.blocklist.warnings.values.invalidPath', { defaultMessage: 'Path may be formed incorrectly; verify value', }), + WILDCARD_PRESENT: i18n.translate( + 'xpack.securitySolution.blocklist.warnings.values.wildcardPresent', + { + defaultMessage: "A wildcard in the filename will affect the endpoint's performance", + } + ), DUPLICATE_VALUE: i18n.translate( - 'xpack.securitySolution.blocklists.warnings.values.duplicateValue', + 'xpack.securitySolution.blocklist.warnings.values.duplicateValue', { defaultMessage: 'This value already exists', } ), DUPLICATE_VALUES: i18n.translate( - 'xpack.securitySolution.blocklists.warnings.values.duplicateValues', + 'xpack.securitySolution.blocklist.warnings.values.duplicateValues', { defaultMessage: 'One or more duplicate values removed', } diff --git a/x-pack/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx b/x-pack/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx index c4d4edb5e1331..a48d770460ec9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx @@ -7,6 +7,9 @@ import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { DocLinks } from '@kbn/doc-links'; +import { EuiLink } from '@elastic/eui'; import { useHttp } from '../../../../common/lib/kibana'; import { ArtifactListPage, ArtifactListPageProps } from '../../../components/artifact_list_page'; @@ -48,7 +51,7 @@ const BLOCKLIST_PAGE_LABELS: ArtifactListPageProps['labels'] = { ), flyoutCreateSubmitSuccess: ({ name }) => i18n.translate('xpack.securitySolution.blocklist.flyoutCreateSubmitSuccess', { - defaultMessage: '"{name}" has been added to your blocklist.', // FIXME: match this to design (needs count of items) + defaultMessage: '"{name}" has been added to your blocklist.', values: { name }, }), flyoutEditSubmitSuccess: ({ name }) => @@ -56,28 +59,23 @@ const BLOCKLIST_PAGE_LABELS: ArtifactListPageProps['labels'] = { defaultMessage: '"{name}" has been updated.', values: { name }, }), - flyoutDowngradedLicenseDocsInfo: () => { - return 'tbd...'; - // FIXME: define docs link for license downgrade message. sample code below - - // const { docLinks } = useKibana().services; - // return ( - // - // {' '} - // {' '} - // - // ), - // }} - // /> - // ); + flyoutDowngradedLicenseDocsInfo: ( + securitySolutionDocsLinks: DocLinks['securitySolution'] + ): React.ReactNode => { + return ( + <> + + + + + + ); }, deleteActionSuccess: (itemName) => i18n.translate('xpack.securitySolution.blocklist.deleteSuccess', { @@ -85,15 +83,15 @@ const BLOCKLIST_PAGE_LABELS: ArtifactListPageProps['labels'] = { values: { itemName }, }), emptyStateTitle: i18n.translate('xpack.securitySolution.blocklist.emptyStateTitle', { - defaultMessage: 'Add your first blocklist', + defaultMessage: 'Add your first blocklist entry', + }), + emptyStateInfo: i18n.translate('xpack.securitySolution.blocklist.emptyStateInfo', { + defaultMessage: + 'The blocklist prevents selected applications from running on your hosts by extending the list of processes the Endpoint considers malicious.', }), - emptyStateInfo: i18n.translate( - 'xpack.securitySolution.blocklist.emptyStateInfo', - { defaultMessage: 'Add a blocklist to prevent execution on the endpoint' } // FIXME: need wording here form PM - ), emptyStatePrimaryButtonLabel: i18n.translate( 'xpack.securitySolution.blocklist.emptyStatePrimaryButtonLabel', - { defaultMessage: 'Add blocklist' } + { defaultMessage: 'Add blocklist entry' } ), searchPlaceholderInfo: i18n.translate('xpack.securitySolution.blocklist.searchPlaceholderInfo', { defaultMessage: 'Search on the fields below: name, description, value', diff --git a/x-pack/plugins/security_solution/public/management/pages/index.tsx b/x-pack/plugins/security_solution/public/management/pages/index.tsx index fb23b7466623c..cc90aca4b8022 100644 --- a/x-pack/plugins/security_solution/public/management/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/index.tsx @@ -7,8 +7,7 @@ import React, { memo } from 'react'; import { Route, Switch, Redirect } from 'react-router-dom'; -import { EuiEmptyPrompt, EuiLoadingSpinner, EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { MANAGEMENT_ROUTING_ENDPOINTS_PATH, MANAGEMENT_ROUTING_EVENT_FILTERS_PATH, @@ -29,35 +28,7 @@ import { getEndpointListPath } from '../common/routing'; import { useUserPrivileges } from '../../common/components/user_privileges'; import { HostIsolationExceptionsContainer } from './host_isolation_exceptions'; import { BlocklistContainer } from './blocklist'; - -const NoPermissions = memo(() => { - return ( - <> - - } - body={ - - - - } - /> - - - ); -}); -NoPermissions.displayName = 'NoPermissions'; +import { NoPermissions } from '../components/no_permissons'; const EndpointTelemetry = () => ( @@ -99,7 +70,12 @@ export const ManagementContainer = memo(() => { } if (!canAccessEndpointManagement) { - return ; + return ( + <> + + + + ); } return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx index b6b91d8d64e4f..94c9c073d483c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx @@ -23,6 +23,8 @@ import { EventFiltersApiClient } from '../../../../event_filters/service/event_f import { HostIsolationExceptionsApiClient } from '../../../../host_isolation_exceptions/host_isolation_exceptions_api_client'; import { BlocklistsApiClient } from '../../../../blocklist/services'; import { useCanSeeHostIsolationExceptionsMenu } from '../../../../host_isolation_exceptions/view/hooks'; +import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint'; +import { NoPermissions } from '../../../../../components/no_permissons'; export const TRUSTED_APPS_LABELS = { artifactsSummaryApiError: (error: string) => @@ -87,7 +89,7 @@ export const BLOCKLISTS_LABELS = { ), cardTitle: ( ), @@ -97,6 +99,7 @@ export const EndpointPackageCustomExtension = memo { const http = useHttp(); const canSeeHostIsolationExceptions = useCanSeeHostIsolationExceptionsMenu(); + const { canAccessEndpointManagement } = useEndpointPrivileges(); const trustedAppsApiClientInstance = useMemo( () => TrustedAppsApiClient.getInstance(http), @@ -113,47 +116,62 @@ export const EndpointPackageCustomExtension = memo BlocklistsApiClient.getInstance(http), [http]); + const blocklistsApiClientInstance = useMemo( + () => BlocklistsApiClient.getInstance(http), + [http] + ); - return ( -
- - - - {canSeeHostIsolationExceptions && ( - <> - - - - )} - - -
+ const artifactCards = useMemo( + () => ( +
+ + + + {canSeeHostIsolationExceptions && ( + <> + + + + )} + + +
+ ), + [ + blocklistsApiClientInstance, + canSeeHostIsolationExceptions, + eventFiltersApiClientInstance, + hostIsolationExceptionsApiClientInstance, + trustedAppsApiClientInstance, + props, + ] ); + + return canAccessEndpointManagement ? artifactCards : ; } ); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx index 24aad4e9ed686..20ab62a099c8b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx @@ -41,6 +41,7 @@ import { SEARCHABLE_FIELDS as BLOCKLIST_SEARCHABLE_FIELDS } from '../../../block import { SEARCHABLE_FIELDS as HOST_ISOLATION_EXCEPTIONS_SEARCHABLE_FIELDS } from '../../../host_isolation_exceptions/constants'; import { SEARCHABLE_FIELDS as EVENT_FILTERS_SEARCHABLE_FIELDS } from '../../../event_filters/constants'; import { SEARCHABLE_FIELDS as TRUSTED_APPS_SEARCHABLE_FIELDS } from '../../../trusted_apps/constants'; +import { useEndpointPrivileges } from '../../../../../common/components/user_privileges/endpoint'; export const BLOCKLISTS_LABELS = { artifactsSummaryApiError: (error: string) => @@ -50,7 +51,7 @@ export const BLOCKLISTS_LABELS = { }), cardTitle: ( ), @@ -158,6 +159,7 @@ const WrappedPolicyDetailsForm = memo<{ const http = useHttp(); const blocklistsApiClientInstance = useMemo(() => BlocklistsApiClient.getInstance(http), [http]); + const { canAccessEndpointManagement } = useEndpointPrivileges(); const hostIsolationExceptionsApiClientInstance = useMemo( () => HostIsolationExceptionsApiClient.getInstance(http), @@ -225,8 +227,8 @@ const WrappedPolicyDetailsForm = memo<{ }); }, [onChange, updatedPolicy]); - return ( -
+ const artifactCards = useMemo( + () => ( <>
@@ -276,6 +278,22 @@ const WrappedPolicyDetailsForm = memo<{ />
+ + ), + [ + blocklistsApiClientInstance, + eventFiltersApiClientInstance, + hostIsolationExceptionsApiClientInstance, + policyId, + privileges.canIsolateHost, + trustedAppsApiClientInstance, + ] + ); + + return ( +
+ <> + {canAccessEndpointManagement && artifactCards}
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/blocklists_translations.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/blocklists_translations.ts index 0bc32d35ea341..bf695c92b51ff 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/blocklists_translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/blocklists_translations.ts @@ -10,27 +10,27 @@ import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types' export const POLICY_ARTIFACT_BLOCKLISTS_LABELS = Object.freeze({ deleteModalTitle: i18n.translate( - 'xpack.securitySolution.endpoint.policy.blocklists.list.removeDialog.title', + 'xpack.securitySolution.endpoint.policy.blocklist.list.removeDialog.title', { defaultMessage: 'Remove blocklist entry from policy', } ), deleteModalImpactInfo: i18n.translate( - 'xpack.securitySolution.endpoint.policy.blocklists.list.removeDialog.messageCallout', + 'xpack.securitySolution.endpoint.policy.blocklist.list.removeDialog.messageCallout', { defaultMessage: 'This blocklist entry will be removed only from this policy and can still be found and managed from the artifact page.', } ), deleteModalErrorMessage: i18n.translate( - 'xpack.securitySolution.endpoint.policy.blocklists.list.removeDialog.errorToastTitle', + 'xpack.securitySolution.endpoint.policy.blocklist.list.removeDialog.errorToastTitle', { defaultMessage: 'Error while attempting to remove blocklist entry', } ), flyoutWarningCalloutMessage: (maxNumber: number) => i18n.translate( - 'xpack.securitySolution.endpoint.policy.blocklists.layout.flyout.searchWarning.text', + 'xpack.securitySolution.endpoint.policy.blocklist.layout.flyout.searchWarning.text', { defaultMessage: 'Only the first {maxNumber} blocklist entries are displayed. Please use the search bar to refine the results.', @@ -38,30 +38,30 @@ export const POLICY_ARTIFACT_BLOCKLISTS_LABELS = Object.freeze({ } ), flyoutNoArtifactsToBeAssignedMessage: i18n.translate( - 'xpack.securitySolution.endpoint.policy.blocklists.layout.flyout.noAssignable', + 'xpack.securitySolution.endpoint.policy.blocklist.layout.flyout.noAssignable', { defaultMessage: 'There are no blocklist entries that can be assigned to this policy.', } ), flyoutTitle: i18n.translate( - 'xpack.securitySolution.endpoint.policy.blocklists.layout.flyout.title', + 'xpack.securitySolution.endpoint.policy.blocklist.layout.flyout.title', { defaultMessage: 'Assign blocklist entries', } ), flyoutSubtitle: (policyName: string): string => - i18n.translate('xpack.securitySolution.endpoint.policy.blocklists.layout.flyout.subtitle', { + i18n.translate('xpack.securitySolution.endpoint.policy.blocklist.layout.flyout.subtitle', { defaultMessage: 'Select blocklist entries to add to {policyName}', values: { policyName }, }), flyoutSearchPlaceholder: i18n.translate( - 'xpack.securitySolution.endpoint.policy.blocklists.layout.search.label', + 'xpack.securitySolution.endpoint.policy.blocklist.layout.search.label', { defaultMessage: 'Search blocklist entries', } ), flyoutErrorMessage: i18n.translate( - 'xpack.securitySolution.endpoint.policy.blocklists.layout.flyout.toastError.text', + 'xpack.securitySolution.endpoint.policy.blocklist.layout.flyout.toastError.text', { defaultMessage: `An error occurred updating blocklist entry`, } @@ -69,53 +69,53 @@ export const POLICY_ARTIFACT_BLOCKLISTS_LABELS = Object.freeze({ flyoutSuccessMessageText: (updatedExceptions: ExceptionListItemSchema[]): string => updatedExceptions.length > 1 ? i18n.translate( - 'xpack.securitySolution.endpoint.policy.blocklists.layout.flyout.toastSuccess.textMultiples', + 'xpack.securitySolution.endpoint.policy.blocklist.layout.flyout.toastSuccess.textMultiples', { defaultMessage: '{count} blocklist entries have been added to your list.', values: { count: updatedExceptions.length }, } ) : i18n.translate( - 'xpack.securitySolution.endpoint.policy.blocklists.layout.flyout.toastSuccess.textSingle', + 'xpack.securitySolution.endpoint.policy.blocklist.layout.flyout.toastSuccess.textSingle', { defaultMessage: '"{name}" blocklist has been added to your list.', values: { name: updatedExceptions[0].name }, } ), emptyUnassignedTitle: i18n.translate( - 'xpack.securitySolution.endpoint.policy.blocklists.empty.unassigned.title', + 'xpack.securitySolution.endpoint.policy.blocklist.empty.unassigned.title', { defaultMessage: 'No assigned blocklist entries' } ), emptyUnassignedMessage: (policyName: string): string => - i18n.translate('xpack.securitySolution.endpoint.policy.blocklists.empty.unassigned.content', { + i18n.translate('xpack.securitySolution.endpoint.policy.blocklist.empty.unassigned.content', { defaultMessage: 'There are currently no blocklist entries assigned to {policyName}. Assign blocklist entries now or add and manage them on the blocklist page.', values: { policyName }, }), emptyUnassignedPrimaryActionButtonTitle: i18n.translate( - 'xpack.securitySolution.endpoint.policy.blocklists.empty.unassigned.primaryAction', + 'xpack.securitySolution.endpoint.policy.blocklist.empty.unassigned.primaryAction', { defaultMessage: 'Assign blocklist entry', } ), emptyUnassignedSecondaryActionButtonTitle: i18n.translate( - 'xpack.securitySolution.endpoint.policy.blocklists.empty.unassigned.secondaryAction', + 'xpack.securitySolution.endpoint.policy.blocklist.empty.unassigned.secondaryAction', { defaultMessage: 'Manage blocklist entries', } ), emptyUnexistingTitle: i18n.translate( - 'xpack.securitySolution.endpoint.policy.blocklists.empty.unexisting.title', - { defaultMessage: 'No blocklist entries exist' } + 'xpack.securitySolution.endpoint.policy.blocklist.empty.unexisting.title', + { defaultMessage: 'No blocklists entries exist' } ), emptyUnexistingMessage: i18n.translate( - 'xpack.securitySolution.endpoint.policy.blocklists.empty.unexisting.content', + 'xpack.securitySolution.endpoint.policy.blocklist.empty.unexisting.content', { defaultMessage: 'There are currently no blocklist entries applied to your endpoints.', } ), emptyUnexistingPrimaryActionButtonTitle: i18n.translate( - 'xpack.securitySolution.endpoint.policy.blocklists.empty.unexisting.action', + 'xpack.securitySolution.endpoint.policy.blocklist.empty.unexisting.action', { defaultMessage: 'Add blocklist entry' } ), listTotalItemCountMessage: (totalItemsCount: number): string => @@ -125,28 +125,28 @@ export const POLICY_ARTIFACT_BLOCKLISTS_LABELS = Object.freeze({ values: { totalItemsCount }, }), listRemoveActionNotAllowedMessage: i18n.translate( - 'xpack.securitySolution.endpoint.policy.blocklists.list.removeActionNotAllowed', + 'xpack.securitySolution.endpoint.policy.blocklist.list.removeActionNotAllowed', { defaultMessage: 'Globally applied blocklist cannot be removed from policy.', } ), listSearchPlaceholderMessage: i18n.translate( - 'xpack.securitySolution.endpoint.policy.blocklists.list.search.placeholder', + 'xpack.securitySolution.endpoint.policy.blocklist.list.search.placeholder', { defaultMessage: `Search on the fields below: name, description, value`, } ), - layoutTitle: i18n.translate('xpack.securitySolution.endpoint.policy.blocklists.layout.title', { + layoutTitle: i18n.translate('xpack.securitySolution.endpoint.policy.blocklist.layout.title', { defaultMessage: 'Assigned blocklist entries', }), layoutAssignButtonTitle: i18n.translate( - 'xpack.securitySolution.endpoint.policy.blocklists.layout.assignToPolicy', + 'xpack.securitySolution.endpoint.policy.blocklist.layout.assignToPolicy', { defaultMessage: 'Assign blocklist entry to policy', } ), layoutViewAllLinkMessage: i18n.translate( - 'xpack.securitySolution.endpoint.policy.blocklists.layout.about.viewAllLinkLabel', + 'xpack.securitySolution.endpoint.policy.blocklist.layout.about.viewAllLinkLabel', { defaultMessage: 'view all blocklist entries', } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx index 6b40477c7bb6f..ad09653017ebb 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx @@ -155,7 +155,7 @@ export const PolicyTabs = React.memo(() => { ...POLICY_ARTIFACT_BLOCKLISTS_LABELS, layoutAboutMessage: (count: number, link: React.ReactElement): React.ReactNode => ( diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx index 570f444814d7f..68222ce8cd9e7 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx @@ -370,7 +370,7 @@ const SchemaInformation = ({
{ values: { totalTimelines }, defaultMessage: - 'Failed to import {totalTimelines} {totalTimelines, plural, =1 {rule} other {rules}}', + 'Failed to import {totalTimelines} {totalTimelines, plural, =1 {timeline} other {timelines}}', } ); diff --git a/x-pack/plugins/security_solution/public/users/components/all_users/index.test.tsx b/x-pack/plugins/security_solution/public/users/components/all_users/index.test.tsx index 117a62e0494df..95cc6ceef4e62 100644 --- a/x-pack/plugins/security_solution/public/users/components/all_users/index.test.tsx +++ b/x-pack/plugins/security_solution/public/users/components/all_users/index.test.tsx @@ -48,5 +48,29 @@ describe('Users Table Component', () => { expect(getAllByRole('columnheader').length).toBe(3); expect(getByText(userName)).toBeInTheDocument(); }); + + test('it renders empty string token when users name is empty', () => { + const { getByTestId } = render( + + {}} + /> + + ); + + expect(getByTestId('table-allUsers-loading-false')).toHaveTextContent('(Empty string)'); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/users/components/all_users/index.tsx b/x-pack/plugins/security_solution/public/users/components/all_users/index.tsx index 9a00a637f551d..fff8a188fd0a6 100644 --- a/x-pack/plugins/security_solution/public/users/components/all_users/index.tsx +++ b/x-pack/plugins/security_solution/public/users/components/all_users/index.tsx @@ -10,6 +10,7 @@ import { useDispatch } from 'react-redux'; import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; import { UserDetailsLink } from '../../../common/components/links'; +import { getOrEmptyTagFromValue } from '../../../common/components/empty_value'; import { Columns, @@ -66,12 +67,14 @@ const getUsersColumns = (): UsersTableColumns => [ sortable: true, mobileOptions: { show: true }, render: (name) => - getRowItemDraggables({ - rowItems: [name], - attrName: 'user.name', - idPrefix: `users-table-${name}-name`, - render: (item) => , - }), + name != null && name.length > 0 + ? getRowItemDraggables({ + rowItems: [name], + attrName: 'user.name', + idPrefix: `users-table-${name}-name`, + render: (item) => , + }) + : getOrEmptyTagFromValue(name), }, { field: 'lastSeen', @@ -88,11 +91,13 @@ const getUsersColumns = (): UsersTableColumns => [ truncateText: false, mobileOptions: { show: true }, render: (domain) => - getRowItemDraggables({ - rowItems: [domain], - attrName: 'user.domain', - idPrefix: `users-table-${domain}-domain`, - }), + domain != null && domain.length > 0 + ? getRowItemDraggables({ + rowItems: [domain], + attrName: 'user.domain', + idPrefix: `users-table-${domain}-domain`, + }) + : getOrEmptyTagFromValue(domain), }, ]; 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 94294192531f8..e9cc0e8eee777 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 @@ -38,7 +38,7 @@ import { deleteRules } from '../../rules/delete_rules'; import { duplicateRule } from '../../rules/duplicate_rule'; import { findRules } from '../../rules/find_rules'; import { readRules } from '../../rules/read_rules'; -import { patchRules } from '../../rules/patch_rules'; +import { editRule } from '../../rules/edit_rule'; import { applyBulkActionEditToRule } from '../../rules/bulk_action_edit'; import { getExportByObjectIds } from '../../rules/get_export_by_object_ids'; import { buildSiemResponse } from '../utils'; @@ -424,24 +424,18 @@ export const performBulkActionRoute = ( rule, }); - const editedRule = body[BulkAction.edit].reduce( - (acc, action) => applyBulkActionEditToRule(acc, action), - migratedRule - ); - - const { tags, params: { timelineTitle, timelineId } = {} } = editedRule; - const index = 'index' in editedRule.params ? editedRule.params.index : undefined; - - await patchRules({ + const updatedRule = await editRule({ rulesClient, rule: migratedRule, - tags, - index, - timelineTitle, - timelineId, + edit: (ruleToEdit) => { + return body[BulkAction.edit].reduce( + (acc, action) => applyBulkActionEditToRule(acc, action), + ruleToEdit + ); + }, }); - return editedRule; + return updatedRule; }, abortSignal: abortController.signal, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/edit_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/edit_rule.ts new file mode 100644 index 0000000000000..5975f106225a1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/edit_rule.ts @@ -0,0 +1,139 @@ +/* + * 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 { cloneDeep, isEqual, pick } from 'lodash'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; + +import type { RulesClient } from '../../../../../alerting/server'; +import { RuleAlertType } from '../rules/types'; +import { InternalRuleUpdate, internalRuleUpdate } from '../schemas/rule_schemas'; +import { addTags } from './add_tags'; + +class EditRuleError extends Error { + public readonly statusCode: number; + constructor(message: string, statusCode: number) { + super(message); + this.statusCode = statusCode; + } +} + +interface EditRuleParams { + /** An instance of RulesClient from the Alerting Framework. */ + rulesClient: RulesClient; + + /** Original, existing rule to be edited. Needs to be fetched from Elasticsearch via RulesClient. */ + rule: RuleAlertType; + + /** A function that implements in-memory modifications: returns a new rule object with the changes. */ + edit: (rule: RuleAlertType) => RuleAlertType; +} + +/** At this point we support editing of only these fields. */ +const FIELDS_THAT_CAN_BE_EDITED = ['params', 'tags'] as const; + +/** + * Applies in-memory modifications to a given rule and updates it in Elasticsearch via RulesClient. + * + * NOTE: At this point we only support editing of the following fields: + * - rule.params + * - rule.tags + * All other changes made by the `edit` function will be ignored. + * + * @returns The edited rule. + */ +export const editRule = async (params: EditRuleParams): Promise => { + const { rulesClient, rule, edit } = params; + const isPrebuiltRule = rule.params.immutable; + const isCustomRule = !rule.params.immutable; + + if (isPrebuiltRule) { + throw new EditRuleError('Elastic rule can`t be edited', 400); + } + + const editedRule = applyChanges(rule, edit); + + // If the rule wasn't changed by the `edit` function, we don't need to proceed with the update. + if (!isRuleChanged(rule, editedRule)) { + return rule; + } + + // We need to increment the rule's version if it is a custom rule. If the rule is an Elastic + // prebuilt rule, we don't want to touch its version - it's managed by the rule authors. + // This check is left here explicitly because we're planning to allow editing for prebuilt rules, + // and the check for isPrebuiltRule above might be removed. + if (isCustomRule) { + editedRule.params.version = editedRule.params.version + 1; + } + + const updateData = createUpdateData(rule, editedRule); + await rulesClient.update({ + id: rule.id, + data: updateData, + }); + + // It would be great to return the updated rule returned from the RulesClient.update() call. + // Note that there's a type mismatch between RuleAlertType and the update method result. + return editedRule; +}; + +const applyChanges = ( + originalRule: RuleAlertType, + edit: (rule: RuleAlertType) => RuleAlertType +): RuleAlertType => { + // For safety, deeply clone the rule object before applying edits to it. + const clonedRule = cloneDeep(originalRule); + const editedRule = edit(clonedRule); + const sanitizedRule = validateAndSanitizeChanges(originalRule, editedRule); + return sanitizedRule; +}; + +const validateAndSanitizeChanges = ( + original: RuleAlertType, + changed: RuleAlertType +): RuleAlertType => { + // These checks should never throw unless there's a bug in the passed `edit` function. + if (changed.params.immutable !== original.params.immutable) { + throw new EditRuleError(`Internal rule editing error: can't change "params.immutable"`, 500); + } + if (changed.params.version !== original.params.version) { + throw new EditRuleError(`Internal rule editing error: can't change "params.version"`, 500); + } + + return { + ...changed, + tags: addTags(changed.tags, changed.params.ruleId, changed.params.immutable), + }; +}; + +const isRuleChanged = (originalRule: RuleAlertType, editedRule: RuleAlertType): boolean => { + const originalData = pick(originalRule, FIELDS_THAT_CAN_BE_EDITED); + const editedData = pick(editedRule, FIELDS_THAT_CAN_BE_EDITED); + return !isEqual(originalData, editedData); +}; + +const createUpdateData = ( + originalRule: RuleAlertType, + editedRule: RuleAlertType +): InternalRuleUpdate => { + const data: InternalRuleUpdate = { + // At this point we intentionally support updating of only these fields: + ...pick(editedRule, FIELDS_THAT_CAN_BE_EDITED), + // We omit other fields and get them from the original (unedited) rule: + name: originalRule.name, + schedule: originalRule.schedule, + actions: originalRule.actions, + throttle: originalRule.throttle, + notifyWhen: originalRule.notifyWhen, + }; + + const [validatedData, validationError] = validate(data, internalRuleUpdate); + if (validationError != null || validatedData === null) { + throw new EditRuleError(`Editing rule would create invalid rule: ${validationError}`, 500); + } + + return validatedData; +}; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/endpoint_alerts.ts b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/endpoint_alerts.ts index 15f7b0a2a54c8..5a62513b1ab38 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/endpoint_alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/endpoint_alerts.ts @@ -62,8 +62,8 @@ const allowlistBaseEventFields: AllowlistFields = { directory: true, hash: true, Ext: { - compressed_bytes: true, - compressed_bytes_present: true, + bytes_compressed: true, + bytes_compressed_present: true, code_signature: true, header_bytes: true, header_data: true, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts index dff3676c20c8a..f269388e5ac3e 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -59,8 +59,8 @@ describe('TelemetryEventsSender', () => { test: 'me', another: 'nope', Ext: { - compressed_bytes: 'data up to 4mb', - compressed_bytes_present: 'data up to 4mb', + bytes_compressed: 'data up to 4mb', + bytes_compressed_present: 'data up to 4mb', code_signature: { key1: 'X', key2: 'Y', @@ -133,8 +133,8 @@ describe('TelemetryEventsSender', () => { created: 0, path: 'X', Ext: { - compressed_bytes: 'data up to 4mb', - compressed_bytes_present: 'data up to 4mb', + bytes_compressed: 'data up to 4mb', + bytes_compressed_present: 'data up to 4mb', code_signature: { key1: 'X', key2: 'Y', diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index a1abc744c3336..6cd022b68cd8a 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -647,8 +647,6 @@ "xpack.lens.shared.legendInsideAlignmentLabel": "Alignement", "xpack.lens.shared.legendInsideColumnsLabel": "Nombre de colonnes", "xpack.lens.shared.legendInsideLocationAlignmentLabel": "Alignement", - "xpack.lens.shared.legendInsideTooltip": "Requiert que la légende soit placée dans la visualisation", - "xpack.lens.shared.legendIsTruncated": "Requiert que le texte soit tronqué", "xpack.lens.shared.legendLabel": "Légende", "xpack.lens.shared.legendLocationBottomLeft": "En bas à gauche", "xpack.lens.shared.legendLocationBottomRight": "En bas à droite", @@ -660,7 +658,6 @@ "xpack.lens.shared.legendPositionRight": "Droite", "xpack.lens.shared.legendPositionTop": "Haut", "xpack.lens.shared.legendVisibilityLabel": "Affichage", - "xpack.lens.shared.legendVisibleTooltip": "Requiert que la légende soit affichée", "xpack.lens.shared.maxLinesLabel": "Nombre maximal de lignes", "xpack.lens.shared.nestedLegendLabel": "Imbriqué", "xpack.lens.shared.truncateLegend": "Tronquer le texte", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 72aa6e24034d6..59ba105e0296d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -776,8 +776,6 @@ "xpack.lens.shared.legendInsideAlignmentLabel": "アラインメント", "xpack.lens.shared.legendInsideColumnsLabel": "列の数", "xpack.lens.shared.legendInsideLocationAlignmentLabel": "アラインメント", - "xpack.lens.shared.legendInsideTooltip": "凡例をビジュアライゼーション内に配置する必要があります", - "xpack.lens.shared.legendIsTruncated": "テキストを切り捨てる必要があります", "xpack.lens.shared.legendLabel": "凡例", "xpack.lens.shared.legendLocationBottomLeft": "左下", "xpack.lens.shared.legendLocationBottomRight": "右下", @@ -789,7 +787,6 @@ "xpack.lens.shared.legendPositionRight": "右", "xpack.lens.shared.legendPositionTop": "トップ", "xpack.lens.shared.legendVisibilityLabel": "表示", - "xpack.lens.shared.legendVisibleTooltip": "凡例を表示する必要があります", "xpack.lens.shared.maxLinesLabel": "最大行", "xpack.lens.shared.nestedLegendLabel": "ネスト済み", "xpack.lens.shared.overwriteAxisTitle": "軸タイトルを上書き", @@ -861,7 +858,6 @@ "xpack.lens.xyChart.axisOrientation.horizontal": "横", "xpack.lens.xyChart.axisOrientation.label": "向き", "xpack.lens.xyChart.axisOrientation.vertical": "縦", - "xpack.lens.xyChart.axisOrientationMultilayer.disabled": "このオプションは時間に基づかない軸でのみ構成できます", "xpack.lens.xyChart.axisSide.auto": "自動", "xpack.lens.xyChart.axisSide.bottom": "一番下", "xpack.lens.xyChart.axisSide.label": "軸側", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 41e2dab0f29ec..c01827e7977b5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -780,8 +780,6 @@ "xpack.lens.shared.legendInsideAlignmentLabel": "对齐方式", "xpack.lens.shared.legendInsideColumnsLabel": "列数目", "xpack.lens.shared.legendInsideLocationAlignmentLabel": "对齐方式", - "xpack.lens.shared.legendInsideTooltip": "需要图例位于可视化内", - "xpack.lens.shared.legendIsTruncated": "需要截断文本", "xpack.lens.shared.legendLabel": "图例", "xpack.lens.shared.legendLocationBottomLeft": "左下方", "xpack.lens.shared.legendLocationBottomRight": "右下方", @@ -793,7 +791,6 @@ "xpack.lens.shared.legendPositionRight": "右", "xpack.lens.shared.legendPositionTop": "顶部", "xpack.lens.shared.legendVisibilityLabel": "显示", - "xpack.lens.shared.legendVisibleTooltip": "需要图表显示", "xpack.lens.shared.maxLinesLabel": "最大行数", "xpack.lens.shared.nestedLegendLabel": "嵌套", "xpack.lens.shared.overwriteAxisTitle": "覆盖轴标题", @@ -865,7 +862,6 @@ "xpack.lens.xyChart.axisOrientation.horizontal": "水平", "xpack.lens.xyChart.axisOrientation.label": "方向", "xpack.lens.xyChart.axisOrientation.vertical": "垂直", - "xpack.lens.xyChart.axisOrientationMultilayer.disabled": "只能使用非基于时间的轴配置这些选项", "xpack.lens.xyChart.axisSide.auto": "自动", "xpack.lens.xyChart.axisSide.bottom": "底部", "xpack.lens.xyChart.axisSide.label": "轴侧", diff --git a/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts b/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts index ab9a6f90de0a0..8ee664bda1ebf 100644 --- a/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts +++ b/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts @@ -12,19 +12,16 @@ import { tEnum } from '../../utils/t_enum'; export enum BandwidthLimitKey { DOWNLOAD = 'download', UPLOAD = 'upload', - LATENCY = 'latency', } export const DEFAULT_BANDWIDTH_LIMIT = { [BandwidthLimitKey.DOWNLOAD]: 100, [BandwidthLimitKey.UPLOAD]: 30, - [BandwidthLimitKey.LATENCY]: 1000, }; export const DEFAULT_THROTTLING = { [BandwidthLimitKey.DOWNLOAD]: DEFAULT_BANDWIDTH_LIMIT[BandwidthLimitKey.DOWNLOAD], [BandwidthLimitKey.UPLOAD]: DEFAULT_BANDWIDTH_LIMIT[BandwidthLimitKey.UPLOAD], - [BandwidthLimitKey.LATENCY]: DEFAULT_BANDWIDTH_LIMIT[BandwidthLimitKey.LATENCY], }; export const BandwidthLimitKeyCodec = tEnum( @@ -107,7 +104,6 @@ export const isServiceLocationInvalid = (location: MonitorServiceLocation) => export const ThrottlingOptionsCodec = t.interface({ [BandwidthLimitKey.DOWNLOAD]: t.number, [BandwidthLimitKey.UPLOAD]: t.number, - [BandwidthLimitKey.LATENCY]: t.number, }); export const ServiceLocationsApiResponseCodec = t.interface({ diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/throttling_fields.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/throttling_fields.test.tsx index f817f1ff9a805..dc74717ec2c98 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/browser/throttling_fields.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/throttling_fields.test.tsx @@ -214,7 +214,6 @@ describe('', () => { const throttling = { [BandwidthLimitKey.DOWNLOAD]: 100, [BandwidthLimitKey.UPLOAD]: 50, - [BandwidthLimitKey.LATENCY]: 25, }; const defaultLocations = [defaultLocation]; @@ -339,54 +338,6 @@ describe('', () => { ) ).not.toBeInTheDocument(); }); - - it("shows latency warnings when exceeding the node's latency limits", () => { - const { getByLabelText, queryByText } = render( - - ); - - const latencyLimit = throttling[BandwidthLimitKey.LATENCY]; - - const latency = getByLabelText('Latency') as HTMLInputElement; - userEvent.clear(latency); - userEvent.type(latency, String(latencyLimit + 1)); - - expect( - queryByText( - `You have exceeded the latency limit for Synthetic Nodes. The latency value can't be larger than ${latencyLimit}ms.` - ) - ).toBeInTheDocument(); - - expect( - queryByText("You've exceeded the Synthetics Node bandwidth limits") - ).toBeInTheDocument(); - - expect( - queryByText( - 'When using throttling values larger than a Synthetics Node bandwidth limit, your monitor will still have its bandwidth capped.' - ) - ).toBeInTheDocument(); - - userEvent.clear(latency); - userEvent.type(latency, String(latencyLimit - 1)); - expect( - queryByText( - `You have exceeded the latency limit for Synthetic Nodes. The latency value can't be larger than ${latencyLimit}ms.` - ) - ).not.toBeInTheDocument(); - - expect( - queryByText("You've exceeded the Synthetics Node bandwidth limits") - ).not.toBeInTheDocument(); - - expect( - queryByText( - 'When using throttling values larger than a Synthetics Node bandwidth limit, your monitor will still have its bandwidth capped.' - ) - ).not.toBeInTheDocument(); - }); }); it('only displays download, upload, and latency fields with throttling is on', () => { diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/throttling_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/throttling_fields.tsx index 683bc1e79e386..f7fc15887e721 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/browser/throttling_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/throttling_fields.tsx @@ -76,17 +76,15 @@ export const ThrottlingExceededCallout = () => { export const ThrottlingExceededMessage = ({ throttlingField, limit, - unit, }: { throttlingField: string; limit: number; - unit: string; }) => { return ( ); }; @@ -97,7 +95,6 @@ export const ThrottlingFields = memo(({ validate, minColumnWidth, onField const maxDownload = throttling[BandwidthLimitKey.DOWNLOAD]; const maxUpload = throttling[BandwidthLimitKey.UPLOAD]; - const maxLatency = throttling[BandwidthLimitKey.LATENCY]; const handleInputChange = useCallback( ({ value, configKey }: { value: unknown; configKey: ThrottlingConfigs }) => { @@ -110,7 +107,6 @@ export const ThrottlingFields = memo(({ validate, minColumnWidth, onField runsOnService && parseFloat(fields[ConfigKey.DOWNLOAD_SPEED]) > maxDownload; const exceedsUploadLimits = runsOnService && parseFloat(fields[ConfigKey.UPLOAD_SPEED]) > maxUpload; - const exceedsLatencyLimits = runsOnService && parseFloat(fields[ConfigKey.LATENCY]) > maxLatency; const isThrottlingEnabled = fields[ConfigKey.IS_THROTTLING_ENABLED]; const throttlingInputs = isThrottlingEnabled ? ( @@ -127,7 +123,7 @@ export const ThrottlingFields = memo(({ validate, minColumnWidth, onField isInvalid={!!validate[ConfigKey.DOWNLOAD_SPEED]?.(fields) || exceedsDownloadLimits} error={ exceedsDownloadLimits ? ( - + ) : ( (({ validate, minColumnWidth, onField isInvalid={!!validate[ConfigKey.UPLOAD_SPEED]?.(fields) || exceedsUploadLimits} error={ exceedsUploadLimits ? ( - + ) : ( (({ validate, minColumnWidth, onField /> } labelAppend={} - isInvalid={!!validate[ConfigKey.LATENCY]?.(fields) || exceedsLatencyLimits} + isInvalid={!!validate[ConfigKey.LATENCY]?.(fields)} error={ - exceedsLatencyLimits ? ( - - ) : ( - - ) + } > (({ validate, minColumnWidth, onField } onBlur={() => onFieldBlur?.(ConfigKey.IS_THROTTLING_ENABLED)} /> - {isThrottlingEnabled && - (exceedsDownloadLimits || exceedsUploadLimits || exceedsLatencyLimits) ? ( + {isThrottlingEnabled && (exceedsDownloadLimits || exceedsUploadLimits) ? ( <> diff --git a/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.ts b/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.ts index e71a0511cbaf1..8f3dd82b3dd93 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.ts +++ b/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { CommonFields, ConfigKey } from '../types'; +import { CommonFields, ConfigKey, DataStream } from '../types'; import { NewPackagePolicyInput } from '../../../../../fleet/common'; import { defaultValues as commonDefaultValues } from './default_values'; import { DEFAULT_NAMESPACE_STRING } from '../../../../common/constants'; +import { defaultConfig } from '../contexts'; // TO DO: create a standard input format that all fields resolve to export type Normalizer = (fields: NewPackagePolicyInput['vars']) => unknown; @@ -64,6 +65,7 @@ export const commonNormalizers: CommonNormalizerMap = { [ConfigKey.LOCATIONS]: getCommonNormalizer(ConfigKey.LOCATIONS), [ConfigKey.SCHEDULE]: (fields) => { const value = fields?.[ConfigKey.SCHEDULE]?.value; + const type = fields?.[ConfigKey.MONITOR_TYPE]?.value as DataStream; if (value) { const fullString = JSON.parse(fields?.[ConfigKey.SCHEDULE]?.value); const fullSchedule = fullString.replace('@every ', ''); @@ -74,7 +76,7 @@ export const commonNormalizers: CommonNormalizerMap = { number, }; } else { - return commonDefaultValues[ConfigKey.SCHEDULE]; + return defaultConfig[type][ConfigKey.SCHEDULE]; } }, [ConfigKey.APM_SERVICE_NAME]: getCommonNormalizer(ConfigKey.APM_SERVICE_NAME), diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context.tsx index 015023318556a..f7b67488e4bcc 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context.tsx @@ -6,7 +6,7 @@ */ import React, { createContext, useContext, useMemo, useState } from 'react'; -import { BrowserSimpleFields, ConfigKey, DataStream } from '../types'; +import { BrowserSimpleFields, ConfigKey, DataStream, ScheduleUnit } from '../types'; import { defaultValues as commonDefaultValues } from '../common/default_values'; interface BrowserSimpleFieldsContext { @@ -22,6 +22,10 @@ interface BrowserSimpleFieldsContextProvider { export const initialValues: BrowserSimpleFields = { ...commonDefaultValues, + [ConfigKey.SCHEDULE]: { + unit: ScheduleUnit.MINUTES, + number: '10', + }, [ConfigKey.METADATA]: { script_source: { is_generated_script: false, diff --git a/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx index 5f6e67e363171..50bbeb8a40aa8 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx @@ -121,7 +121,7 @@ export const ActionBar = ({ {DISCARD_LABEL} diff --git a/x-pack/plugins/uptime/public/routes.tsx b/x-pack/plugins/uptime/public/routes.tsx index e66954a5fd4af..ed7f2b5001b34 100644 --- a/x-pack/plugins/uptime/public/routes.tsx +++ b/x-pack/plugins/uptime/public/routes.tsx @@ -259,7 +259,8 @@ const getRoutes = (): RouteProps[] => { diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.test.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.test.ts index 82fe06f36d533..9151a1dcac6ad 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.test.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.test.ts @@ -18,7 +18,6 @@ describe('getServiceLocations', function () { throttling: { [BandwidthLimitKey.DOWNLOAD]: 100, [BandwidthLimitKey.UPLOAD]: 50, - [BandwidthLimitKey.LATENCY]: 20, }, locations: { us_central: { @@ -50,7 +49,6 @@ describe('getServiceLocations', function () { throttling: { [BandwidthLimitKey.DOWNLOAD]: 100, [BandwidthLimitKey.UPLOAD]: 50, - [BandwidthLimitKey.LATENCY]: 20, }, locations: [ { diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.ts index 50ecfe38d20dd..45dd0ded1438d 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/get_service_locations.ts @@ -54,8 +54,7 @@ export async function getServiceLocations(server: UptimeServerSetup) { const throttling = pick( data.throttling, BandwidthLimitKey.DOWNLOAD, - BandwidthLimitKey.UPLOAD, - BandwidthLimitKey.LATENCY + BandwidthLimitKey.UPLOAD ) as ThrottlingOptions; return { throttling, locations }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts index 82899ff0de8f7..cf7d6ca8e05f9 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts @@ -33,10 +33,6 @@ import { import { ROLES } from '../../../../plugins/security_solution/common/test'; import { createUserAndRole, deleteUserAndRole } from '../../../common/services/security_solution'; -function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); @@ -311,7 +307,6 @@ export default ({ getService }: FtrProviderContext) => { bodyId, RuleExecutionStatus['partial failure'] ); - await sleep(5000); const { body: rule } = await supertest .get(DETECTION_ENGINE_RULES_URL) @@ -344,7 +339,6 @@ export default ({ getService }: FtrProviderContext) => { bodyId, RuleExecutionStatus['partial failure'] ); - await sleep(5000); await waitForSignalsToBePresent(supertest, log, 2, [bodyId]); const { body: rule } = await supertest diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/timestamps.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/timestamps.ts index e0d539a3fe33b..0df0ac3376a7a 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/timestamps.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/timestamps.ts @@ -27,10 +27,6 @@ import { getEqlRuleForSignalTesting, } from '../../utils'; -function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); @@ -70,7 +66,6 @@ export default ({ getService }: FtrProviderContext) => { const rule = getRuleForSignalTesting(['timestamp_in_seconds']); const { id } = await createRule(supertest, log, rule); await waitForRuleSuccessOrStatus(supertest, log, id); - await sleep(5000); await waitForSignalsToBePresent(supertest, log, 1, [id]); const signalsOpen = await getSignalsByIds(supertest, log, [id]); const hits = signalsOpen.hits.hits @@ -86,7 +81,6 @@ export default ({ getService }: FtrProviderContext) => { }; const { id } = await createRule(supertest, log, rule); await waitForRuleSuccessOrStatus(supertest, log, id); - await sleep(5000); await waitForSignalsToBePresent(supertest, log, 1, [id]); const signalsOpen = await getSignalsByIds(supertest, log, [id]); const hits = signalsOpen.hits.hits @@ -101,7 +95,6 @@ export default ({ getService }: FtrProviderContext) => { const rule = getEqlRuleForSignalTesting(['timestamp_in_seconds']); const { id } = await createRule(supertest, log, rule); await waitForRuleSuccessOrStatus(supertest, log, id); - await sleep(5000); await waitForSignalsToBePresent(supertest, log, 1, [id]); const signalsOpen = await getSignalsByIds(supertest, log, [id]); const hits = signalsOpen.hits.hits @@ -117,7 +110,6 @@ export default ({ getService }: FtrProviderContext) => { }; const { id } = await createRule(supertest, log, rule); await waitForRuleSuccessOrStatus(supertest, log, id); - await sleep(5000); await waitForSignalsToBePresent(supertest, log, 1, [id]); const signalsOpen = await getSignalsByIds(supertest, log, [id]); const hits = signalsOpen.hits.hits @@ -184,7 +176,6 @@ export default ({ getService }: FtrProviderContext) => { id, RuleExecutionStatus['partial failure'] ); - await sleep(5000); await waitForSignalsToBePresent(supertest, log, 3, [id]); const signalsResponse = await getSignalsByIds(supertest, log, [id], 3); const signals = signalsResponse.hits.hits.map((hit) => hit._source); @@ -204,7 +195,6 @@ export default ({ getService }: FtrProviderContext) => { id, RuleExecutionStatus['partial failure'] ); - await sleep(5000); await waitForSignalsToBePresent(supertest, log, 2, [id]); const signalsResponse = await getSignalsByIds(supertest, log, [id]); const signals = signalsResponse.hits.hits.map((hit) => hit._source); @@ -226,7 +216,6 @@ export default ({ getService }: FtrProviderContext) => { id, RuleExecutionStatus['partial failure'] ); - await sleep(5000); await waitForSignalsToBePresent(supertest, log, 2, [id]); const signalsResponse = await getSignalsByIds(supertest, log, [id, id]); const signals = signalsResponse.hits.hits.map((hit) => hit._source); @@ -249,7 +238,6 @@ export default ({ getService }: FtrProviderContext) => { const { id } = await createRule(supertest, log, rule); await waitForRuleSuccessOrStatus(supertest, log, id); - await sleep(5000); await waitForSignalsToBePresent(supertest, log, 1, [id]); const signalsResponse = await getSignalsByIds(supertest, log, [id, id]); const hits = signalsResponse.hits.hits @@ -271,7 +259,6 @@ export default ({ getService }: FtrProviderContext) => { id, RuleExecutionStatus['partial failure'] ); - await sleep(5000); await waitForSignalsToBePresent(supertest, log, 2, [id]); const signalsResponse = await getSignalsByIds(supertest, log, [id]); const signals = signalsResponse.hits.hits.map((hit) => hit._source); @@ -293,7 +280,6 @@ export default ({ getService }: FtrProviderContext) => { id, RuleExecutionStatus['partial failure'] ); - await sleep(5000); await waitForSignalsToBePresent(supertest, log, 2, [id]); const signalsResponse = await getSignalsByIds(supertest, log, [id, id]); const signals = signalsResponse.hits.hits.map((hit) => hit._source); @@ -348,7 +334,6 @@ export default ({ getService }: FtrProviderContext) => { id, RuleExecutionStatus['partial failure'] ); - await sleep(5000); await waitForSignalsToBePresent(supertest, log, 200, [id]); const signalsResponse = await getSignalsByIds(supertest, log, [id], 200); const signals = signalsResponse.hits.hits.map((hit) => hit._source); diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 2087e0d6ab523..c3cf935492730 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -523,7 +523,9 @@ export const deleteAllAlerts = async ( .get(`${DETECTION_ENGINE_RULES_URL}/_find`) .set('kbn-xsrf', 'true') .send(); - return finalCheck.data.length === 0; + return { + passed: finalCheck.data.length === 0, + }; }, 'deleteAllAlerts', log, @@ -580,7 +582,7 @@ export const deleteAllTimelines = async (es: Client): Promise => { /** * Remove all rules execution info saved objects from the .kibana index - * This will retry 20 times before giving up and hopefully still not interfere with other tests + * This will retry 50 times before giving up and hopefully still not interfere with other tests * @param es The ElasticSearch handle * @param log The tooling logger */ @@ -605,7 +607,7 @@ export const deleteAllRuleExecutionInfo = async (es: Client, log: ToolingLog): P /** * Creates the signals index for use inside of beforeEach blocks of tests - * This will retry 20 times before giving up and hopefully still not interfere with other tests + * This will retry 50 times before giving up and hopefully still not interfere with other tests * @param supertest The supertest client library */ export const createSignalsIndex = async ( @@ -615,7 +617,9 @@ export const createSignalsIndex = async ( await countDownTest( async () => { await supertest.post(DETECTION_ENGINE_INDEX_URL).set('kbn-xsrf', 'true').send(); - return true; + return { + passed: true, + }; }, 'createSignalsIndex', log @@ -657,7 +661,9 @@ export const deleteSignalsIndex = async ( await countDownTest( async () => { await supertest.delete(DETECTION_ENGINE_INDEX_URL).set('kbn-xsrf', 'true').send(); - return true; + return { + passed: true, + }; }, 'deleteSignalsIndex', log @@ -957,17 +963,19 @@ export const countDownES = async ( esFunction: () => Promise, unknown>>, esFunctionName: string, log: ToolingLog, - retryCount: number = 20, + retryCount: number = 50, timeoutWait = 250 ): Promise => { await countDownTest( async () => { const result = await esFunction(); if (result.body.version_conflicts !== 0) { - log.error(`Version conflicts for ${result.body.version_conflicts}`); - return false; + return { + passed: false, + errorMessage: 'Version conflicts for ${result.body.version_conflicts}', + }; } else { - return true; + return { passed: true }; } }, esFunctionName, @@ -998,22 +1006,37 @@ export const refreshIndex = async (es: Client, index?: string) => { * @param retryCount The number of times to retry before giving up (has default) * @param timeoutWait Time to wait before trying again (has default) */ -export const countDownTest = async ( - functionToTest: () => Promise, +export const countDownTest = async ( + functionToTest: () => Promise<{ + passed: boolean; + returnValue?: T | undefined; + errorMessage?: string; + }>, name: string, log: ToolingLog, - retryCount: number = 20, + retryCount: number = 50, timeoutWait = 250, ignoreThrow: boolean = false -) => { +): Promise => { if (retryCount > 0) { try { - const passed = await functionToTest(); - if (!passed) { - log.error(`Failure trying to ${name}, retries left are: ${retryCount - 1}`); + const testReturn = await functionToTest(); + if (!testReturn.passed) { + const error = testReturn.errorMessage != null ? ` error: ${testReturn.errorMessage},` : ''; + log.error(`Failure trying to ${name},${error} retries left are: ${retryCount - 1}`); // retry, counting down, and delay a bit before await new Promise((resolve) => setTimeout(resolve, timeoutWait)); - await countDownTest(functionToTest, name, log, retryCount - 1, timeoutWait, ignoreThrow); + const returnValue = await countDownTest( + functionToTest, + name, + log, + retryCount - 1, + timeoutWait, + ignoreThrow + ); + return returnValue; + } else { + return testReturn.returnValue; } } catch (err) { if (ignoreThrow) { @@ -1026,11 +1049,20 @@ export const countDownTest = async ( ); // retry, counting down, and delay a bit before await new Promise((resolve) => setTimeout(resolve, timeoutWait)); - await countDownTest(functionToTest, name, log, retryCount - 1, timeoutWait, ignoreThrow); + const returnValue = await countDownTest( + functionToTest, + name, + log, + retryCount - 1, + timeoutWait, + ignoreThrow + ); + return returnValue; } } } else { log.error(`Could not ${name}, no retries are left`); + return undefined; } }; @@ -1554,7 +1586,7 @@ export const indexEventLogExecutionEvents = async ( /** * Remove all .kibana-event-log-* documents with an execution.uuid - * This will retry 20 times before giving up and hopefully still not interfere with other tests + * This will retry 50 times before giving up and hopefully still not interfere with other tests * @param es The ElasticSearch handle * @param log The tooling logger */ @@ -1589,21 +1621,32 @@ export const getSignalsByRuleIds = async ( log: ToolingLog, ruleIds: string[] ): Promise> => { - const response = await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalsRuleId(ruleIds)); - - if (response.status !== 200) { - log.error( - `Did not get an expected 200 "ok" when getting a signal by rule_id (getSignalsByRuleIds). CI issues could happen. Suspect this line if you are seeing CI issues. body: ${JSON.stringify( - response.body - )}, status: ${JSON.stringify(response.status)}` - ); + const signalsOpen = await countDownTest>( + async () => { + const response = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalsRuleId(ruleIds)); + if (response.status !== 200) { + return { + passed: false, + errorMessage: `Status is not 200 as expected, it is: ${response.status}`, + }; + } else { + return { + passed: true, + returnValue: response.body, + }; + } + }, + 'getSignalsByRuleIds', + log + ); + if (signalsOpen == null) { + throw new Error('Signals not defined after countdown, cannot continue'); + } else { + return signalsOpen; } - - const { body: signalsOpen }: { body: estypes.SearchResponse } = response; - return signalsOpen; }; /** @@ -1618,20 +1661,32 @@ export const getSignalsByIds = async ( ids: string[], size?: number ): Promise> => { - const response = await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalsId(ids, size)); - - if (response.status !== 200) { - log.error( - `Did not get an expected 200 "ok" when getting a signal by id. CI issues could happen (getSignalsByIds). Suspect this line if you are seeing CI issues. body: ${JSON.stringify( - response.body - )}, status: ${JSON.stringify(response.status)}` - ); + const signalsOpen = await countDownTest>( + async () => { + const response = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalsId(ids, size)); + if (response.status !== 200) { + return { + passed: false, + errorMessage: `Status is not 200 as expected, it is: ${response.status}`, + }; + } else { + return { + passed: true, + returnValue: response.body, + }; + } + }, + 'getSignalsByIds', + log + ); + if (signalsOpen == null) { + throw new Error('Signals not defined after countdown, cannot continue'); + } else { + return signalsOpen; } - const { body: signalsOpen }: { body: estypes.SearchResponse } = response; - return signalsOpen; }; /** @@ -1644,20 +1699,32 @@ export const getSignalsById = async ( log: ToolingLog, id: string ): Promise> => { - const response = await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalsId([id])); - - if (response.status !== 200) { - log.error( - `Did not get an expected 200 "ok" when getting signals by id (getSignalsById). CI issues could happen. Suspect this line if you are seeing CI issues. body: ${JSON.stringify( - response.body - )}, status: ${JSON.stringify(response.status)}` - ); + const signalsOpen = await countDownTest>( + async () => { + const response = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalsId([id])); + if (response.status !== 200) { + return { + passed: false, + returnValue: undefined, + }; + } else { + return { + passed: true, + returnValue: response.body, + }; + } + }, + 'getSignalsById', + log + ); + if (signalsOpen == null) { + throw new Error('Signals not defined after countdown, cannot continue'); + } else { + return signalsOpen; } - const { body: signalsOpen }: { body: estypes.SearchResponse } = response; - return signalsOpen; }; export const installPrePackagedRules = async ( @@ -1671,14 +1738,15 @@ export const installPrePackagedRules = async ( .set('kbn-xsrf', 'true') .send(); if (status !== 200) { - log.debug( - `Did not get an expected 200 "ok" when installing pre-packaged rules (installPrePackagedRules) yet. Retrying until we get a 200 "ok". body: ${JSON.stringify( + return { + passed: false, + errorMessage: `Did not get an expected 200 "ok" when installing pre-packaged rules (installPrePackagedRules) yet. Retrying until we get a 200 "ok". body: ${JSON.stringify( body - )}, status: ${JSON.stringify(status)}` - ); + )}, status: ${JSON.stringify(status)}`, + }; + } else { + return { passed: true }; } - - return status === 200; }, 'installPrePackagedRules', log diff --git a/x-pack/test/functional_synthetics/apps/uptime/synthetics_integration.ts b/x-pack/test/functional_synthetics/apps/uptime/synthetics_integration.ts index 93635ea0b0a73..972dfe5a2acab 100644 --- a/x-pack/test/functional_synthetics/apps/uptime/synthetics_integration.ts +++ b/x-pack/test/functional_synthetics/apps/uptime/synthetics_integration.ts @@ -555,7 +555,7 @@ export default function (providerContext: FtrProviderContext) { monitorType: 'browser', config: { screenshots: 'on', - schedule: '@every 3m', + schedule: '@every 10m', timeout: null, tags: [config.tags], throttling: '5d/3u/20l', @@ -605,7 +605,7 @@ export default function (providerContext: FtrProviderContext) { monitorType: 'browser', config: { screenshots: 'on', - schedule: '@every 3m', + schedule: '@every 10m', timeout: null, tags: [config.tags], throttling: '5d/3u/20l', @@ -664,7 +664,7 @@ export default function (providerContext: FtrProviderContext) { monitorType: 'browser', config: { screenshots: advancedConfig.screenshots, - schedule: '@every 3m', + schedule: '@every 10m', timeout: null, tags: [config.tags], throttling: '1337d/1338u/1339l', @@ -728,7 +728,7 @@ export default function (providerContext: FtrProviderContext) { monitorType: 'browser', config: { screenshots: advancedConfig.screenshots, - schedule: '@every 3m', + schedule: '@every 10m', timeout: null, tags: [config.tags], 'service.name': config.apmServiceName, diff --git a/x-pack/test/lists_api_integration/utils.ts b/x-pack/test/lists_api_integration/utils.ts index c0dc1d930a57e..67b6ed334ea7e 100644 --- a/x-pack/test/lists_api_integration/utils.ts +++ b/x-pack/test/lists_api_integration/utils.ts @@ -27,7 +27,7 @@ import { countDownTest } from '../detection_engine_api_integration/utils'; /** * Creates the lists and lists items index for use inside of beforeEach blocks of tests - * This will retry 20 times before giving up and hopefully still not interfere with other tests + * This will retry 50 times before giving up and hopefully still not interfere with other tests * @param supertest The supertest client library */ export const createListsIndex = async ( @@ -37,7 +37,9 @@ export const createListsIndex = async ( return countDownTest( async () => { await supertest.post(LIST_INDEX).set('kbn-xsrf', 'true').send(); - return true; + return { + passed: true, + }; }, 'createListsIndex', log @@ -55,7 +57,9 @@ export const deleteListsIndex = async ( return countDownTest( async () => { await supertest.delete(LIST_INDEX).set('kbn-xsrf', 'true').send(); - return true; + return { + passed: true, + }; }, 'deleteListsIndex', log @@ -64,7 +68,7 @@ export const deleteListsIndex = async ( /** * Creates the exception lists and lists items index for use inside of beforeEach blocks of tests - * This will retry 20 times before giving up and hopefully still not interfere with other tests + * This will retry 50 times before giving up and hopefully still not interfere with other tests * @param supertest The supertest client library */ export const createExceptionListsIndex = async ( @@ -74,7 +78,9 @@ export const createExceptionListsIndex = async ( return countDownTest( async () => { await supertest.post(LIST_INDEX).set('kbn-xsrf', 'true').send(); - return true; + return { + passed: true, + }; }, 'createListsIndex', log @@ -223,7 +229,9 @@ export const deleteAllExceptionsByType = async ( .get(`${EXCEPTION_LIST_URL}/_find?namespace_type=${type}`) .set('kbn-xsrf', 'true') .send(); - return finalCheck.data.length === 0; + return { + passed: finalCheck.data.length === 0, + }; }, `deleteAllExceptions by type: "${type}"`, log, diff --git a/x-pack/test/security_api_integration/session_idle.config.ts b/x-pack/test/security_api_integration/session_idle.config.ts index ee1fe3782a42a..0c78327c044eb 100644 --- a/x-pack/test/security_api_integration/session_idle.config.ts +++ b/x-pack/test/security_api_integration/session_idle.config.ts @@ -51,6 +51,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { saml_disable: { order: 3, realm: 'saml1', session: { idleTimeout: 0 } }, }, })}`, + // Exclude Uptime tasks to not interfere (additional ES load) with the session cleanup task. + `--xpack.task_manager.unsafe.exclude_task_types=${JSON.stringify(['UPTIME:*'])}`, ], }, diff --git a/x-pack/test/security_api_integration/session_lifespan.config.ts b/x-pack/test/security_api_integration/session_lifespan.config.ts index e236cbb8484d4..bc3ef851ceeca 100644 --- a/x-pack/test/security_api_integration/session_lifespan.config.ts +++ b/x-pack/test/security_api_integration/session_lifespan.config.ts @@ -51,6 +51,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { saml_disable: { order: 3, realm: 'saml1', session: { lifespan: 0 } }, }, })}`, + // Exclude Uptime tasks to not interfere (additional ES load) with the session cleanup task. + `--xpack.task_manager.unsafe.exclude_task_types=${JSON.stringify(['UPTIME:*'])}`, ], }, diff --git a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts index 1668d9bc1b811..ee8422fe728ab 100644 --- a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts +++ b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts @@ -9,7 +9,7 @@ import { parse as parseCookie, Cookie } from 'tough-cookie'; import { setTimeout as setTimeoutAsync } from 'timers/promises'; import expect from '@kbn/expect'; import { adminTestUser } from '@kbn/test'; -import type { AuthenticationProvider } from '../../../../plugins/security/common/model'; +import type { AuthenticationProvider } from '../../../../plugins/security/common'; import { getSAMLRequestId, getSAMLResponse } from '../../fixtures/saml/saml_tools'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -76,8 +76,7 @@ export default function ({ getService }: FtrProviderContext) { return cookie; } - // FLAKY: https://github.com/elastic/kibana/issues/121482 - describe.skip('Session Idle cleanup', () => { + describe('Session Idle cleanup', () => { beforeEach(async () => { await es.cluster.health({ index: '.kibana_security_session*', wait_for_status: 'green' }); await esDeleteAllIndices('.kibana_security_session*'); diff --git a/x-pack/test/security_solution_cypress/cases_cli_config.ts b/x-pack/test/security_solution_cypress/cases_cli_config.ts new file mode 100644 index 0000000000000..90fec2404e6f0 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cases_cli_config.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 { FtrConfigProviderContext } from '@kbn/test'; + +import { SecuritySolutionCypressCliCasesTestRunner } from './runner'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const securitySolutionCypressConfig = await readConfigFile(require.resolve('./config.ts')); + return { + ...securitySolutionCypressConfig.getAll(), + + testRunner: SecuritySolutionCypressCliCasesTestRunner, + }; +} diff --git a/x-pack/test/security_solution_cypress/runner.ts b/x-pack/test/security_solution_cypress/runner.ts index b537be4e80b41..21404b8b2e47f 100644 --- a/x-pack/test/security_solution_cypress/runner.ts +++ b/x-pack/test/security_solution_cypress/runner.ts @@ -38,6 +38,33 @@ export async function SecuritySolutionCypressCliTestRunner({ getService }: FtrPr }); } +export async function SecuritySolutionCypressCliCasesTestRunner({ + getService, +}: FtrProviderContext) { + const log = getService('log'); + const config = getService('config'); + const esArchiver = getService('esArchiver'); + + await esArchiver.load('x-pack/test/security_solution_cypress/es_archives/auditbeat'); + + await withProcRunner(log, async (procs) => { + await procs.run('cypress', { + cmd: 'yarn', + args: ['cypress:run:cases'], + cwd: resolve(__dirname, '../../plugins/security_solution'), + env: { + FORCE_COLOR: '1', + CYPRESS_BASE_URL: Url.format(config.get('servers.kibana')), + CYPRESS_ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')), + CYPRESS_ELASTICSEARCH_USERNAME: config.get('servers.elasticsearch.username'), + CYPRESS_ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'), + ...process.env, + }, + wait: true, + }); + }); +} + export async function SecuritySolutionCypressCliFirefoxTestRunner({ getService, }: FtrProviderContext) {